'use strict';

const { app, BrowserView, BrowserWindow, ipcMain, powerSaveBlocker, shell, dialog, globalShortcut } = require('electron');
const { spawn } = require('node:child_process');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');

// ────────────────────────────────────────────────────────────────────────────
// 防节流：Chromium 进程级开关。比单 webPreferences 更彻底。
// - disable-background-timer-throttling: 后台窗口里的 setTimeout/setInterval
//   不再被限制到 1/秒（默认 30 秒后会进一步降到 1/分钟）。
// - disable-renderer-backgrounding: 后台标签的渲染进程不再被降优先级。
// - disable-backgrounding-occluded-windows: 即使被其他窗口完全遮挡也不被
//   认为是"backgrounded"，避免 5 分钟后整个渲染进程被挂起。
// 必须在 app.whenReady() 之前 append。
// ────────────────────────────────────────────────────────────────────────────
app.commandLine.appendSwitch('disable-background-timer-throttling');
app.commandLine.appendSwitch('disable-renderer-backgrounding');
app.commandLine.appendSwitch('disable-backgrounding-occluded-windows');

const PDD_LOGIN_URL = 'https://mms.pinduoduo.com/login';
const PDD_WORKBENCH_URL = 'https://mms.pinduoduo.com/chat-merchant/index.html';
const DOUYIN_LOGIN_URL = 'https://fxg.jinritemai.com/login';
const DOUYIN_WORKBENCH_URL = 'https://im.jinritemai.com/';
const LOCAL_DASHBOARD_URL = process.env.AICS_AGENT_DASHBOARD_URL || 'http://127.0.0.1:3789';
const ELECTRON_CDP_PORT = process.env.AICS_ELECTRON_CDP_PORT || '9223';
const ELECTRON_CDP_URL = `http://127.0.0.1:${ELECTRON_CDP_PORT}`;
const CONTROLLED_WORKBENCH_CONFIG_PATH = path.join(os.homedir(), '.aics-agent', 'controlled-workbench.json');
const AGENT_CONFIG_PATH = path.join(os.homedir(), '.aics-agent', 'config.json');

function readSavedWorkbenchConfig() {
  try {
    const data = JSON.parse(fs.readFileSync(CONTROLLED_WORKBENCH_CONFIG_PATH, 'utf8'));
    return data && typeof data === 'object' ? data : {};
  } catch {
    return {};
  }
}

function writeSavedWorkbenchConfig(patch) {
  const next = {
    ...readSavedWorkbenchConfig(),
    ...patch,
    updatedAt: new Date().toISOString(),
  };
  fs.mkdirSync(path.dirname(CONTROLLED_WORKBENCH_CONFIG_PATH), { recursive: true });
  fs.writeFileSync(CONTROLLED_WORKBENCH_CONFIG_PATH, JSON.stringify(next, null, 2));
  return next;
}

function readSavedWorkbenchPlatform() {
  const data = readSavedWorkbenchConfig();
  return typeof data.platform === 'string' ? data.platform : '';
}

function normalizeWorkbenchPlatform(value) {
  const platform = String(value || '').toLowerCase();
  return ['pdd', 'douyin'].includes(platform) ? platform : 'pdd';
}

function readSavedPlatformRuntimeUrl(platform) {
  const data = readSavedWorkbenchConfig();
  const urls = data.lastRuntimeUrls && typeof data.lastRuntimeUrls === 'object' ? data.lastRuntimeUrls : {};
  const url = typeof urls[platform] === 'string' ? urls[platform] : '';
  if (!url) return '';
  try {
    const parsed = new URL(url);
    if (platform === 'pdd') {
      return parsed.protocol === 'https:' && (
        parsed.hostname === 'pinduoduo.com'
        || parsed.hostname.endsWith('.pinduoduo.com')
        || parsed.hostname === 'yangkeduo.com'
        || parsed.hostname.endsWith('.yangkeduo.com')
      ) ? url : '';
    }
    if (platform === 'douyin') {
      return parsed.protocol === 'https:' && (
        parsed.hostname === 'jinritemai.com'
        || parsed.hostname.endsWith('.jinritemai.com')
      ) ? url : '';
    }
  } catch {
    return '';
  }
  return '';
}

let activeWorkbenchPlatform = normalizeWorkbenchPlatform(
  process.env.AICS_WORKBENCH_PLATFORM || readSavedWorkbenchPlatform() || 'pdd'
);
const PLATFORM_CONFIGS = {
  pdd: {
    platform: 'pdd',
    label: '拼多多',
    workbenchLabel: '拼多多客服页',
    loginUrl: PDD_LOGIN_URL,
    workbenchUrl: PDD_WORKBENCH_URL,
    configuredUrl: process.env.AICS_PDD_WORKBENCH_URL || readSavedPlatformRuntimeUrl('pdd') || PDD_WORKBENCH_URL,
    source: process.env.AICS_PDD_WORKBENCH_URL ? 'MOCK_TEST' : 'REAL_PDD',
    accountKey: process.env.AICS_PDD_ACCOUNT_KEY || 'pdd-main-shop',
    accountName: process.env.AICS_PDD_ACCOUNT_NAME || '拼多多主账号',
    requireManualWorkbenchPopup: true,
    mockEnvName: 'AICS_PDD_WORKBENCH_URL',
    defaultAddressLabel: 'PDD 默认地址',
    allowUrl(url) {
      return (
        url.hostname === 'pinduoduo.com'
        || url.hostname.endsWith('.pinduoduo.com')
        || url.hostname === 'yangkeduo.com'
        || url.hostname.endsWith('.yangkeduo.com')
      );
    },
  },
  douyin: {
    platform: 'douyin',
    label: '抖店飞鸽',
    workbenchLabel: '抖店飞鸽客服页',
    loginUrl: DOUYIN_LOGIN_URL,
    workbenchUrl: DOUYIN_WORKBENCH_URL,
    configuredUrl: process.env.AICS_DOUYIN_WORKBENCH_URL || readSavedPlatformRuntimeUrl('douyin') || DOUYIN_LOGIN_URL,
    source: process.env.AICS_DOUYIN_WORKBENCH_URL ? 'MOCK_TEST' : 'REAL_DOUYIN',
    accountKey: process.env.AICS_DOUYIN_ACCOUNT_KEY || 'douyin-main-shop',
    accountName: process.env.AICS_DOUYIN_ACCOUNT_NAME || '飞鸽主账号',
    requireManualWorkbenchPopup: false,
    mockEnvName: 'AICS_DOUYIN_WORKBENCH_URL',
    defaultAddressLabel: '飞鸽登录地址',
    allowUrl(url) {
      return url.hostname === 'jinritemai.com' || url.hostname.endsWith('.jinritemai.com');
    },
  },
};
const PLATFORM_IDS = Object.keys(PLATFORM_CONFIGS);
function getWorkbenchConfig() {
  return PLATFORM_CONFIGS[activeWorkbenchPlatform] || PLATFORM_CONFIGS.pdd;
}
function getPlatformConfig(platform) {
  return PLATFORM_CONFIGS[normalizeWorkbenchPlatform(platform)] || PLATFORM_CONFIGS.pdd;
}
function getWorkbenchUrl() {
  return getWorkbenchConfig().configuredUrl;
}
function getWorkbenchSource() {
  return getWorkbenchConfig().source;
}
function getAllowedPlatformsFromStatus(status = latestStatus) {
  if (Array.isArray(status?.allowedPlatforms)) return status.allowedPlatforms;
  if (Array.isArray(status?.platformGrants)) {
    return status.platformGrants
      .filter((grant) => grant && grant.enabled)
      .map((grant) => grant.platform);
  }
  return PLATFORM_IDS;
}
function getAllowedPlatforms() {
  return getAllowedPlatformsFromStatus(latestStatus);
}
function hasUsableAuthorization(status = latestStatus) {
  return Boolean(status?.connected) && getAllowedPlatformsFromStatus(status).length > 0;
}
function platformAuthorizationMessage(status = latestStatus) {
  if (status?.connected) {
    return '当前客户端尚未分配平台席位。请重新输入绑定码，或联系蜂序运营后台为该客户开通平台。';
  }
  return '请先输入绑定码完成连接。';
}
function isPlatformAllowed(platform) {
  return getAllowedPlatforms().includes(normalizeWorkbenchPlatform(platform));
}
function saveWorkbenchPlatform(platform) {
  writeSavedWorkbenchConfig({ platform });
}

function savePlatformRuntimeUrl(platform, url) {
  const id = normalizeWorkbenchPlatform(platform);
  if (!isWorkbenchRuntimeUrl(url, id)) return;
  const data = readSavedWorkbenchConfig();
  writeSavedWorkbenchConfig({
    lastRuntimeUrls: {
      ...(data.lastRuntimeUrls && typeof data.lastRuntimeUrls === 'object' ? data.lastRuntimeUrls : {}),
      [id]: url,
    },
  });
}

function clearSavedPlatformRuntimeUrl(platform) {
  const id = normalizeWorkbenchPlatform(platform);
  const data = readSavedWorkbenchConfig();
  const lastRuntimeUrls = { ...(data.lastRuntimeUrls && typeof data.lastRuntimeUrls === 'object' ? data.lastRuntimeUrls : {}) };
  delete lastRuntimeUrls[id];
  writeSavedWorkbenchConfig({ lastRuntimeUrls });
}

app.commandLine.appendSwitch('remote-debugging-port', ELECTRON_CDP_PORT);
app.commandLine.appendSwitch('remote-allow-origins', '*');

const MODES = {
  AI_AUTO: 'AI_AUTO',
  AI_PAUSED: 'AI_PAUSED',
  AI_SUGGEST_ONLY: 'AI_SUGGEST_ONLY',
  HUMAN_ACTIVE: 'HUMAN_ACTIVE',
  AI_SENDING: 'AI_SENDING',
};

const OPERATOR_ALERT_REASON_LABELS = {
  image_message: '客户发送图片',
  keyword: '命中人工关键词',
  order_action: '订单操作请求',
  low_confidence: '问题需要确认',
  manual: '客户要求人工',
};
const OPERATOR_ALERT_MAX_AGE_MS = Math.max(
  30_000,
  Number(process.env.AICS_OPERATOR_ALERT_MAX_AGE_MS || 5 * 60 * 1000),
);

const TABS = {
  PDD: 'pdd',
  DOUYIN: 'douyin',
  KNOWLEDGE: 'knowledge',
};

let mainWindow = null;
const platformStates = Object.fromEntries(PLATFORM_IDS.map((platform) => [platform, {
  view: null,
  detachedControlView: null,
  detachedWindow: null,
  mode: MODES.AI_PAUSED,
  controlVersion: 1,
  reason: 'startup_default',
  updatedAt: new Date().toISOString(),
  lastError: '',
  lastPopupUrl: '',
  lastPopupAt: '',
  driverStarting: false,
  controlBusy: false,
  controlPendingMode: '',
  controlAbortVersion: 0,
  handoffDetail: null,
}]));
let overlayView = null;
let overlayRenderKey = '';
let dashboardProcess = null;
let activeTab = TABS.PDD;
let shellMode = 'booting';
let latestStatus = null;
let lastError = '';
let loginBusy = false;
let appQuitCleanupDone = false;
let dashboardEventsAbort = null;
let latestOperatorAlert = null;
const ENABLE_POWER_SAVE_BLOCKER = process.env.AICS_ENABLE_POWER_SAVE_BLOCKER === '1';
const timeline = [];

function appRoot() {
  return path.resolve(__dirname, '..');
}

function appDir() {
  return __dirname;
}

function logEvent(kind, payload = {}) {
  const event = {
    at: new Date().toISOString(),
    kind,
    payload,
  };
  timeline.push(event);
  if (timeline.length > 400) timeline.splice(0, timeline.length - 400);
  if (mainWindow && !mainWindow.isDestroyed()) {
    mainWindow.webContents.send('timeline:update', event);
  }
}

function getPlatformState(platform = activeWorkbenchPlatform) {
  return platformStates[normalizeWorkbenchPlatform(platform)] || platformStates.pdd;
}

function getWorkbenchView(platform = activeWorkbenchPlatform) {
  return getPlatformState(platform).view;
}

function controlSnapshot(platform = activeWorkbenchPlatform) {
  const state = getPlatformState(platform);
  return {
    mode: state.mode,
    controlVersion: state.controlVersion,
    reason: state.reason,
    updatedAt: state.updatedAt,
    handoffDetail: state.handoffDetail || null,
    overlayLocked: state.mode === MODES.AI_AUTO || state.mode === MODES.AI_SENDING,
    busy: Boolean(state.controlBusy),
    pendingMode: state.controlPendingMode || '',
  };
}

function shouldApplyIncomingControl(state, incoming) {
  const incomingVersion = Number(incoming.controlVersion || 0);
  const localVersion = Number(state.controlVersion || 0);
  if (incomingVersion > localVersion) return true;
  if (incomingVersion < localVersion) return false;
  const incomingAt = Date.parse(incoming.updatedAt || '');
  const localAt = Date.parse(state.updatedAt || '');
  if (Number.isFinite(incomingAt) && Number.isFinite(localAt)) {
    return incomingAt >= localAt;
  }
  return true;
}

function applyDashboardControl(control) {
  let changed = false;
  if (latestStatus?.platformControls && typeof latestStatus.platformControls === 'object') {
    for (const platform of PLATFORM_IDS) {
      const incoming = latestStatus.platformControls[platform];
      if (!incoming || !Object.values(MODES).includes(incoming.mode)) continue;
      const state = getPlatformState(platform);
      if (!shouldApplyIncomingControl(state, incoming)) continue;
      const nextVersion = Number(incoming.controlVersion || state.controlVersion || 1);
      const nextReason = incoming.reason || state.reason;
      const nextUpdatedAt = incoming.updatedAt || state.updatedAt;
      changed = changed
        || state.mode !== incoming.mode
        || state.controlVersion !== nextVersion
        || state.reason !== nextReason
        || state.updatedAt !== nextUpdatedAt;
      state.mode = incoming.mode;
      state.controlVersion = nextVersion;
      state.reason = nextReason;
      state.updatedAt = nextUpdatedAt;
      if (incoming.mode === MODES.AI_AUTO) {
        if (state.handoffDetail || latestOperatorAlert?.platform === platform) changed = true;
        state.handoffDetail = null;
        if (latestOperatorAlert?.platform === platform) latestOperatorAlert = null;
      }
    }
    return changed;
  }
  if (!control || !Object.values(MODES).includes(control.mode)) return false;
  const state = getPlatformState();
  if (!shouldApplyIncomingControl(state, control)) return false;
  const nextVersion = Number(control.controlVersion || state.controlVersion || 1);
  const nextReason = control.reason || state.reason;
  const nextUpdatedAt = control.updatedAt || state.updatedAt;
  changed = changed
    || state.mode !== control.mode
    || state.controlVersion !== nextVersion
    || state.reason !== nextReason
    || state.updatedAt !== nextUpdatedAt;
  state.mode = control.mode;
  state.controlVersion = nextVersion;
  state.reason = nextReason;
  state.updatedAt = nextUpdatedAt;
  if (control.mode === MODES.AI_AUTO) {
    if (state.handoffDetail || latestOperatorAlert?.platform === activeWorkbenchPlatform) changed = true;
    state.handoffDetail = null;
    if (latestOperatorAlert?.platform === activeWorkbenchPlatform) latestOperatorAlert = null;
  }
  return changed;
}

function stateSnapshot() {
  const config = getWorkbenchConfig();
  const source = getWorkbenchSource();
  const configuredUrl = getWorkbenchUrl();
  const platforms = {};
  const allowedPlatforms = getAllowedPlatforms();
  const grantByPlatform = Object.fromEntries(
    (Array.isArray(latestStatus?.platformGrants) ? latestStatus.platformGrants : [])
      .filter((grant) => grant && grant.platform)
      .map((grant) => [grant.platform, grant])
  );
  for (const platform of PLATFORM_IDS) {
    const platformConfig = getPlatformConfig(platform);
    const state = getPlatformState(platform);
    const view = state.view;
    const platformSource = platformConfig.source;
    platforms[platform] = {
      id: platformConfig.platform,
      label: platformConfig.label,
      workbenchLabel: platformConfig.workbenchLabel,
      loginUrl: platformConfig.loginUrl,
      workbenchUrl: platformConfig.workbenchUrl,
      configuredUrl: platformConfig.configuredUrl,
      source: platformSource,
      isMock: platformSource === 'MOCK_TEST',
      url: view?.webContents.getURL() || platformConfig.configuredUrl,
      detached: Boolean(state.detachedWindow && !state.detachedWindow.isDestroyed()),
      lastPopupUrl: state.lastPopupUrl,
      lastPopupAt: state.lastPopupAt,
      lastError: state.lastError,
      driverStarting: state.driverStarting,
      control: controlSnapshot(platform),
      accountKey: platformConfig.accountKey,
      accountName: platformConfig.accountName,
      allowed: allowedPlatforms.includes(platform),
      grant: grantByPlatform[platform] || null,
    };
  }
  return {
    shellMode,
    platform: {
      id: config.platform,
      label: config.label,
      workbenchLabel: config.workbenchLabel,
      loginUrl: config.loginUrl,
      workbenchUrl: config.workbenchUrl,
      requireManualWorkbenchPopup: config.requireManualWorkbenchPopup,
      configPath: CONTROLLED_WORKBENCH_CONFIG_PATH,
    },
    activeTab,
    activePlatform: activeWorkbenchPlatform,
    platforms,
    allowedPlatforms,
    control: controlSnapshot(),
    workbench: {
      url: getWorkbenchView()?.webContents.getURL() || configuredUrl,
      configuredUrl,
      loginUrl: config.loginUrl,
      workbenchUrl: config.workbenchUrl,
      source,
      isMock: source === 'MOCK_TEST',
      lastPopupUrl: getPlatformState().lastPopupUrl,
      lastPopupAt: getPlatformState().lastPopupAt,
    },
    dashboard: {
      url: LOCAL_DASHBOARD_URL,
      status: latestStatus,
      running: Boolean(latestStatus),
    },
    embeddedDriver: {
      cdpUrl: ELECTRON_CDP_URL,
      platform: config.platform,
      accountKey: config.accountKey,
      accountName: config.accountName,
      starting: getPlatformState().driverStarting,
    },
    lastError: getPlatformState().lastError || lastError,
    operatorAlert: latestOperatorAlert,
    timeline,
  };
}

// 防 macOS/Windows 在 AI 托管期间挂起整个 app（合盖、休眠、Energy Saver）。
// `prevent-app-suspension` 不锁屏（屏幕仍可黑），仅保证进程不被 OS suspend。
// 仅在 AI_AUTO 或 AI_SENDING 期间持锁；切回 PAUSED/HUMAN 立即释放，免得无谓耗电。
let powerBlockerId = null;
function ensurePowerSaveBlockerForMode(mode) {
  if (!ENABLE_POWER_SAVE_BLOCKER) {
    if (powerBlockerId !== null && powerSaveBlocker.isStarted(powerBlockerId)) {
      powerSaveBlocker.stop(powerBlockerId);
    }
    powerBlockerId = null;
    return;
  }
  const wantBlock = mode === MODES.AI_AUTO || mode === MODES.AI_SENDING;
  if (wantBlock && powerBlockerId === null) {
    try {
      powerBlockerId = powerSaveBlocker.start('prevent-app-suspension');
      logEvent('POWER_SAVE_BLOCKER_STARTED', { id: powerBlockerId, mode });
    } catch (err) {
      logEvent('POWER_SAVE_BLOCKER_FAILED', { error: String(err) });
    }
  } else if (!wantBlock && powerBlockerId !== null) {
    try {
      powerSaveBlocker.stop(powerBlockerId);
      logEvent('POWER_SAVE_BLOCKER_STOPPED', { id: powerBlockerId, mode });
    } catch (err) {
      logEvent('POWER_SAVE_BLOCKER_STOP_FAILED', { error: String(err) });
    }
    powerBlockerId = null;
  }
}

function normalizeHandoffDetail(detail) {
  if (!detail || typeof detail !== 'object') return null;
  const reason = typeof detail.reason === 'string' ? detail.reason.slice(0, 80) : '';
  const ruleLabel = typeof detail.ruleLabel === 'string' ? detail.ruleLabel.slice(0, 80) : '';
  const messagePreview = typeof detail.messagePreview === 'string' ? detail.messagePreview.slice(0, 500) : '';
  const customerName = typeof detail.customerName === 'string' ? detail.customerName.slice(0, 120) : '';
  const externalChatId = typeof detail.externalChatId === 'string' ? detail.externalChatId.slice(0, 160) : '';
  const source = typeof detail.source === 'string' ? detail.source.slice(0, 80) : '';
  if (!reason && !ruleLabel && !messagePreview && !customerName && !externalChatId && !source) return null;
  return {
    reason,
    ruleLabel,
    messagePreview,
    customerName,
    externalChatId,
    source,
    at: new Date().toISOString(),
  };
}

function setControlMode(mode, reason, platform = activeWorkbenchPlatform, handoffDetail = null) {
  if (!Object.values(MODES).includes(mode)) return controlSnapshot();
  const state = getPlatformState(platform);
  state.mode = mode;
  state.controlVersion += 1;
  state.reason = reason || 'local_control';
  state.updatedAt = new Date().toISOString();
  if (mode === MODES.HUMAN_ACTIVE) {
    state.handoffDetail = normalizeHandoffDetail(handoffDetail) || state.handoffDetail || {
      reason: reason || 'manual',
      ruleLabel: '操作员手动切换',
      messagePreview: '',
      customerName: '',
      externalChatId: '',
      source: 'manual',
      at: state.updatedAt,
    };
    if (latestOperatorAlert?.platform === platform) latestOperatorAlert = null;
  } else if (mode === MODES.AI_AUTO) {
    state.handoffDetail = null;
    if (latestOperatorAlert?.platform === platform) latestOperatorAlert = null;
  } else if ((mode === MODES.AI_PAUSED || mode === MODES.AI_SUGGEST_ONLY) && latestOperatorAlert?.platform === platform) {
    latestOperatorAlert = null;
  }
  const anyAuto = PLATFORM_IDS.some((id) => {
    const m = getPlatformState(id).mode;
    return m === MODES.AI_AUTO || m === MODES.AI_SENDING;
  });
  ensurePowerSaveBlockerForMode(anyAuto ? MODES.AI_AUTO : MODES.AI_PAUSED);
  updateViewStack();
  const snapshot = controlSnapshot(platform);
  logEvent('CONTROL_MODE_CHANGED', { platform, ...snapshot });
  mainWindow?.webContents.send('control:update', snapshot);
  return snapshot;
}

function dismissOperatorAlert(platform = activeWorkbenchPlatform, reason = 'operator_alert_dismiss') {
  const id = normalizeWorkbenchPlatform(platform);
  const hadAlert = latestOperatorAlert?.platform === id;
  if (hadAlert) latestOperatorAlert = null;
  logEvent('OPERATOR_ALERT_DISMISSED', { platform: id, reason, hadAlert });
  mainWindow?.webContents.send('state:update', stateSnapshot());
  return stateSnapshot();
}

function isEmergencyControlMode(mode) {
  return mode === MODES.AI_PAUSED || mode === MODES.HUMAN_ACTIVE;
}

function isWorkbenchRuntimeUrl(url, platform = activeWorkbenchPlatform) {
  const config = getPlatformConfig(platform);
  if (config.source === 'MOCK_TEST') return Boolean(url);
  return typeof url === 'string' && url.startsWith(config.workbenchUrl);
}

function isSafeExternalUrl(url) {
  try {
    const parsed = new URL(url);
    return parsed.protocol === 'https:' || parsed.protocol === 'http:';
  } catch {
    return false;
  }
}

function isBlockedWorkbenchProtocol(url) {
  try {
    const parsed = new URL(url);
    return !['http:', 'https:', 'about:', 'data:', 'blob:'].includes(parsed.protocol);
  } catch {
    return true;
  }
}

function isPlatformUrl(url, platform = activeWorkbenchPlatform) {
  const config = getPlatformConfig(platform);
  try {
    const parsed = new URL(url);
    return parsed.protocol === 'https:' && config.allowUrl(parsed);
  } catch {
    return false;
  }
}

function loadWorkbenchUrl(url, reason, platform = activeWorkbenchPlatform) {
  const view = getWorkbenchView(platform);
  if (!view || !url) return;
  activeTab = platform;
  activeWorkbenchPlatform = platform;
  logEvent('WORKBENCH_LOAD_REQUESTED', { platform, url, reason });
  view.webContents.loadURL(url);
  updateViewStack();
}

function handleWorkbenchPopup(platform, url, details = {}) {
  const config = getPlatformConfig(platform);
  const source = config.source;
  const state = getPlatformState(platform);
  state.lastPopupUrl = url || '';
  state.lastPopupAt = new Date().toISOString();
  logEvent('WORKBENCH_POPUP_INTERCEPTED', {
    platform,
    url: state.lastPopupUrl || '(empty)',
    frameName: details.frameName || '',
    disposition: details.disposition || '',
    source,
  });

  if (!url || url === 'about:blank') {
    if (config.requireManualWorkbenchPopup) {
      loadWorkbenchUrl(config.workbenchUrl, 'popup_blank_fallback', platform);
    } else {
      logEvent('WORKBENCH_POPUP_IGNORED', { reason: 'blank_popup_without_manual_popup_model' });
    }
    return;
  }

  if (isBlockedWorkbenchProtocol(url)) {
    logEvent('WORKBENCH_EXTERNAL_PROTOCOL_BLOCKED', { platform, url });
    return;
  }

  if (source === 'MOCK_TEST' || isPlatformUrl(url, platform)) {
    loadWorkbenchUrl(url, 'popup_intercept', platform);
    return;
  }

  logEvent('WORKBENCH_POPUP_BLOCKED', {
    url,
    reason: isSafeExternalUrl(url) ? `non_${config.platform}_external_blocked` : 'unsupported_url',
  });
}

function attachWorkbenchNavigationGuards(platform, contents, label) {
  contents.setWindowOpenHandler((details) => {
    handleWorkbenchPopup(platform, details.url, details);
    return { action: 'deny' };
  });
  contents.on('will-navigate', (_event, url) => {
    logEvent('WORKBENCH_WILL_NAVIGATE', { platform, label, url });
  });
  contents.on('did-navigate', (_event, url) => {
    savePlatformRuntimeUrl(platform, url);
    logEvent('WORKBENCH_DID_NAVIGATE', { platform, label, url });
    mainWindow?.webContents.send('state:update', stateSnapshot());
  });
  contents.on('did-navigate-in-page', (_event, url) => {
    savePlatformRuntimeUrl(platform, url);
    logEvent('WORKBENCH_DID_NAVIGATE_IN_PAGE', { platform, label, url });
    mainWindow?.webContents.send('state:update', stateSnapshot());
  });
}

async function fetchJson(url, timeoutMs = 8000) {
  const res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
  if (!res.ok) throw new Error(`HTTP ${res.status}: ${url}`);
  return res.json();
}

async function postJson(url, body, timeoutMs = 8000) {
  const res = await fetch(url, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
    signal: AbortSignal.timeout(timeoutMs),
  });
  const text = await res.text();
  let data = {};
  if (text) {
    try {
      data = JSON.parse(text);
    } catch {
      data = { text };
    }
  }
  if (!res.ok || data.success === false) {
    throw new Error(data.error || data.message || `HTTP ${res.status}: ${url}`);
  }
  return data;
}

function readAgentConfig() {
  try {
    return JSON.parse(fs.readFileSync(AGENT_CONFIG_PATH, 'utf8'));
  } catch {
    return {};
  }
}

function cloudHttpBaseFromAgentConfig(config = readAgentConfig()) {
  const wsUrl = String(config.cloudWsUrl || '').trim();
  if (!wsUrl) throw new Error('客户端尚未连接服务端');
  const parsed = new URL(wsUrl);
  const secure = parsed.protocol === 'wss:' || parsed.protocol === 'https:';
  return `${secure ? 'https' : 'http'}://${parsed.host}`;
}

async function cloudKnowledgeRequest(pathname, init = {}) {
  const config = readAgentConfig();
  if (!config.jwtToken) throw new Error('客户端尚未连接服务端');
  const url = `${cloudHttpBaseFromAgentConfig(config)}${pathname}`;
  const headers = { Authorization: `Bearer ${config.jwtToken}` };
  if (!init.formData) headers['Content-Type'] = 'application/json';
  const res = await fetch(url, {
    method: init.method || 'GET',
    headers,
    body: init.formData || (init.body === undefined ? undefined : JSON.stringify(init.body)),
  });
  const text = await res.text();
  let data = {};
  if (text) {
    try {
      data = JSON.parse(text);
    } catch {
      data = { text };
    }
  }
  if (!res.ok || data.success === false) {
    throw new Error(data.error || data.message || `HTTP ${res.status}: ${pathname}`);
  }
  return data;
}

async function checkDashboard() {
  try {
    const data = await fetchJson(`${LOCAL_DASHBOARD_URL}/api/status`);
    latestStatus = data.snapshot || data;
    const controlChanged = applyDashboardControl(latestStatus.control);
    if (latestStatus?.connected && !hasUsableAuthorization(latestStatus)) {
      shellMode = 'login';
      lastError = platformAuthorizationMessage(latestStatus);
    } else if (shellMode !== 'login') {
      lastError = '';
    }
    if (controlChanged) updateViewStack();
    mainWindow?.webContents.send('dashboard:status', latestStatus);
    startDashboardEventStream();
    return true;
  } catch (err) {
    latestStatus = null;
    lastError = err instanceof Error ? err.message : String(err);
    return false;
  }
}

function startDashboardEventStream() {
  if (dashboardEventsAbort) return;
  dashboardEventsAbort = new AbortController();
  const signal = dashboardEventsAbort.signal;
  void (async () => {
    try {
      const res = await fetch(`${LOCAL_DASHBOARD_URL}/api/events`, { signal });
      if (!res.ok || !res.body) throw new Error(`HTTP ${res.status}: /api/events`);
      logEvent('DASHBOARD_EVENTS_CONNECTED', { url: `${LOCAL_DASHBOARD_URL}/api/events` });
      const reader = res.body.getReader();
      const decoder = new TextDecoder();
      let buffer = '';
      while (!signal.aborted) {
        const { value, done } = await reader.read();
        if (done) break;
        buffer += decoder.decode(value, { stream: true });
        const chunks = buffer.split('\n\n');
        buffer = chunks.pop() || '';
        for (const chunk of chunks) {
          const dataLine = chunk.split('\n').find((line) => line.startsWith('data: '));
          if (!dataLine) continue;
          let event;
          try {
            event = JSON.parse(dataLine.slice(6));
          } catch {
            continue;
          }
          const nestedKind = event.payload?.kind || event.kind || event.type || 'event';
          if (nestedKind === 'handoff_alert') {
            handleDashboardHandoffAlert(event);
          }
          logEvent(`DASHBOARD_${String(nestedKind).toUpperCase()}`, event);
        }
      }
    } catch (err) {
      if (!signal.aborted) {
        logEvent('DASHBOARD_EVENTS_DISCONNECTED', {
          error: err instanceof Error ? err.message : String(err),
        });
      }
    } finally {
      if (dashboardEventsAbort?.signal === signal) dashboardEventsAbort = null;
    }
  })();
}

function normalizeDashboardHandoffAlert(event) {
  const payload = event?.payload || event || {};
  return payload.payload || payload;
}

function parseOperatorAlertTimeMs(value) {
  if (typeof value === 'number' && Number.isFinite(value)) return value;
  if (typeof value === 'string' && value.trim()) {
    const numeric = Number(value);
    if (Number.isFinite(numeric)) return numeric;
    const parsed = Date.parse(value);
    if (Number.isFinite(parsed)) return parsed;
  }
  return 0;
}

function getOperatorAlertTimeMs(alert, event) {
  return (
    parseOperatorAlertTimeMs(alert?.at)
    || parseOperatorAlertTimeMs(alert?.timestamp)
    || parseOperatorAlertTimeMs(event?.at)
    || parseOperatorAlertTimeMs(event?.timestamp)
    || parseOperatorAlertTimeMs(event?.payload?.at)
    || parseOperatorAlertTimeMs(event?.payload?.timestamp)
    || 0
  );
}

function isFreshDashboardHandoffAlert(alert, event) {
  const alertTimeMs = getOperatorAlertTimeMs(alert, event);
  if (!alertTimeMs) return true;
  const ageMs = Date.now() - alertTimeMs;
  return ageMs >= -60_000 && ageMs <= OPERATOR_ALERT_MAX_AGE_MS;
}

function handoffDetailFromAlert(alert) {
  const reason = alert?.reason || 'operator_alert';
  return {
    reason,
    ruleLabel: OPERATOR_ALERT_REASON_LABELS[reason] || '需要人工接管',
    messagePreview: alert?.textPreview || alert?.messagePreview || '',
    customerName: alert?.customerName || '',
    externalChatId: alert?.externalChatId || '',
    source: alert?.source || 'operator_alert',
  };
}

function handleDashboardHandoffAlert(event) {
  const alert = normalizeDashboardHandoffAlert(event);
  const platform = normalizeWorkbenchPlatform(alert.platform || activeWorkbenchPlatform);
  if (!isFreshDashboardHandoffAlert(alert, event)) {
    logEvent('HANDOFF_ALERT_STALE_IGNORED', {
      platform,
      reason: alert.reason,
      externalChatId: alert.externalChatId,
      ageMs: Date.now() - getOperatorAlertTimeMs(alert, event),
    });
    return;
  }
  latestOperatorAlert = {
    ...alert,
    platform,
    receivedAt: Date.now(),
  };
  logEvent('HANDOFF_ALERT_RECEIVED', {
    platform,
    reason: latestOperatorAlert.reason,
    externalChatId: latestOperatorAlert.externalChatId,
    cmdId: latestOperatorAlert.cmdId,
  });
  mainWindow?.webContents.send('operator-alert:show', latestOperatorAlert);
  mainWindow?.webContents.send('state:update', stateSnapshot());
}

async function waitForDashboard(timeoutMs = 20_000) {
  const started = Date.now();
  while (Date.now() - started < timeoutMs) {
    if (await checkDashboard()) return true;
    await new Promise((resolve) => setTimeout(resolve, 500));
  }
  return false;
}

async function ensureDashboardRunning() {
  if (await checkDashboard()) {
    logEvent('DASHBOARD_ALREADY_RUNNING', { url: LOCAL_DASHBOARD_URL });
    return true;
  }

  const root = appRoot();
  const nodePath = process.platform === 'win32'
    ? path.join(root, 'bin', 'node.exe')
    : path.join(root, 'bin', 'node');
  const mainPath = path.join(appDir(), 'main.cjs');
  if (!fs.existsSync(nodePath) || !fs.existsSync(mainPath)) {
    lastError = `Local dashboard runtime not found: ${nodePath}`;
    logEvent('DASHBOARD_START_SKIPPED', { reason: lastError });
    return false;
  }

  dashboardProcess = spawn(nodePath, [mainPath, 'ui', '--no-open'], {
    cwd: root,
    env: {
      ...process.env,
      AICS_AGENT_UI_PORT: new URL(LOCAL_DASHBOARD_URL).port || '3789',
    },
    stdio: ['ignore', 'pipe', 'pipe'],
  });
  logEvent('DASHBOARD_STARTING', { pid: dashboardProcess.pid, url: LOCAL_DASHBOARD_URL });
  dashboardProcess.stdout.on('data', (chunk) => logEvent('DASHBOARD_STDOUT', { text: String(chunk).trim().slice(0, 500) }));
  dashboardProcess.stderr.on('data', (chunk) => logEvent('DASHBOARD_STDERR', { text: String(chunk).trim().slice(0, 500) }));
  dashboardProcess.on('exit', (code, signal) => {
    logEvent('DASHBOARD_EXIT', { code, signal });
    dashboardProcess = null;
  });
  const ok = await waitForDashboard();
  if (!ok) logEvent('DASHBOARD_START_TIMEOUT', { url: LOCAL_DASHBOARD_URL, error: lastError });
  return ok;
}

async function syncControlModeToDashboard(mode, reason, platform = activeWorkbenchPlatform) {
  const data = await postJson(`${LOCAL_DASHBOARD_URL}/api/control/mode`, { mode, reason, platform }, 6000);
  logEvent('DASHBOARD_CONTROL_SYNCED', { platform, mode, reason, control: data.control });
  return data.control;
}

async function stopDashboardDrivers(reason = 'lifecycle_cleanup') {
  let status = latestStatus;
  try {
    if (!status) {
      const data = await fetchJson(`${LOCAL_DASHBOARD_URL}/api/status`);
      status = data.snapshot || data;
    }
  } catch (err) {
    logEvent('DASHBOARD_DRIVER_CLEANUP_SKIPPED', {
      reason,
      error: err instanceof Error ? err.message : String(err),
    });
    return;
  }

  const drivers = Array.isArray(status?.drivers) ? status.drivers : [];
  for (const driver of drivers) {
    if (!driver?.id) continue;
    try {
      await postJson(`${LOCAL_DASHBOARD_URL}/api/drivers/stop`, { driverId: driver.id });
      logEvent('DASHBOARD_DRIVER_STOPPED', { reason, driverId: driver.id });
    } catch (err) {
      logEvent('DASHBOARD_DRIVER_STOP_FAILED', {
        reason,
        driverId: driver.id,
        error: err instanceof Error ? err.message : String(err),
      });
    }
  }

  for (const platform of PLATFORM_IDS) {
    try {
      await syncControlModeToDashboard(MODES.AI_PAUSED, reason, platform);
    } catch (err) {
      logEvent('DASHBOARD_CONTROL_PAUSE_FAILED', {
        reason,
        platform,
        error: err instanceof Error ? err.message : String(err),
      });
    }
  }
  await checkDashboard().catch(() => undefined);
}

async function startEmbeddedDriver(platform, config) {
  const state = getPlatformState(platform);
  if (state.driverStarting) return false;
  state.driverStarting = true;
  updateViewStack();
  mainWindow?.webContents.send('state:update', stateSnapshot());
  try {
    await ensureDashboardRunning();
    const currentUrl = getWorkbenchView(platform)?.webContents.getURL() || '';
    if (!isWorkbenchRuntimeUrl(currentUrl, platform)) {
      throw new Error(`WORKBENCH_PAGE_NOT_FOUND: open ${config.label} workbench inside this client first. currentUrl=${currentUrl || '(empty)'}`);
    }
    logEvent('EMBEDDED_DRIVER_STARTING', {
      platform,
      accountKey: config.accountKey,
      cdpUrl: ELECTRON_CDP_URL,
      currentUrl,
    });
    const data = await postJson(`${LOCAL_DASHBOARD_URL}/api/drivers/start`, {
      platform,
      accountKey: config.accountKey,
      accountName: config.accountName,
      cdpUrl: ELECTRON_CDP_URL,
      controlledWorkbenchAttachOnly: true,
      forceRestart: true,
    });
    latestStatus = data.snapshot || latestStatus;
    logEvent('EMBEDDED_DRIVER_STARTED', {
      driverId: data.driverId,
      cdpUrl: ELECTRON_CDP_URL,
    });
    await checkDashboard();
    return true;
  } catch (err) {
    state.lastError = err instanceof Error ? err.message : String(err);
    lastError = state.lastError;
    logEvent('EMBEDDED_DRIVER_START_FAILED', { platform, error: state.lastError, cdpUrl: ELECTRON_CDP_URL });
    return false;
  } finally {
    state.driverStarting = false;
    updateViewStack();
    mainWindow?.webContents.send('state:update', stateSnapshot());
  }
}

function contentBounds() {
  if (!mainWindow) return { x: 0, y: 0, width: 0, height: 0 };
  const bounds = mainWindow.getContentBounds();
  const top = 112;
  const side = 360;
  const sideX = Math.max(0, bounds.width - side);
  const mainWidth = Math.max(0, sideX);
  return {
    main: {
      x: 0,
      y: top,
      width: mainWidth,
      height: Math.max(360, bounds.height - top),
    },
    side: {
      x: sideX,
      y: top,
      width: side,
      height: Math.max(360, bounds.height - top),
    },
  };
}

function ensureView(view) {
  if (!mainWindow || !view) return;
  if (!mainWindow.getBrowserViews().includes(view)) mainWindow.addBrowserView(view);
}

function removeView(view) {
  if (!mainWindow || !view) return;
  if (mainWindow.getBrowserViews().includes(view)) mainWindow.removeBrowserView(view);
}

function layoutViews() {
  if (!mainWindow) return;
  const { main } = contentBounds();
  for (const platform of PLATFORM_IDS) {
    const state = getPlatformState(platform);
    if (!state.detachedWindow || state.detachedWindow.isDestroyed()) {
      state.view?.setBounds(main);
      state.detachedControlView?.setBounds({ x: 0, y: 0, width: 0, height: 0 });
    } else {
      const detachedBounds = state.detachedWindow.getContentBounds();
      state.detachedControlView?.setBounds({ x: 0, y: 0, width: detachedBounds.width, height: 44 });
      state.view?.setBounds({ x: 0, y: 44, width: detachedBounds.width, height: Math.max(200, detachedBounds.height - 44) });
    }
  }
  overlayView?.setBounds(main);
  if (process.platform === 'win32') {
    logEvent('WORKBENCH_OVERLAY_BOUNDS', { overlay: overlayView?.getBounds?.(), main });
  }
  mainWindow.webContents.send('layout:update', contentBounds());
}

function updateOverlay() {
  if (!overlayView || !mainWindow) return;
  if (overlayView.webContents.isDestroyed()) return;
  const control = controlSnapshot(activeWorkbenchPlatform);
  const platformState = getPlatformState(activeWorkbenchPlatform);
  const locked = PLATFORM_IDS.includes(activeTab)
    && (control.mode === MODES.AI_AUTO || control.mode === MODES.AI_SENDING || platformState.driverStarting);
  if (!locked) {
    removeView(overlayView);
    overlayRenderKey = '';
    return;
  }
  const title = platformState.driverStarting
    ? '正在启动托管'
    : control.mode === MODES.AI_SENDING ? 'AI 正在发送' : 'AI 托管中';
  const renderKey = `${activeWorkbenchPlatform}:${control.mode}:${platformState.driverStarting ? 'starting' : 'ready'}`;
  if (overlayRenderKey !== renderKey) {
    overlayRenderKey = renderKey;
    overlayView.webContents.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(`
    <!doctype html>
    <html><head><meta charset="utf-8" />
    <style>
      html,body{margin:0;height:100%;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;background:rgba(15,23,42,.18);}
      .bar{position:absolute;left:24px;right:24px;top:24px;padding:16px 18px;border-radius:8px;background:rgba(255,255,255,.96);border:1px solid #cbd5e1;box-shadow:0 12px 32px rgba(15,23,42,.22);color:#0f172a;}
      h1{font-size:18px;margin:0 0 6px;}
      p{font-size:13px;margin:0;color:#475569;line-height:1.45;}
      .actions{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:14px}
      button{border:1px solid #cbd5e1;border-radius:7px;background:#fff;color:#0f172a;font-size:14px;font-weight:700;padding:10px 12px;cursor:pointer}
      button.pause{background:#eff6ff;border-color:#93c5fd;color:#1d4ed8}
      button.human{background:#fef2f2;border-color:#fca5a5;color:#991b1b}
    </style></head>
    <body><div class="bar">
      <h1>${title}</h1>
      <p>当前平台已由蜂序客服托管。需要手动处理时，可直接在这里暂停或人工接管。</p>
      <div class="actions">
        <button class="pause" id="pause">暂停托管</button>
        <button class="human" id="human">人工接管</button>
      </div>
    </div>
    <script>
      const { ipcRenderer } = require('electron');
      document.getElementById('pause').addEventListener('click', () => ipcRenderer.invoke('control:setModeForPlatform', '${activeWorkbenchPlatform}', 'AI_PAUSED', 'overlay_pause'));
      document.getElementById('human').addEventListener('click', () => ipcRenderer.invoke('control:setModeForPlatform', '${activeWorkbenchPlatform}', 'HUMAN_ACTIVE', 'overlay_human'));
    </script></body></html>
  `)}`);
  }
  ensureView(overlayView);
  mainWindow.setTopBrowserView(overlayView);
}

function detachedControlHtml(platform) {
  const label = getPlatformConfig(platform).label;
  return `<!doctype html><html><head><meta charset="utf-8" />
  <style>
    html,body{margin:0;height:100%;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;background:#0f172a;color:#e2e8f0}
    body{display:flex;align-items:center;gap:10px;padding:6px 10px;box-sizing:border-box}
    strong{font-size:13px;margin-right:auto}
    button{border:1px solid #475569;border-radius:7px;background:#fff;color:#0f172a;font-size:13px;font-weight:700;padding:7px 12px;cursor:pointer}
    button.human{background:#fef2f2;border-color:#fca5a5;color:#991b1b}
  </style></head><body>
    <strong>${label}托管控制</strong>
    <button id="auto">恢复 AI</button>
    <button id="pause">暂停托管</button>
    <button class="human" id="human">人工接管</button>
    <script>
      const { ipcRenderer } = require('electron');
      document.getElementById('auto').addEventListener('click', () => ipcRenderer.invoke('control:setModeForPlatform', '${platform}', 'AI_AUTO', 'detached_resume_ai'));
      document.getElementById('pause').addEventListener('click', () => ipcRenderer.invoke('control:setModeForPlatform', '${platform}', 'AI_PAUSED', 'detached_pause'));
      document.getElementById('human').addEventListener('click', () => ipcRenderer.invoke('control:setModeForPlatform', '${platform}', 'HUMAN_ACTIVE', 'detached_human'));
    </script>
  </body></html>`;
}

function ensureDetachedControlView(platform) {
  const state = getPlatformState(platform);
  if (state.detachedControlView && !state.detachedControlView.webContents.isDestroyed()) return state.detachedControlView;
  state.detachedControlView = new BrowserView({
    webPreferences: { nodeIntegration: true, contextIsolation: false, backgroundThrottling: false },
  });
  state.detachedControlView.webContents.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(detachedControlHtml(platform))}`);
  return state.detachedControlView;
}

function updateViewStack() {
  if (!mainWindow) return;
  if (shellMode !== 'ready') {
    for (const platform of PLATFORM_IDS) removeView(getPlatformState(platform).view);
    removeView(overlayView);
    mainWindow.webContents.send('state:update', stateSnapshot());
    return;
  }
  let activeView = null;
  for (const platform of PLATFORM_IDS) {
    const state = getPlatformState(platform);
    const view = state.view;
    if (!view || state.detachedWindow || !isPlatformAllowed(platform)) {
      removeView(view);
      continue;
    }
    if (activeTab === platform) {
      ensureView(view);
      activeView = view;
    } else {
      removeView(view);
    }
  }
  layoutViews();
  if (activeView && mainWindow.getBrowserViews().includes(activeView)) {
    mainWindow.setTopBrowserView(activeView);
  }
  updateOverlay();
  mainWindow.webContents.send('state:update', stateSnapshot());
}

function rendererHtml() {
  return `<!doctype html>
  <html lang="zh-CN"><head><meta charset="utf-8" />
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <title>蜂序客服工作台</title>
    <style>
      :root{--bg:#f8fafc;--border:#d9e2ec;--text:#0f172a;--muted:#64748b;--blue:#2563eb;--green:#15803d;--amber:#b45309;--red:#b91c1c;--nav:#0f172a}
      *{box-sizing:border-box} body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC",sans-serif;background:var(--bg);color:var(--text);font-size:13px}
      .login{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;background:#f8fafc;padding:24px}
      .login-card{width:min(420px,100%);background:#fff;border:1px solid var(--border);border-radius:8px;padding:28px;box-shadow:0 18px 45px rgba(15,23,42,.08)}
      .login h1{margin:0 0 8px;font-size:24px;color:#0f172a}.login p{margin:0 0 20px;color:#64748b;line-height:1.6}
      .login label{display:block;font-weight:700;margin-bottom:8px;color:#334155}.login input{width:100%;padding:13px 14px;border:1px solid #cbd5e1;border-radius:7px;font-size:17px;letter-spacing:2px;text-transform:uppercase}
      .login .error{min-height:20px;color:#b91c1c;font-size:12px;margin-top:10px;line-height:1.5}
      .app{display:none}
      body.ready .login{display:none} body.ready .app{display:block}
      header{height:64px;display:flex;align-items:center;justify-content:space-between;padding:0 18px;background:var(--nav);color:#fff;border-bottom:1px solid #1e293b}
      header strong{font-size:15px} header span{font-size:12px;color:#cbd5e1}
      .statusbar{display:flex;gap:8px;align-items:center;flex-wrap:wrap;justify-content:flex-end}
      .pill{display:inline-flex;align-items:center;gap:4px;padding:6px 10px;border-radius:999px;background:#e2e8f0;color:#334155;font-size:12px;font-weight:600}
      .pill.dark{background:#1e293b;color:#e2e8f0;border:1px solid #334155}.pill.ok{background:#dcfce7;color:#166534}.pill.warn{background:#fef3c7;color:#92400e}.pill.bad{background:#fee2e2;color:#991b1b}
      nav{position:fixed;left:0;right:360px;top:64px;height:48px;display:flex;align-items:end;padding:0 12px;background:#fff;border-bottom:1px solid var(--border)}
      nav button{width:auto;margin:0 4px 8px 0;padding:8px 12px;border-radius:7px;border:1px solid transparent;background:#fff;color:#334155;cursor:pointer;font-size:13px}
      nav button.active{background:#eff6ff;border-color:#93c5fd;color:#1d4ed8;font-weight:700}
      aside{position:fixed;right:0;top:64px;bottom:0;width:360px;padding:14px;border-left:1px solid var(--border);background:#fff;overflow:auto}
      main{position:fixed;left:0;right:360px;top:112px;bottom:0;overflow:auto;padding:16px;background:#f8fafc}
      body.knowledge nav{right:0} body.knowledge main{right:0} body.knowledge aside{display:none} body.knowledge .workbench-label{display:none}
      .tab-panel{display:none}.tab-panel.active{display:block}
      h2{font-size:13px;margin:16px 0 8px;color:#334155}
      button{width:100%;margin:5px 0;padding:10px 12px;border-radius:7px;border:1px solid var(--border);background:#fff;color:var(--text);font-size:13px;cursor:pointer}
      input,textarea{width:100%;border:1px solid var(--border);border-radius:7px;padding:9px 10px;font:inherit;color:var(--text);background:#fff} textarea{min-height:74px;resize:vertical}
      label.small{display:block;margin:8px 0 4px;font-size:11px;font-weight:700;color:#64748b}
      button.primary{background:var(--blue);border-color:var(--blue);color:#fff} button.warn{background:#fffbeb;border-color:#f59e0b;color:#92400e} button.danger{background:#fef2f2;border-color:#fca5a5;color:#991b1b}
      .hint,.card{font-size:12px;line-height:1.5;color:var(--muted);padding:10px;border:1px solid var(--border);border-radius:7px;background:#fff}
      pre{font-size:12px;line-height:1.45;background:#0f172a;color:#e2e8f0;padding:10px;border-radius:7px;min-height:180px;white-space:pre-wrap;overflow:auto}
      .workbench-label{position:fixed;left:12px;top:120px;z-index:2;padding:4px 8px;border-radius:999px;background:rgba(15,23,42,.72);color:#fff;font-size:11px;pointer-events:none}
      .mock{background:#fef2f2!important;color:#991b1b!important}
      .grid{display:grid;grid-template-columns:1fr 1fr;gap:10px}.grid button{margin:0}
      .keyval{display:grid;grid-template-columns:110px 1fr;gap:6px 10px;align-items:start}.keyval code{word-break:break-all;color:#0f172a}
      .msg-row{display:flex;flex-direction:column;gap:2px;margin-bottom:4px}
      .msg-bubble{max-width:90%;padding:6px 10px;border-radius:10px;font-size:12px;line-height:1.4;word-break:break-all;white-space:pre-wrap}
      .msg-in .msg-bubble{background:#f1f5f9;color:#0f172a;align-self:flex-start}
      .msg-out .msg-bubble{background:#2563eb;color:#fff;align-self:flex-end}
      .msg-stage .msg-bubble{background:#fffbeb;color:#92400e;border:1px solid #fde68a;align-self:center;max-width:96%;font-size:11px}
      .msg-error .msg-bubble{background:#fef2f2;color:#991b1b;border:1px solid #fecaca;align-self:center;max-width:96%;font-size:11px}
      .msg-meta{font-size:10px;color:#94a3b8;padding:0 4px}
      .msg-in .msg-meta{align-self:flex-start}
      .msg-out .msg-meta{align-self:flex-end}
      .msg-stage .msg-meta,.msg-error .msg-meta{align-self:center}
      .tag-chip{display:inline-flex;padding:2px 7px;border-radius:999px;background:#eff6ff;color:#1d4ed8;font-size:11px;font-weight:600}
      .customer-line{font-size:11px;color:#64748b;line-height:1.5;margin:2px 0}
      .risk{margin-top:8px;padding:7px;border-radius:7px;background:#fffbeb;border:1px solid #fde68a;color:#92400e;font-size:11px;line-height:1.45}
      .takeover-notice{display:none;margin:0 0 10px;padding:12px;border-radius:8px;border:1px solid #fecaca;background:#fef2f2;color:#7f1d1d}
      .takeover-notice.show{display:block}
      .takeover-notice strong{display:block;font-size:14px;margin-bottom:4px;color:#991b1b}
      .takeover-notice p{margin:0 0 10px;font-size:12px;line-height:1.5;color:#7f1d1d}
      .takeover-detail{display:grid;gap:6px;margin:0 0 10px;font-size:12px;line-height:1.45}
      .takeover-detail div{padding:7px 8px;border-radius:7px;background:#fff;border:1px solid #fecaca;color:#7f1d1d;word-break:break-word}
      .takeover-detail b{color:#991b1b}
      .takeover-notice button{margin:0;background:#2563eb;border-color:#2563eb;color:#fff}
      .detached-note{height:100%;min-height:360px;display:none;align-items:center;justify-content:center;text-align:center;color:#64748b;border:1px dashed #cbd5e1;border-radius:8px;background:#fff}
      .platform-actions{display:grid;grid-template-columns:1fr 1fr;gap:8px}.platform-actions button{margin:0}
      .knowledge-layout{max-width:1180px;margin:0 auto;display:grid;grid-template-columns:320px 1fr;gap:16px}
      .knowledge-list{max-height:640px;overflow:auto;display:flex;flex-direction:column;gap:8px}
      .knowledge-item{padding:10px;border:1px solid var(--border);border-radius:7px;background:#fff;cursor:pointer}
      .knowledge-item.active{border-color:#93c5fd;background:#eff6ff}.knowledge-item.disabled{opacity:.58}
      .knowledge-title{font-weight:700;color:#0f172a;margin-bottom:4px}.knowledge-meta{font-size:11px;color:#64748b;line-height:1.45}
      .knowledge-toolbar{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:10px}.knowledge-toolbar button{margin:0}
      .platform-checks{display:flex;gap:14px;margin:8px 0 10px;color:#475569;flex-wrap:wrap}
      .detail-actions{display:grid;grid-template-columns:repeat(3,1fr);gap:8px}.detail-actions button{margin:0}
      .search-results{max-height:220px;overflow:auto;margin-top:8px}
      .operator-alert{position:fixed;left:auto;right:0;top:64px;bottom:0;width:360px;z-index:30;display:none;align-items:flex-start;justify-content:center;background:rgba(15,23,42,.12);padding:88px 14px 14px}
      .operator-alert.show{display:flex}
      .operator-alert-card{width:min(460px,100%);background:#fff;border:1px solid #fecaca;border-radius:8px;box-shadow:0 24px 60px rgba(15,23,42,.28);padding:18px}
      .operator-alert-card h2{margin:0 0 8px;color:#991b1b;font-size:18px}
      .operator-alert-card p{margin:0 0 12px;color:#334155;line-height:1.55}
      .operator-alert-meta{font-size:12px;color:#64748b;line-height:1.5;margin-bottom:12px}
      .operator-alert-actions{display:grid;grid-template-columns:1fr 1fr;gap:8px}.operator-alert-actions button{margin:0}
      @media (max-width: 980px){.knowledge-layout{grid-template-columns:1fr}}
    </style></head>
    <body>
      <section class="login" id="loginView">
        <div class="login-card">
          <h1>蜂序客服</h1>
          <p>输入管理台生成的绑定码，连接后进入客服工作台。</p>
          <label for="bindingCodeInput">绑定码</label>
          <input id="bindingCodeInput" autocomplete="one-time-code" spellcheck="false" placeholder="10 位绑定码" maxlength="14" />
          <button class="primary" id="connectBtn" style="margin-top:14px">连接服务端</button>
          <div class="error" id="loginError"></div>
        </div>
      </section>
      <section class="app" id="appView">
        <header>
          <div><strong>蜂序客服工作台</strong><br/><span>多平台客服托管</span></div>
          <div class="statusbar">
            <span id="cloudBadge" class="pill dark">服务端 ?</span>
            <span id="driverBadge" class="pill dark">店铺 ?</span>
            <span id="sourceBadge" class="pill">拼多多</span>
            <span id="modeBadge" class="pill">AI 已暂停</span>
          </div>
        </header>
        <nav>
          <button data-tab="pdd" class="active">拼多多</button>
          <button data-tab="douyin">抖店飞鸽</button>
          <button data-tab="knowledge">知识库</button>
        </nav>
        <div class="workbench-label" id="workbenchLabel">拼多多客服页</div>
        <main>
          <section id="tab-pdd" class="tab-panel active">
            <div class="hint">拼多多客服页面在此区域显示。开始托管后页面会被锁定；人工接管后可手动操作。</div>
            <div class="detached-note" id="detached-pdd"><div><strong>拼多多已在独立窗口中运行</strong><br/>关闭独立窗口后会回到这里。</div></div>
          </section>
          <section id="tab-douyin" class="tab-panel">
            <div class="hint">抖店飞鸽客服页面在此区域显示。开始托管后页面会被锁定；人工接管后可手动操作。</div>
            <div class="detached-note" id="detached-douyin"><div><strong>抖店飞鸽已在独立窗口中运行</strong><br/>关闭独立窗口后会回到这里。</div></div>
          </section>
          <section id="tab-knowledge" class="tab-panel">
            <div class="knowledge-layout">
              <div class="card">
                <div style="display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px">
                  <h2 style="margin:0">云端知识库</h2>
                  <button id="refreshKnowledge" style="width:auto;margin:0">刷新</button>
                </div>
                <div class="knowledge-toolbar">
                  <button id="uploadKnowledge" class="primary">上传文件</button>
                  <button id="newTextKnowledge">新增文本</button>
                  <button id="newQaKnowledge">新增 QA</button>
                  <button id="showDisabledKnowledge">显示停用</button>
                </div>
                <div id="knowledgeStatus" class="hint" style="margin-bottom:10px"></div>
                <div id="knowledgeList" class="knowledge-list"></div>
              </div>
              <div class="card">
                <h2 style="margin-top:0" id="knowledgeDetailTitle">知识详情</h2>
                <input type="hidden" id="knowledgeKind" />
                <input type="hidden" id="knowledgeId" />
                <label class="small" for="knowledgeTitleInput">标题 / 问题</label>
                <input id="knowledgeTitleInput" placeholder="例如：小五金产品问答库" />
                <label class="small" for="knowledgeContentInput">内容 / 回答</label>
                <textarea id="knowledgeContentInput" style="min-height:190px" placeholder="输入知识内容或 QA 答案"></textarea>
                <div class="platform-checks">
                  <label><input id="knowledgePdd" type="checkbox" style="width:auto" /> 仅拼多多</label>
                  <label><input id="knowledgeDouyin" type="checkbox" style="width:auto" /> 仅抖店飞鸽</label>
                  <label><input id="knowledgeEnabled" type="checkbox" style="width:auto" checked /> 启用</label>
                </div>
                <div class="detail-actions">
                  <button id="saveKnowledge" class="primary">保存</button>
                  <button id="disableKnowledge" class="warn">停用</button>
                  <button id="clearKnowledge">清空</button>
                </div>
                <h2>搜索测试</h2>
                <div style="display:grid;grid-template-columns:1fr 110px;gap:8px">
                  <input id="knowledgeSearchInput" placeholder="输入客户可能会问的问题" />
                  <button id="searchKnowledge" class="primary" style="margin:0">测试</button>
                </div>
                <div id="knowledgeSearchResults" class="search-results"></div>
              </div>
            </div>
          </section>
        </main>
        <aside id="panel">
          <h2 id="controlTitle">拼多多托管控制</h2>
          <div class="takeover-notice" id="takeoverNotice">
            <strong>当前已转人工接管</strong>
            <p>蜂序客服已停止自动回复当前平台。需要恢复自动托管时，点击下方按钮切回 AI 接管。</p>
            <div class="takeover-detail">
              <div id="takeoverRule"><b>触发规则：</b>—</div>
              <div id="takeoverQuote"><b>触发内容：</b>—</div>
            </div>
            <button id="resumeAiControl">恢复 AI 托管</button>
          </div>
          <button class="primary" data-mode="AI_AUTO">开始托管</button>
          <button data-mode="AI_PAUSED">暂停托管</button>
          <button class="warn" data-mode="AI_SUGGEST_ONLY" style="display:none">只给建议</button>
          <button class="danger" data-mode="HUMAN_ACTIVE">人工接管</button>
          <h2>平台窗口</h2>
          <div class="platform-actions">
            <button id="detachPlatform">独立窗口</button>
            <button id="reloadPlatform">刷新页面</button>
            <button id="openLoginPlatform">登录页</button>
            <button id="openWorkbenchPlatform">客服页</button>
            <button id="clearPlatformSession">清除登录态</button>
          </div>
          <div id="platformHint" class="hint" style="margin-top:10px">当前平台：拼多多</div>
          <h2>会话动态</h2>
          <div id="msgFeed" style="height:220px;overflow-y:auto;display:flex;flex-direction:column;border:1px solid var(--border);border-radius:7px;padding:6px;background:#fff"></div>
          <h2>当前客户</h2>
          <div id="customerPanel" class="card">
            <div id="customerName" style="font-weight:600;margin-bottom:6px;color:#0f172a">—</div>
            <div id="customerTags" style="display:flex;flex-wrap:wrap;gap:4px;margin-bottom:6px"></div>
            <div id="customerProfile" style="font-size:11px;color:var(--muted);line-height:1.5"></div>
            <div class="risk">风险提示：库存、价格、发货时间必须来自可信数据源；不确定时转人工或只给建议。</div>
          </div>
        </aside>
      </section>
      <section class="operator-alert" id="operatorAlert">
        <div class="operator-alert-card">
          <h2 id="operatorAlertTitle">需要人工接管</h2>
          <p id="operatorAlertMessage">客户问题需要人工处理。</p>
          <div class="operator-alert-meta" id="operatorAlertMeta"></div>
          <div class="operator-alert-actions">
            <button class="danger" id="operatorAlertTakeover">人工接管</button>
            <button id="operatorAlertClose">稍后处理</button>
          </div>
        </div>
      </section>
      <script>
        const { ipcRenderer } = require('electron');
        const loginError = document.getElementById('loginError');
        const bindingCodeInput = document.getElementById('bindingCodeInput');
        const connectBtn = document.getElementById('connectBtn');
        const modeBadge = document.getElementById('modeBadge');
        const cloudBadge = document.getElementById('cloudBadge');
        const driverBadge = document.getElementById('driverBadge');
        const sourceBadge = document.getElementById('sourceBadge');
        const workbenchLabel = document.getElementById('workbenchLabel');
        const msgFeed = document.getElementById('msgFeed');
        const customerNameEl = document.getElementById('customerName');
        const customerTagsEl = document.getElementById('customerTags');
        const customerProfileEl = document.getElementById('customerProfile');
        const controlTitle = document.getElementById('controlTitle');
        const platformHint = document.getElementById('platformHint');
        const takeoverNotice = document.getElementById('takeoverNotice');
        const resumeAiControl = document.getElementById('resumeAiControl');
        const takeoverRule = document.getElementById('takeoverRule');
        const takeoverQuote = document.getElementById('takeoverQuote');
        const detachPlatform = document.getElementById('detachPlatform');
        const reloadPlatform = document.getElementById('reloadPlatform');
        const openLoginPlatform = document.getElementById('openLoginPlatform');
        const openWorkbenchPlatform = document.getElementById('openWorkbenchPlatform');
        const clearPlatformSession = document.getElementById('clearPlatformSession');
        const refreshKnowledge = document.getElementById('refreshKnowledge');
        const uploadKnowledge = document.getElementById('uploadKnowledge');
        const newTextKnowledge = document.getElementById('newTextKnowledge');
        const newQaKnowledge = document.getElementById('newQaKnowledge');
        const showDisabledKnowledge = document.getElementById('showDisabledKnowledge');
        const knowledgeStatus = document.getElementById('knowledgeStatus');
        const knowledgeList = document.getElementById('knowledgeList');
        const knowledgeDetailTitle = document.getElementById('knowledgeDetailTitle');
        const knowledgeKind = document.getElementById('knowledgeKind');
        const knowledgeId = document.getElementById('knowledgeId');
        const knowledgeTitleInput = document.getElementById('knowledgeTitleInput');
        const knowledgeContentInput = document.getElementById('knowledgeContentInput');
        const knowledgePdd = document.getElementById('knowledgePdd');
        const knowledgeDouyin = document.getElementById('knowledgeDouyin');
        const knowledgeEnabled = document.getElementById('knowledgeEnabled');
        const saveKnowledge = document.getElementById('saveKnowledge');
        const disableKnowledge = document.getElementById('disableKnowledge');
        const clearKnowledge = document.getElementById('clearKnowledge');
        const knowledgeSearchInput = document.getElementById('knowledgeSearchInput');
        const searchKnowledge = document.getElementById('searchKnowledge');
        const knowledgeSearchResults = document.getElementById('knowledgeSearchResults');
        const operatorAlert = document.getElementById('operatorAlert');
        const operatorAlertTitle = document.getElementById('operatorAlertTitle');
        const operatorAlertMessage = document.getElementById('operatorAlertMessage');
        const operatorAlertMeta = document.getElementById('operatorAlertMeta');
        const operatorAlertTakeover = document.getElementById('operatorAlertTakeover');
        const operatorAlertClose = document.getElementById('operatorAlertClose');
        let currentTab = 'pdd';
        let currentAlertPlatform = 'pdd';
        let currentAlertDetail = null;
        const recentOperatorAlerts = new Map();
        const shownOperatorAlerts = new Set();
        let knowledgeItems = [];
        let selectedKnowledge = null;
        let includeDisabledKnowledge = false;
        const modeLabels = {
          AI_AUTO: '托管中',
          AI_PAUSED: '已暂停',
          AI_SUGGEST_ONLY: '只给建议',
          HUMAN_ACTIVE: '已转人工接管',
          AI_SENDING: '发送中',
        };
        const alertReasonLabels = {
          image_message: '客户发送图片',
          keyword: '命中人工关键词',
          order_action: '订单操作请求',
          low_confidence: '问题需要确认',
          manual: '客户要求人工',
        };

        function playOperatorAlertSound(){
          try {
            const AudioCtx = window.AudioContext || window.webkitAudioContext;
            if (!AudioCtx) return;
            const ctx = new AudioCtx();
            const now = ctx.currentTime;
            [0, 0.18].forEach((offset) => {
              const osc = ctx.createOscillator();
              const gain = ctx.createGain();
              osc.type = 'sine';
              osc.frequency.setValueAtTime(880, now + offset);
              gain.gain.setValueAtTime(0.0001, now + offset);
              gain.gain.exponentialRampToValueAtTime(0.18, now + offset + 0.02);
              gain.gain.exponentialRampToValueAtTime(0.0001, now + offset + 0.14);
              osc.connect(gain);
              gain.connect(ctx.destination);
              osc.start(now + offset);
              osc.stop(now + offset + 0.16);
            });
            setTimeout(() => ctx.close().catch(() => undefined), 700);
          } catch {}
        }

        function showOperatorAlert(p){
          const platform = p.platform || currentTab || 'pdd';
          const alertKey = p.cmdId || [platform, p.externalChatId || '', p.reason || '', p.textPreview || p.messagePreview || '', p.at || p.timestamp || p.receivedAt || ''].join('|');
          const nowMs = Date.now();
          for (const [key, at] of recentOperatorAlerts.entries()) {
            if (nowMs - at > 20000) recentOperatorAlerts.delete(key);
          }
          if (shownOperatorAlerts.has(alertKey) || recentOperatorAlerts.has(alertKey)) return;
          shownOperatorAlerts.add(alertKey);
          if (shownOperatorAlerts.size > 80) {
            const oldest = shownOperatorAlerts.values().next().value;
            if (oldest) shownOperatorAlerts.delete(oldest);
          }
          recentOperatorAlerts.set(alertKey, nowMs);
          currentAlertPlatform = platform;
          const reason = alertReasonLabels[p.reason] || '需要人工接管';
          currentAlertDetail = {
            reason: p.reason || 'operator_alert',
            ruleLabel: reason,
            messagePreview: p.textPreview || p.messagePreview || '',
            customerName: p.customerName || '',
            externalChatId: p.externalChatId || '',
            source: 'operator_alert',
          };
          operatorAlertTitle.textContent = p.title || '需要人工接管';
          operatorAlertMessage.textContent = (p.textPreview || p.messagePreview)
            ? reason + '：' + (p.textPreview || p.messagePreview)
            : reason + '，请尽快查看当前会话。';
          operatorAlertMeta.textContent = [
            platform === 'pdd' ? '拼多多' : platform === 'douyin' ? '抖店飞鸽' : platform,
            p.customerName ? '客户：' + p.customerName : '',
            p.externalChatId ? '会话：' + shortChat(p.externalChatId) : '',
          ].filter(Boolean).join(' · ');
          operatorAlert.classList.add('show');
          playOperatorAlertSound();
          try { window.focus(); } catch {}
        }

        function hideOperatorAlert(){
          operatorAlert.classList.remove('show');
          currentAlertDetail = null;
        }

        function appendEvent(e, options = {}){
          appendMsgFeed(e, options);
        }
        function render(s){
          if (!s) return;
          document.body.classList.toggle('ready', s.shellMode === 'ready');
          document.body.classList.toggle('knowledge', currentTab === 'knowledge');
          if (s.shellMode !== 'ready') {
            if (s.lastError) loginError.textContent = s.lastError;
            return;
          }
          if (s.operatorAlert) showOperatorAlert(s.operatorAlert);
          currentTab = s.activeTab || s.activePlatform || currentTab;
          document.body.classList.toggle('knowledge', currentTab === 'knowledge');
          const controlPlatform = s.platforms[currentTab] ? currentTab : (s.activePlatform || 'pdd');
          const p = s.platforms[controlPlatform] || s.platforms.pdd;
          const drivers = s.dashboard.status ? (s.dashboard.status.drivers || []) : [];
          const platformDrivers = drivers.filter((d) => d.platform === p.id && d.accountKey === p.accountKey);
          const activePlatformDriver = platformDrivers.find((d) => d.status === 'online') || platformDrivers[0];
          const driverReady = Boolean(activePlatformDriver && activePlatformDriver.status === 'online');
          const aiBlocked = p.control.mode === 'AI_AUTO' && !driverReady;
          const humanActive = p.control.mode === 'HUMAN_ACTIVE';
          const handoffDetail = p.control.handoffDetail || {};
          const handoffRule = handoffDetail.ruleLabel || alertReasonLabels[handoffDetail.reason] || (handoffDetail.reason === 'operator_button' ? '操作员手动切换' : handoffDetail.reason || '操作员手动切换');
          const handoffQuote = handoffDetail.messagePreview
            ? '“' + handoffDetail.messagePreview + '”'
            : '无客户原话，可能由操作员手动切换或旧版控制状态触发。';
          modeBadge.textContent = aiBlocked ? '店铺未就绪' : (modeLabels[p.control.mode] || p.control.mode);
          modeBadge.className = 'pill ' + (aiBlocked ? 'bad' : p.control.overlayLocked ? 'ok' : p.control.mode === 'HUMAN_ACTIVE' ? 'warn' : '');
          sourceBadge.textContent = p.label;
          sourceBadge.className = 'pill ' + (p.isMock ? 'mock' : 'ok');
          workbenchLabel.textContent = p.isMock ? '测试页面' : p.workbenchLabel;
          workbenchLabel.style.background = p.isMock ? 'rgba(185,28,28,.82)' : 'rgba(15,23,42,.72)';
          controlTitle.textContent = p.label + '托管控制';
          takeoverNotice.classList.toggle('show', humanActive);
          resumeAiControl.disabled = !p.allowed || p.control.busy;
          takeoverRule.textContent = '触发规则：' + handoffRule;
          takeoverQuote.textContent = '触发内容：' + handoffQuote;
          platformHint.textContent = p.lastError || (humanActive
            ? '当前平台已转人工接管，AI 不会自动回复；可点击“恢复 AI 托管”重新接管。'
            : '当前平台：' + p.label + (p.detached ? ' · 已在独立窗口中运行' : ''));
          platformHint.style.color = p.lastError || humanActive ? '#991b1b' : '#64748b';
          detachPlatform.textContent = p.detached ? '回到主窗口' : '独立窗口';
          detachPlatform.disabled = !p.allowed;
          reloadPlatform.disabled = !p.allowed;
          openLoginPlatform.disabled = !p.allowed;
          openWorkbenchPlatform.disabled = !p.allowed;
          clearPlatformSession.disabled = !p.allowed;
          for (const platform of ['pdd', 'douyin']) {
            const tabButton = document.querySelector('nav button[data-tab="' + platform + '"]');
            if (tabButton) {
              tabButton.disabled = !s.platforms[platform]?.allowed;
              tabButton.style.opacity = s.platforms[platform]?.allowed ? '1' : '.42';
              tabButton.title = s.platforms[platform]?.allowed ? '' : '当前租户未开通该平台';
            }
            document.getElementById('detached-' + platform).style.display = s.platforms[platform]?.detached ? 'flex' : 'none';
          }
          const knowledgeTabButton = document.querySelector('nav button[data-tab="knowledge"]');
          if (knowledgeTabButton) {
            knowledgeTabButton.disabled = false;
            knowledgeTabButton.style.opacity = '1';
            knowledgeTabButton.title = '';
          }
          document.querySelectorAll('[data-mode]').forEach((btn) => {
            btn.disabled = !p.allowed;
            btn.title = p.allowed ? '' : '当前租户未开通该平台';
            if (p.control.busy && btn.dataset.mode === p.control.pendingMode) {
              btn.textContent = '处理中…';
            } else if (btn.dataset.mode === 'AI_AUTO') {
              btn.textContent = p.control.mode === 'AI_AUTO' || p.control.mode === 'AI_SENDING'
                ? '托管中'
                : humanActive ? '恢复 AI 托管' : '开始托管';
            } else if (btn.dataset.mode === 'AI_PAUSED') {
              btn.textContent = '暂停托管';
            } else if (btn.dataset.mode === 'HUMAN_ACTIVE') {
              btn.textContent = humanActive ? '当前人工接管中' : '人工接管';
            }
            if (btn.dataset.mode === 'AI_AUTO' && (p.control.mode === 'AI_AUTO' || p.control.mode === 'AI_SENDING')) btn.disabled = true;
            if (btn.dataset.mode === 'HUMAN_ACTIVE' && humanActive) btn.disabled = true;
          });
          if (!p.allowed) {
            platformHint.textContent = p.label + '尚未开通，请联系蜂序运营后台配置平台席位。';
            platformHint.style.color = '#991b1b';
          }
          if (s.dashboard.status) {
            cloudBadge.textContent = s.dashboard.status.connected ? '服务端已连接' : '服务端未连接';
            cloudBadge.className = 'pill ' + (s.dashboard.status.connected ? 'ok' : 'warn');
            const priority = { error: 5, logging_in: 4, initializing: 3, online: 2, idle: 1, offline: 0 };
            const driver = platformDrivers.slice().sort((a, b) => (priority[b.status] || 0) - (priority[a.status] || 0))[0];
            if (!driver) {
              driverBadge.textContent = '店铺未启动';
              driverBadge.title = '';
              driverBadge.className = 'pill dark';
            } else {
              const countSuffix = platformDrivers.length > 1 ? ' +' + (platformDrivers.length - 1) : '';
              driverBadge.textContent = '店铺连接 ' + driver.status + countSuffix;
              driverBadge.title = [driver.id, driver.accountName, driver.statusInfo].filter(Boolean).join(' · ');
              const klass = driver.status === 'online'
                ? 'ok'
                : driver.status === 'error'
                  ? 'bad'
                  : (driver.status === 'logging_in' || driver.status === 'initializing')
                    ? 'warn'
                    : 'dark';
              driverBadge.className = 'pill ' + klass;
            }
          }
        }
        async function setTab(tab){
          currentTab = tab;
          document.querySelectorAll('nav button').forEach((b) => b.classList.toggle('active', b.dataset.tab === tab));
          document.querySelectorAll('.tab-panel').forEach((p) => p.classList.toggle('active', p.id === 'tab-' + tab));
          document.body.classList.toggle('knowledge', tab === 'knowledge');
          workbenchLabel.style.display = tab === 'knowledge' ? 'none' : 'block';
          const s = await ipcRenderer.invoke('tab:set', tab);
          render(s);
          if (tab === 'knowledge') loadKnowledge();
        }
        function shortChat(chatId) {
          const value = String(chatId || '');
          return value.length > 16 ? value.slice(0, 8) + '…' + value.slice(-6) : value;
        }
        function buildMsgRow(dir, text, time, metaText) {
          const row = document.createElement('div');
          row.className = 'msg-row msg-' + dir;
          const bubble = document.createElement('div');
          bubble.className = 'msg-bubble';
          bubble.textContent = text;
          const meta = document.createElement('div');
          meta.className = 'msg-meta';
          meta.textContent = (metaText ? metaText + ' · ' : '') + new Date(time || Date.now()).toLocaleTimeString();
          row.appendChild(bubble);
          row.appendChild(meta);
          return row;
        }
        function appendPanelLine(dir, text, time, metaText) {
          if (!text) return;
          msgFeed.appendChild(buildMsgRow(dir, text, time, metaText));
          while (msgFeed.children.length > 80) msgFeed.removeChild(msgFeed.firstChild);
          msgFeed.scrollTop = msgFeed.scrollHeight;
        }
        function appendMsgFeed(e, options = {}) {
          const k = e.kind || '';
          const raw = e.payload || {};
          const envelope = raw.type && raw.payload ? raw.payload : raw;
          const p = envelope.payload || envelope;
          let dir = null, text = '', meta = '';
          if (k === 'DASHBOARD_CUSTOMER_INBOUND_DETECTED') {
            dir = 'in';
            text = p.contentType === 'image' ? '[图片]' : (p.textPreview || '');
            meta = (p.customerName || '客户') + (p.externalChatId ? ' #' + shortChat(p.externalChatId) : '');
            updateCustomerInfo(p);
          } else if (k === 'DASHBOARD_INBOUND_OBSERVED') {
            dir = 'stage';
            text = '收到客户消息，准备上传云端：' + (p.textPreview || p.messageId || '');
            meta = (p.customerName || p.senderName || '客户') + (p.externalChatId ? ' #' + shortChat(p.externalChatId) : '');
            updateCustomerInfo(p);
          } else if (k === 'DASHBOARD_INBOUND_EMITTED') {
            dir = 'stage';
            text = '已上传云端，等待 ACK / batch：' + (p.textPreview || p.messageId || '');
            meta = p.externalChatId ? '#' + shortChat(p.externalChatId) : '';
            updateCustomerInfo(p);
          } else if (k === 'DASHBOARD_INBOUND_ACK') {
            const ack = envelope.ack || p.ack || p;
            dir = ack.received ? 'stage' : 'error';
            text = ack.received ? '服务端已确认收到消息' : '服务端拒绝消息：' + (ack.error || 'unknown');
            meta = ack.evtId ? 'evt ' + shortChat(ack.evtId) : '';
          } else if (k === 'DASHBOARD_OUTBOUND_RECEIVED') {
            dir = 'stage';
            text = '收到 AI 发送指令：' + (p.textPreview || p.cmdId || '');
            meta = p.externalChatId ? '#' + shortChat(p.externalChatId) : '';
          } else if (k === 'DASHBOARD_OUTBOUND_COMPLETED') {
            if (p.ok === false) {
              dir = 'error';
              text = '发送失败：' + (p.error || 'unknown');
            } else {
              dir = 'out';
              text = p.textPreview || '[已发送]';
            }
            meta = p.externalChatId ? '#' + shortChat(p.externalChatId) : '';
          } else if (k === 'DASHBOARD_HANDOFF_ALERT') {
            const alert = p.payload || p;
            dir = 'error';
            text = alert.title || '需要人工接管';
            if (alert.textPreview) text += '：' + alert.textPreview;
            meta = [
              alert.platform === 'pdd' ? '拼多多' : alert.platform === 'douyin' ? '抖店飞鸽' : alert.platform,
              alert.externalChatId ? '#' + shortChat(alert.externalChatId) : '',
            ].filter(Boolean).join(' ');
            if (!options.replay) showOperatorAlert(alert);
          } else if (k === 'DASHBOARD_DRIVER_DEBUG') {
            const dbgEvent = p.kind === 'driver_debug' ? (p.payload || {}) : p;
            const dbg = dbgEvent.payload || {};
            if (dbgEvent.kind === 'message_flow') {
              dir = 'stage';
              text = (dbg.stage === 'observed' ? 'DOM 已观察到消息：' : '消息流：' + (dbg.stage || '?') + ' ') + (dbg.textPreview || dbg.messageId || '');
              meta = dbg.chatId ? '#' + shortChat(dbg.chatId) : '';
            } else if (dbgEvent.kind === 'dom_empty_read_retry') {
              dir = 'stage';
              text = '会话空读，' + (dbg.retryMs || '?') + 'ms 后重试：' + (dbg.preview || '');
              meta = dbg.chatId ? '#' + shortChat(dbg.chatId) : '';
            } else if (dbgEvent.kind === 'auto_session_switch') {
              dir = 'stage';
              text = '切换到待处理会话：' + (dbg.label || dbg.id || '');
              meta = dbg.unreadCount ? '未读 ' + dbg.unreadCount : '';
            }
          } else if (k === 'EMBEDDED_DRIVER_START_FAILED' || k === 'AI_AUTO_BLOCKED') {
            dir = 'error';
            text = p.error || 'AI 托管被阻断';
          } else if (k === 'EMBEDDED_DRIVER_STARTED') {
            dir = 'stage';
            text = '店铺连接已就绪';
            meta = p.driverId || '';
          } else if (k === 'DASHBOARD_CONTROL_MODE_CHANGED') {
            dir = 'stage';
            text = '托管状态：' + (p.mode || '?') + ' · ' + (p.reason || '');
          }
          if (!dir || !text) return;
          appendPanelLine(dir, text, e.at, meta);
        }
        function updateCustomerInfo(p) {
          const prof = p.customerProfile || {};
          const name = p.customerName || p.senderName || prof.nickname || prof.name || prof.userName || p.externalChatId || p.chatId;
          if (name) customerNameEl.textContent = name;
          const tags = []
            .concat(Array.isArray(p.customerTags) ? p.customerTags : [])
            .concat(Array.isArray(prof.tags) ? prof.tags : []);
          customerTagsEl.innerHTML = '';
          if (tags.length > 0) {
            tags.slice(0, 8).forEach((t) => {
              const tag = typeof t === 'string' ? t : (t && (t.tag || t.name || t.label) ? (t.tag || t.name || t.label) : '');
              if (!tag) return;
              const chip = document.createElement('span');
              chip.className = 'tag-chip';
              chip.textContent = tag;
              customerTagsEl.appendChild(chip);
            });
          } else {
            const chip = document.createElement('span');
            chip.className = 'tag-chip';
            chip.style.background = '#f1f5f9';
            chip.style.color = '#64748b';
            chip.textContent = '暂无标签';
            customerTagsEl.appendChild(chip);
          }
          const lines = [];
          if (p.externalChatId || p.chatId) lines.push('会话: ' + shortChat(p.externalChatId || p.chatId));
          if (p.textPreview) lines.push('最近消息: ' + p.textPreview);
          if (prof.level) lines.push('等级: ' + prof.level);
          if (prof.userId) lines.push('用户ID: ' + String(prof.userId).slice(0, 18));
          if (prof.rawText) lines.push('资料: ' + String(prof.rawText).slice(0, 120));
          if (prof.updatedAt) lines.push('资料更新时间: ' + new Date(prof.updatedAt).toLocaleTimeString());
          if (lines.length === 0) lines.push('已识别当前会话，但暂未读取到客户资料。');
          customerProfileEl.innerHTML = '';
          lines.slice(0, 6).forEach((line) => {
            const div = document.createElement('div');
            div.className = 'customer-line';
            div.textContent = line;
            customerProfileEl.appendChild(div);
          });
        }
        function platformLabel(platforms){
          if (!platforms || platforms.length === 0) return '全平台';
          return platforms.map((p) => p === 'pdd' ? '拼多多' : p === 'douyin' ? '抖店飞鸽' : p).join(' / ');
        }
        function setKnowledgePlatforms(platforms){
          knowledgePdd.checked = Array.isArray(platforms) && platforms.includes('pdd');
          knowledgeDouyin.checked = Array.isArray(platforms) && platforms.includes('douyin');
        }
        function getKnowledgePlatforms(){
          const platforms = [];
          if (knowledgePdd.checked) platforms.push('pdd');
          if (knowledgeDouyin.checked) platforms.push('douyin');
          return platforms;
        }
        function selectKnowledge(item){
          selectedKnowledge = item || null;
          knowledgeId.value = item ? item.id : '';
          knowledgeKind.value = item ? item.kind : 'document';
          knowledgeDetailTitle.textContent = item ? (item.kind === 'qa' ? '编辑 QA' : '编辑文档') : '新增文本';
          knowledgeTitleInput.value = item ? (item.kind === 'qa' ? item.question : item.title) : '';
          knowledgeContentInput.value = item ? (item.kind === 'qa' ? item.answer : (item.content || '')) : '';
          knowledgeEnabled.checked = item ? item.enabled !== false : true;
          setKnowledgePlatforms(item ? item.platforms : []);
          disableKnowledge.textContent = item && item.enabled === false ? '恢复启用' : '停用';
          for (const el of knowledgeList.querySelectorAll('.knowledge-item')) {
            el.classList.toggle('active', item && el.dataset.key === item.kind + ':' + item.id);
          }
        }
        function renderKnowledgeList(){
          knowledgeList.innerHTML = '';
          knowledgeItems.forEach((item) => {
            const row = document.createElement('div');
            row.className = 'knowledge-item' + (item.enabled === false ? ' disabled' : '');
            row.dataset.key = item.kind + ':' + item.id;
            const title = document.createElement('div');
            title.className = 'knowledge-title';
            title.textContent = item.kind === 'qa' ? item.question : item.title;
            const meta = document.createElement('div');
            meta.className = 'knowledge-meta';
            const type = item.kind === 'qa' ? 'QA' : '文档';
            const status = item.enabled === false ? '已停用' : '启用中';
            const updated = item.updatedAt ? new Date(item.updatedAt).toLocaleString() : '';
            meta.textContent = type + ' · ' + status + ' · ' + platformLabel(item.platforms || []) + (updated ? ' · ' + updated : '');
            row.appendChild(title);
            row.appendChild(meta);
            row.addEventListener('click', () => selectKnowledge(item));
            knowledgeList.appendChild(row);
          });
        }
        async function loadKnowledge(){
          try {
            knowledgeStatus.textContent = '正在同步云端知识库…';
            const data = await ipcRenderer.invoke('knowledge:list', { includeDisabled: includeDisabledKnowledge });
            const documents = (data.documents || []).map((item) => Object.assign({ kind: 'document' }, item));
            const qa = (data.qa || []).map((item) => Object.assign({ kind: 'qa' }, item));
            knowledgeItems = documents.concat(qa);
            renderKnowledgeList();
            const total = (data.documents || []).length + (data.qa || []).length;
            knowledgeStatus.textContent = total ? ('已同步云端知识库：文档 ' + (data.documents || []).length + '，QA ' + (data.qa || []).length) : '暂无知识库内容';
            if (selectedKnowledge) {
              const next = knowledgeItems.find((item) => item.kind === selectedKnowledge.kind && item.id === selectedKnowledge.id);
              selectKnowledge(next || null);
            }
          } catch (err) {
            knowledgeStatus.textContent = err && err.message ? err.message : String(err);
          }
        }
        async function saveKnowledgeItem(){
          try {
            const kind = knowledgeKind.value || 'document';
            const payload = {
              id: knowledgeId.value,
              kind,
              title: knowledgeTitleInput.value.trim(),
              content: knowledgeContentInput.value,
              platforms: getKnowledgePlatforms(),
              enabled: knowledgeEnabled.checked,
            };
            if (kind === 'qa') {
              await ipcRenderer.invoke(payload.id ? 'knowledge:updateQa' : 'knowledge:createQa', {
                id: payload.id,
                question: payload.title,
                answer: payload.content,
                platforms: payload.platforms,
                enabled: payload.enabled,
              });
            } else {
              await ipcRenderer.invoke(payload.id ? 'knowledge:updateDocument' : 'knowledge:createText', payload);
            }
            knowledgeStatus.textContent = '已保存到云端';
            await loadKnowledge();
          } catch (err) {
            knowledgeStatus.textContent = err && err.message ? err.message : String(err);
          }
        }
        refreshKnowledge.addEventListener('click', loadKnowledge);
        showDisabledKnowledge.addEventListener('click', async () => {
          includeDisabledKnowledge = !includeDisabledKnowledge;
          showDisabledKnowledge.textContent = includeDisabledKnowledge ? '隐藏停用' : '显示停用';
          await loadKnowledge();
        });
        uploadKnowledge.addEventListener('click', async () => {
          try {
            const result = await ipcRenderer.invoke('knowledge:uploadFile', { platforms: getKnowledgePlatforms() });
            if (result && result.cancelled) return;
            knowledgeStatus.textContent = '文件已上传并同步到云端';
            await loadKnowledge();
          } catch (err) {
            knowledgeStatus.textContent = err && err.message ? err.message : String(err);
          }
        });
        newTextKnowledge.addEventListener('click', () => selectKnowledge({ kind: 'document', id: '', title: '', content: '', platforms: [], enabled: true }));
        newQaKnowledge.addEventListener('click', () => selectKnowledge({ kind: 'qa', id: '', question: '', answer: '', platforms: [], enabled: true }));
        saveKnowledge.addEventListener('click', saveKnowledgeItem);
        operatorAlertClose.addEventListener('click', async () => {
          const platform = currentAlertPlatform;
          hideOperatorAlert();
          await setTab(platform);
          render(await ipcRenderer.invoke('operator-alert:dismiss', platform, 'operator_alert_defer'));
        });
        operatorAlertTakeover.addEventListener('click', async () => {
          operatorAlert.classList.remove('show');
          await setTab(currentAlertPlatform);
          render(await ipcRenderer.invoke('control:setModeForPlatform', currentAlertPlatform, 'HUMAN_ACTIVE', 'operator_alert_takeover', currentAlertDetail));
        });
        disableKnowledge.addEventListener('click', async () => {
          if (!selectedKnowledge || !selectedKnowledge.id) return;
          try {
            const enabled = selectedKnowledge.enabled === false;
            await ipcRenderer.invoke(selectedKnowledge.kind === 'qa' ? 'knowledge:updateQa' : 'knowledge:updateDocument', {
              id: selectedKnowledge.id,
              enabled,
            });
            await loadKnowledge();
          } catch (err) {
            knowledgeStatus.textContent = err && err.message ? err.message : String(err);
          }
        });
        clearKnowledge.addEventListener('click', () => selectKnowledge(null));
        searchKnowledge.addEventListener('click', async () => {
          try {
            const data = await ipcRenderer.invoke('knowledge:search', { query: knowledgeSearchInput.value.trim(), platform: getKnowledgePlatforms()[0] || undefined });
            knowledgeSearchResults.innerHTML = '';
            (data.results || []).forEach((item) => {
              const row = document.createElement('div');
              row.className = 'hint';
              row.style.marginBottom = '6px';
              row.textContent = (item.metadata && item.metadata.title ? item.metadata.title + '：' : '') + String(item.content || '').slice(0, 180);
              knowledgeSearchResults.appendChild(row);
            });
            if (!data.results || data.results.length === 0) knowledgeSearchResults.textContent = '未命中知识';
          } catch (err) {
            knowledgeSearchResults.textContent = err && err.message ? err.message : String(err);
          }
        });
        for (const btn of document.querySelectorAll('nav button')) btn.addEventListener('click', () => setTab(btn.dataset.tab));
        for (const btn of document.querySelectorAll('[data-mode]')) {
          btn.addEventListener('click', async () => {
            const platform = ['pdd', 'douyin'].includes(currentTab)
              ? currentTab
              : ((window.__state && window.__state.activePlatform) || 'pdd');
            if (['AI_AUTO', 'AI_PAUSED', 'HUMAN_ACTIVE'].includes(btn.dataset.mode)) hideOperatorAlert();
            render(await ipcRenderer.invoke('control:setModeForPlatform', platform, btn.dataset.mode, 'operator_button'));
          });
        }
        resumeAiControl.addEventListener('click', async () => {
          hideOperatorAlert();
          render(await ipcRenderer.invoke('control:setModeForPlatform', currentTab, 'AI_AUTO', 'operator_resume_ai'));
        });
        detachPlatform.addEventListener('click', async () => render(await ipcRenderer.invoke('platform:detach', currentTab)));
        reloadPlatform.addEventListener('click', async () => render(await ipcRenderer.invoke('platform:reload', currentTab)));
        openLoginPlatform.addEventListener('click', async () => render(await ipcRenderer.invoke('platform:openUrl', currentTab, 'login')));
        openWorkbenchPlatform.addEventListener('click', async () => render(await ipcRenderer.invoke('platform:openUrl', currentTab, 'workbench')));
        clearPlatformSession.addEventListener('click', async () => {
          if (!confirm('清除当前平台登录态并回到登录页？')) return;
          render(await ipcRenderer.invoke('platform:clearSession', currentTab));
        });
        connectBtn.addEventListener('click', async () => {
          loginError.textContent = '';
          connectBtn.disabled = true;
          connectBtn.textContent = '连接中…';
          try {
            render(await ipcRenderer.invoke('auth:connect', bindingCodeInput.value.trim()));
          } catch (err) {
            loginError.textContent = err && err.message ? err.message : String(err);
          } finally {
            connectBtn.disabled = false;
            connectBtn.textContent = '连接服务端';
          }
        });
        bindingCodeInput.addEventListener('keydown', (event) => {
          if (event.key === 'Enter') connectBtn.click();
        });
        ipcRenderer.on('state:update', (_e, s) => render(s));
        ipcRenderer.on('control:update', () => {
          ipcRenderer.invoke('state:get').then((s) => { window.__state = s; render(s); });
        });
        ipcRenderer.on('timeline:update', (_e, event) => appendEvent(event));
        ipcRenderer.on('operator-alert:show', (_e, alert) => showOperatorAlert(alert || {}));
        ipcRenderer.on('dashboard:status', (_e, status) => {
          if (window.__state) window.__state.dashboard.status = status;
          ipcRenderer.invoke('state:get').then((s) => { window.__state = s; render(s); });
        });
        ipcRenderer.invoke('state:get').then((s) => {
          window.__state = s;
          render(s);
          (s.timeline || []).forEach((e) => appendEvent(e, { replay: true }));
          loadKnowledge();
        });
      </script>
    </body></html>`;
}

function createViews() {
  // backgroundThrottling: false 是关键 — Chromium 默认对后台 BrowserView 节流
  // setTimeout/RAF（30s 后 → 1/分钟，5min 后挂起渲染进程），导致 PDD 客服页的
  // DOM 更新被推迟，driver 扫不到新消息。关掉它让消息真正实时。
  for (const platform of PLATFORM_IDS) {
    const config = getPlatformConfig(platform);
    const state = getPlatformState(platform);
    state.view = new BrowserView({
      webPreferences: {
        nodeIntegration: false,
        contextIsolation: true,
        backgroundThrottling: false,
      },
    });
    attachWorkbenchNavigationGuards(platform, state.view.webContents, platform);
    state.view.webContents.loadURL(config.configuredUrl);
    state.view.webContents.on('did-finish-load', () => {
      savePlatformRuntimeUrl(platform, state.view.webContents.getURL());
      logEvent('WORKBENCH_LOADED', { url: state.view.webContents.getURL(), source: config.source, platform });
      mainWindow?.webContents.send('state:update', stateSnapshot());
    });
    state.view.webContents.on('did-fail-load', (_event, errorCode, errorDescription, validatedURL) => {
      state.lastError = `页面加载失败 ${errorCode}: ${errorDescription}`;
      lastError = state.lastError;
      logEvent('WORKBENCH_LOAD_FAILED', { platform, errorCode, errorDescription, url: validatedURL });
    });
  }
  overlayView = new BrowserView({ webPreferences: { nodeIntegration: true, contextIsolation: false, backgroundThrottling: false } });
  overlayView.webContents.loadURL('data:text/html;charset=utf-8,<html><body></body></html>');
}

async function createWindow() {
  await ensureDashboardRunning();
  await stopDashboardDrivers('app_start_cleanup');
  await tryAutoConnect();

  mainWindow = new BrowserWindow({
    width: 1800,
    height: 940,
    minWidth: 1600,
    minHeight: 820,
    title: '蜂序客服工作台',
    webPreferences: { nodeIntegration: true, contextIsolation: false, backgroundThrottling: false },
  });

  createViews();
  mainWindow.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(rendererHtml())}`);
  mainWindow.on('resize', layoutViews);
  mainWindow.on('closed', () => { mainWindow = null; });
  mainWindow.webContents.on('did-finish-load', () => {
    updateViewStack();
    logEvent('SHELL_READY', { platform: getWorkbenchConfig().platform, dashboardUrl: LOCAL_DASHBOARD_URL, shellMode });
  });
}

async function tryAutoConnect() {
  await checkDashboard();
  if (hasUsableAuthorization(latestStatus)) {
    shellMode = 'ready';
    return true;
  }
  if (latestStatus?.connected) {
    shellMode = 'login';
    lastError = platformAuthorizationMessage(latestStatus);
    return false;
  }
  try {
    const data = await postJson(`${LOCAL_DASHBOARD_URL}/api/connect`, {});
    latestStatus = data.snapshot || latestStatus;
    shellMode = hasUsableAuthorization(latestStatus) ? 'ready' : 'login';
    if (!hasUsableAuthorization(latestStatus)) lastError = platformAuthorizationMessage(latestStatus);
    return shellMode === 'ready';
  } catch (err) {
    shellMode = 'login';
    lastError = '';
    logEvent('AUTO_CONNECT_SKIPPED', { error: err instanceof Error ? err.message : String(err) });
    return false;
  }
}

async function ensureDashboardConnectedForAiAuto(platform) {
  await checkDashboard().catch((err) => {
    logEvent('AI_AUTO_DASHBOARD_STATUS_CHECK_FAILED', {
      platform,
      error: err instanceof Error ? err.message : String(err),
    });
  });
  if (latestStatus?.connected) return true;
  try {
    const data = await postJson(`${LOCAL_DASHBOARD_URL}/api/connect`, {});
    latestStatus = data.snapshot || latestStatus;
    await checkDashboard().catch(() => undefined);
    return Boolean(latestStatus?.connected);
  } catch (err) {
    logEvent('AI_AUTO_SILENT_RECONNECT_FAILED', {
      platform,
      error: err instanceof Error ? err.message : String(err),
    });
    return false;
  }
}

ipcMain.handle('state:get', async () => {
  await checkDashboard();
  return stateSnapshot();
});
ipcMain.handle('knowledge:list', async (_event, options = {}) => {
  const includeDisabled = options?.includeDisabled === true;
  const [qaData, docData] = await Promise.all([
    cloudKnowledgeRequest(`/api/v1/knowledge/qa?limit=100${includeDisabled ? '&includeDisabled=1' : ''}`),
    cloudKnowledgeRequest(`/api/v1/knowledge?limit=100${includeDisabled ? '&includeDisabled=1' : ''}`),
  ]);
  logEvent('KNOWLEDGE_CLOUD_LISTED', {
    documents: Array.isArray(docData.documents) ? docData.documents.length : 0,
    qa: Array.isArray(qaData.qa) ? qaData.qa.length : 0,
  });
  return {
    success: true,
    documents: docData.documents || [],
    qa: qaData.qa || [],
  };
});
ipcMain.handle('knowledge:createText', async (_event, input) => {
  const title = String(input?.title || '').trim();
  const content = String(input?.content || '').trim();
  const platforms = Array.isArray(input?.platforms) ? input.platforms : [];
  if (!title || !content) throw new Error('请填写标题和内容');
  const data = await cloudKnowledgeRequest('/api/v1/knowledge/text', {
    method: 'POST',
    body: { title, content, platforms, replaceExisting: true },
  });
  logEvent('KNOWLEDGE_CLOUD_TEXT_CREATED', { title, platforms });
  return data;
});
ipcMain.handle('knowledge:updateDocument', async (_event, input) => {
  const id = String(input?.id || '').trim();
  if (!id) throw new Error('请选择要编辑的文档');
  const body = {};
  if (input?.title !== undefined) body.title = String(input.title || '').trim();
  if (input?.content !== undefined) body.content = String(input.content || '');
  if (input?.platforms !== undefined) body.platforms = Array.isArray(input.platforms) ? input.platforms : [];
  if (input?.enabled !== undefined) body.enabled = input.enabled === true;
  const data = await cloudKnowledgeRequest(`/api/v1/knowledge/documents/${encodeURIComponent(id)}`, {
    method: 'PATCH',
    body,
  });
  logEvent('KNOWLEDGE_CLOUD_DOCUMENT_UPDATED', { id });
  return data;
});
ipcMain.handle('knowledge:uploadFile', async (_event, input) => {
  const result = await dialog.showOpenDialog(mainWindow, {
    title: '上传知识文件',
    properties: ['openFile'],
    filters: [{ name: '知识文件', extensions: ['txt', 'md', 'markdown', 'csv'] }],
  });
  if (result.canceled || !result.filePaths[0]) return { cancelled: true };
  const filePath = result.filePaths[0];
  const ext = path.extname(filePath).slice(1).toLowerCase();
  if (!['txt', 'md', 'markdown', 'csv'].includes(ext)) throw new Error('仅支持 txt、md、csv 文件');
  const platforms = Array.isArray(input?.platforms) ? input.platforms : [];
  const form = new FormData();
  const buffer = fs.readFileSync(filePath);
  const mimeType = ext === 'csv' ? 'text/csv' : ext === 'md' || ext === 'markdown' ? 'text/markdown' : 'text/plain';
  form.append('file', new Blob([buffer], { type: mimeType }), path.basename(filePath));
  form.append('platforms', platforms.join(','));
  form.append('replaceExisting', 'true');
  const data = await cloudKnowledgeRequest('/api/v1/knowledge/upload', { method: 'POST', formData: form });
  logEvent('KNOWLEDGE_CLOUD_FILE_UPLOADED', { name: path.basename(filePath), platforms });
  return data;
});
ipcMain.handle('knowledge:createQa', async (_event, input) => {
  const question = String(input?.question || '').trim();
  const answer = String(input?.answer || '').trim();
  const platforms = Array.isArray(input?.platforms) ? input.platforms : [];
  if (!question || !answer) throw new Error('请填写问题和答案');
  const data = await cloudKnowledgeRequest('/api/v1/knowledge/qa', {
    method: 'POST',
    body: { question, answer, platforms },
  });
  logEvent('KNOWLEDGE_CLOUD_QA_CREATED', { platforms });
  return data;
});
ipcMain.handle('knowledge:updateQa', async (_event, input) => {
  const id = String(input?.id || '').trim();
  if (!id) throw new Error('请选择要编辑的 QA');
  const body = {};
  if (input?.question !== undefined) body.question = String(input.question || '').trim();
  if (input?.answer !== undefined) body.answer = String(input.answer || '');
  if (input?.platforms !== undefined) body.platforms = Array.isArray(input.platforms) ? input.platforms : [];
  if (input?.enabled !== undefined) body.enabled = input.enabled === true;
  const data = await cloudKnowledgeRequest(`/api/v1/knowledge/qa/${encodeURIComponent(id)}`, {
    method: 'PATCH',
    body,
  });
  logEvent('KNOWLEDGE_CLOUD_QA_UPDATED', { id });
  return data;
});
ipcMain.handle('knowledge:search', async (_event, input) => {
  const query = String(input?.query || '').trim();
  if (!query) throw new Error('请输入搜索内容');
  return cloudKnowledgeRequest('/api/v1/knowledge/search', {
    method: 'POST',
    body: { query, platform: input?.platform, limit: 5, minScore: 0.1 },
  });
});
ipcMain.handle('tab:set', async (_event, tab) => {
  if (Object.values(TABS).includes(tab)) {
    if (tab === TABS.KNOWLEDGE) {
      activeTab = tab;
      updateViewStack();
      return stateSnapshot();
    }
    if (!isPlatformAllowed(tab)) {
      lastError = `${getPlatformConfig(tab).label}尚未开通。`;
      return stateSnapshot();
    }
    activeTab = tab;
    activeWorkbenchPlatform = normalizeWorkbenchPlatform(tab);
    saveWorkbenchPlatform(activeWorkbenchPlatform);
  }
  updateViewStack();
  return stateSnapshot();
});
ipcMain.handle('auth:connect', async (_event, bindingCode) => {
  if (loginBusy) return stateSnapshot();
  loginBusy = true;
  try {
    await ensureDashboardRunning();
    const code = String(bindingCode || '').trim();
    if (!code) throw new Error('请输入绑定码。');
    const data = await postJson(`${LOCAL_DASHBOARD_URL}/api/connect`, { bindingCode: code });
    latestStatus = data.snapshot || latestStatus;
    if (!latestStatus?.connected) throw new Error('连接未完成，请检查绑定码。');
    if (!hasUsableAuthorization(latestStatus)) {
      shellMode = 'login';
      throw new Error(platformAuthorizationMessage(latestStatus));
    }
    shellMode = 'ready';
    lastError = '';
    updateViewStack();
    return stateSnapshot();
  } catch (err) {
    shellMode = 'login';
    lastError = err instanceof Error ? err.message : String(err);
    throw err;
  } finally {
    loginBusy = false;
  }
});
ipcMain.handle('platform:detach', async (_event, platform) => {
  const id = normalizeWorkbenchPlatform(platform);
  if (!isPlatformAllowed(id)) {
    lastError = `${getPlatformConfig(id).label}尚未开通。`;
    return stateSnapshot();
  }
  const state = getPlatformState(id);
  const config = getPlatformConfig(id);
  if (!state.view) return stateSnapshot();
  if (state.detachedWindow && !state.detachedWindow.isDestroyed()) {
    state.detachedWindow.close();
    return stateSnapshot();
  }
  removeView(state.view);
  const win = new BrowserWindow({
    width: 1180,
    height: 820,
    title: `蜂序客服工作台 · ${config.label}`,
    webPreferences: { nodeIntegration: false, contextIsolation: true, backgroundThrottling: false },
  });
  const controlView = ensureDetachedControlView(id);
  state.detachedWindow = win;
  win.addBrowserView(controlView);
  win.addBrowserView(state.view);
  const layoutDetached = () => {
    const bounds = win.getContentBounds();
    controlView.setBounds({ x: 0, y: 0, width: bounds.width, height: 44 });
    state.view.setBounds({ x: 0, y: 44, width: bounds.width, height: Math.max(200, bounds.height - 44) });
  };
  win.on('resize', layoutDetached);
  win.on('closed', () => {
    if (state.detachedControlView && !state.detachedControlView.webContents.isDestroyed()) {
      state.detachedControlView.webContents.destroy();
    }
    state.detachedControlView = null;
    state.detachedWindow = null;
    if (mainWindow && !mainWindow.isDestroyed()) {
      updateViewStack();
    }
  });
  layoutDetached();
  logEvent('WORKBENCH_DETACHED', { platform: id });
  updateViewStack();
  return stateSnapshot();
});
ipcMain.handle('platform:reload', async (_event, platform) => {
  if (!isPlatformAllowed(platform)) return stateSnapshot();
  const view = getWorkbenchView(platform);
  view?.webContents.reload();
  return stateSnapshot();
});
ipcMain.handle('platform:openUrl', async (_event, platform, target) => {
  const id = normalizeWorkbenchPlatform(platform);
  if (!isPlatformAllowed(id)) return stateSnapshot();
  const config = getPlatformConfig(id);
  const view = getWorkbenchView(id);
  if (target === 'login') {
    view?.webContents.loadURL(config.loginUrl);
  } else if (target === 'workbench') {
    view?.webContents.loadURL(config.workbenchUrl);
  }
  activeTab = id;
  activeWorkbenchPlatform = id;
  updateViewStack();
  return stateSnapshot();
});
ipcMain.handle('platform:clearSession', async (_event, platform) => {
  const id = normalizeWorkbenchPlatform(platform);
  if (!isPlatformAllowed(id)) return stateSnapshot();
  const config = getPlatformConfig(id);
  const view = getWorkbenchView(id);
  try {
    clearSavedPlatformRuntimeUrl(id);
    const targetSession = view?.webContents.session;
    if (targetSession) {
      await targetSession.clearStorageData({
        storages: ['cookies', 'localstorage', 'indexdb', 'serviceworkers', 'cachestorage', 'websql'],
      });
      await targetSession.clearCache();
    }
    view?.webContents.loadURL(config.loginUrl);
    logEvent('PLATFORM_SESSION_CLEARED', { platform: id });
  } catch (err) {
    const state = getPlatformState(id);
    state.lastError = `清除登录态失败：${err instanceof Error ? err.message : String(err)}`;
    lastError = state.lastError;
    logEvent('PLATFORM_SESSION_CLEAR_FAILED', { platform: id, error: state.lastError });
  }
  activeTab = id;
  activeWorkbenchPlatform = id;
  updateViewStack();
  return stateSnapshot();
});
async function applyControlModeRequest(mode, reason, requestedPlatform = activeWorkbenchPlatform, handoffDetail = null) {
  const platform = normalizeWorkbenchPlatform(requestedPlatform);
  const state = getPlatformState(platform);
  if (state.controlBusy && !isEmergencyControlMode(mode)) {
    logEvent('CONTROL_MODE_BUSY', { platform, mode, reason });
    return stateSnapshot();
  }
  state.controlBusy = true;
  state.controlPendingMode = mode;
  mainWindow?.webContents.send('state:update', stateSnapshot());
  try {
    if (!isPlatformAllowed(platform)) {
      state.lastError = `${getPlatformConfig(platform).label}尚未开通，请联系蜂序运营后台配置平台席位。`;
      lastError = state.lastError;
      logEvent('AI_AUTO_BLOCKED', { platform, error: state.lastError, reason: 'platform_not_allowed' });
      return stateSnapshot();
    }
    if (isEmergencyControlMode(mode)) {
      state.controlAbortVersion += 1;
      state.driverStarting = false;
      if (platform === activeWorkbenchPlatform) {
        removeView(overlayView);
        overlayRenderKey = '';
      }
      state.lastError = '';
      setControlMode(mode, reason, platform, handoffDetail);
      try {
        await syncControlModeToDashboard(mode, reason || 'controlled_workbench_emergency', platform);
      } catch (err) {
        state.lastError = `已在本机暂停；同步服务端失败：${err instanceof Error ? err.message : String(err)}`;
        lastError = state.lastError;
        logEvent('CONTROL_SYNC_FAILED', { platform, mode, error: state.lastError });
      }
      await checkDashboard().catch((err) => logEvent('DASHBOARD_CHECK_AFTER_EMERGENCY_FAILED', {
        platform,
        error: err instanceof Error ? err.message : String(err),
      }));
      return stateSnapshot();
    }
    if (mode === MODES.AI_AUTO) {
      const config = getPlatformConfig(platform);
      const currentUrl = getWorkbenchView(platform)?.webContents.getURL() || '';
      if (!isWorkbenchRuntimeUrl(currentUrl, platform)) {
        state.lastError = `请先打开 ${config.label} 客服页面。当前页面=${currentUrl || '(empty)'}`;
        lastError = state.lastError;
        logEvent('AI_AUTO_BLOCKED', { platform, error: state.lastError, currentUrl });
        return stateSnapshot();
      }
      if (!await ensureDashboardConnectedForAiAuto(platform)) {
        state.lastError = '客户端暂未连接服务端，已尝试自动重连但未成功。请检查网络或重新输入绑定码。';
        lastError = state.lastError;
        logEvent('AI_AUTO_BLOCKED', { platform, error: state.lastError, reason: 'cloud_not_connected' });
        return stateSnapshot();
      }
    }
    try {
      if (mode === MODES.AI_AUTO) {
        const config = getPlatformConfig(platform);
        const startAbortVersion = state.controlAbortVersion;
        state.lastError = '';
        setControlMode(MODES.AI_AUTO, reason || 'controlled_workbench_starting', platform);
        const started = await startEmbeddedDriver(platform, config);
        if (state.controlAbortVersion !== startAbortVersion || isEmergencyControlMode(state.mode)) {
          logEvent('AI_AUTO_CANCELLED_BY_EMERGENCY_CONTROL', { platform, mode: state.mode });
          return stateSnapshot();
        }
        if (!started) {
          setControlMode(MODES.AI_PAUSED, 'driver_start_failed', platform);
          await syncControlModeToDashboard(MODES.AI_PAUSED, 'driver_start_failed', platform).catch(() => undefined);
          await checkDashboard();
          return stateSnapshot();
        }
      }
      state.lastError = '';
      setControlMode(mode, reason, platform, handoffDetail);
      await syncControlModeToDashboard(mode, reason || 'controlled_workbench', platform);
      await checkDashboard();
    } catch (err) {
      state.lastError = err instanceof Error ? err.message : String(err);
      lastError = state.lastError;
      logEvent('CONTROL_SYNC_FAILED', { platform, mode, error: state.lastError });
      if (mode === MODES.AI_AUTO) {
        const syncError = state.lastError;
        setControlMode(MODES.AI_PAUSED, 'cloud_control_sync_failed', platform);
        state.lastError = `服务端未确认托管，已暂停：${syncError}`;
        lastError = state.lastError;
        await syncControlModeToDashboard(MODES.AI_PAUSED, 'cloud_control_sync_failed', platform).catch((pauseErr) => {
          logEvent('CONTROL_PAUSE_AFTER_SYNC_FAILED', {
            platform,
            error: pauseErr instanceof Error ? pauseErr.message : String(pauseErr),
          });
        });
        await checkDashboard().catch(() => undefined);
      }
    }
    return stateSnapshot();
  } finally {
    state.controlBusy = false;
    state.controlPendingMode = '';
    mainWindow?.webContents.send('state:update', stateSnapshot());
  }
}

ipcMain.handle('control:setModeForPlatform', async (_event, platform, mode, reason, handoffDetail) => {
  return applyControlModeRequest(mode, reason, platform, handoffDetail);
});

ipcMain.handle('control:setMode', async (_event, mode, reason, handoffDetail) => {
  return applyControlModeRequest(mode, reason, activeWorkbenchPlatform, handoffDetail);
});
ipcMain.handle('operator-alert:dismiss', async (_event, platform, reason) => {
  return dismissOperatorAlert(platform, reason);
});
ipcMain.handle('dashboard:open', () => shell.openExternal(LOCAL_DASHBOARD_URL));
ipcMain.handle('diagnostics:open', () => shell.openExternal(`${LOCAL_DASHBOARD_URL}/api/diagnostics`));
ipcMain.handle('logs:open', () => shell.openExternal(`${LOCAL_DASHBOARD_URL}/api/logs`));
ipcMain.handle('dashboard:reload', async () => {
  await checkDashboard();
  logEvent('DASHBOARD_IFRAME_RELOAD_REQUESTED', { url: LOCAL_DASHBOARD_URL });
  return stateSnapshot();
});
ipcMain.handle('workbench:reload', () => getWorkbenchView()?.webContents.reload());
ipcMain.handle('workbench:openUrl', (_event, target) => {
  const config = getWorkbenchConfig();
  const view = getWorkbenchView();
  if (target === 'login') view?.webContents.loadURL(config.loginUrl);
  if (target === 'workbench') view?.webContents.loadURL(config.workbenchUrl);
});

function registerEmergencyShortcuts() {
  const pause = globalShortcut.register('CommandOrControl+Shift+P', () => {
    void applyControlModeRequest(MODES.AI_PAUSED, 'keyboard_emergency_pause', activeWorkbenchPlatform);
  });
  const human = globalShortcut.register('CommandOrControl+Shift+H', () => {
    void applyControlModeRequest(MODES.HUMAN_ACTIVE, 'keyboard_emergency_human', activeWorkbenchPlatform);
  });
  logEvent('EMERGENCY_SHORTCUTS_REGISTERED', { pause, human });
}

app.whenReady().then(async () => {
  await createWindow();
  registerEmergencyShortcuts();
});
app.on('will-quit', () => {
  globalShortcut.unregisterAll();
});
app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') app.quit();
});
app.on('before-quit', (event) => {
  if (!appQuitCleanupDone) {
    event.preventDefault();
    void (async () => {
      appQuitCleanupDone = true;
      await stopDashboardDrivers('app_quit_cleanup').catch((err) => {
        logEvent('APP_QUIT_CLEANUP_FAILED', { error: err instanceof Error ? err.message : String(err) });
      });
      if (dashboardEventsAbort) dashboardEventsAbort.abort();
      if (dashboardProcess) dashboardProcess.kill();
      app.quit();
    })();
    return;
  }
  if (dashboardEventsAbort) dashboardEventsAbort.abort();
  if (dashboardProcess) dashboardProcess.kill();
});
app.on('activate', () => {
  if (!mainWindow) void createWindow();
});
