import express from 'express';
import crypto from 'node:crypto';
import fs from 'node:fs';
import { createProxyMiddleware } from 'http-proxy-middleware';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import {
  AUTH_COOKIE,
  clearSessionCookie,
  createAuthManager,
  parseCookies,
  sessionCookie,
} from './auth.mjs';
import { createDbPool, initSchema, isDatabaseConfigured } from './db.mjs';
import { createTkmindProxy } from './tkmind-proxy.mjs';
import {
  clearUserSessionCookie,
  createUserAuth,
  resolveCookieDomainForRequest,
  USER_COOKIE,
  userLoginCookies,
  userSessionCookie,
} from './user-auth.mjs';
import { createWikiAuth } from './wiki-auth.mjs';
import { isLocalDevHostname } from './scripts/local-test-config.mjs';
import { PUBLISH_ROOT_DIR, PUBLISH_KEY_UUID, PUBLIC_ZONE_DIR, resolvePublishDir } from './user-publish.mjs';
import { ensureWorkspaceHtmlThumbnail, startWorkspaceThumbnailWatcher, workspaceThumbnailRelativePath } from './mindspace-workspace-thumbnails.mjs';
import { startWorkspaceAssetSyncWatcher } from './mindspace-workspace-sync.mjs';
import { attachRequestId, sendData, sendError } from './api-response.mjs';
import { createMindSpaceAuditWriter } from './mindspace-audit.mjs';
import { assertMindSpaceRoute, mindspaceFlags } from './mindspace-flags.mjs';
import { createMindSpaceService, DEFAULT_MAX_FILE_BYTES } from './mindspace.mjs';
import { ensureMindSpaceConfig } from './mindspace-config.mjs';
import { createAssetService } from './mindspace-assets.mjs';
import { createPageService, pageInternals } from './mindspace-pages.mjs';
import { createPageLiveEditService } from './mindspace-page-live-edit.mjs';
import { createPageEditSessionService } from './mindspace-page-edit-session.mjs';
import { suggestCoverMetaWithAi } from './mindspace-cover-ai.mjs';
import { createPublicationService } from './mindspace-publications.mjs';
import { createPlazaPostService, formatPostRow, mapPlazaError } from './plaza-posts.mjs';
import { createPlazaEventService } from './plaza-events.mjs';
import { createPlazaRecommendService } from './plaza-recommend.mjs';
import { createPlazaInteractionService } from './plaza-interactions.mjs';
import {
  ensureAlgorithmConfig,
  loadAlgorithmConfig,
  recalculateHotScores,
} from './plaza-algorithm.mjs';
import { createPlazaRedis, createNoopPlazaRedis } from './plaza-redis.mjs';
import { startPlazaTasks, writebackPublications } from './plaza-tasks.mjs';
import { createPlazaSeoService } from './plaza-seo.mjs';
import { createPlazaOpsService } from './plaza-ops.mjs';
import { createWordFilterService } from './word-filter.mjs';
import {
  allowPlazaEmbedFrame,
  preparePublicationHtmlForEmbed,
  isPlazaEmbedRequest,
  publishedPageCspForEmbed,
  stripPublicationHtmlCspMeta,
} from './plaza-embed.mjs';
import { createCleanupService } from './mindspace-cleanup.mjs';
import { createAgentJobService } from './mindspace-agent-jobs.mjs';
import { createMindSpaceAgentRunner } from './mindspace-agent-runner.mjs';
import {
  analyzeChatMessageForSave,
  buildChatSavePreviewFrameUrl,
  buildChatSaveThumbnailUrl,
  buildWorkspaceAssetUrl,
  buildWorkspaceBaseHref,
  buildWorkspaceThumbnailUrl,
  injectHtmlBaseHref,
  resolveClosestHtmlRelativePath,
  resolveChatSaveAnalysis,
  resolveStaticHtmlContent,
} from './mindspace-chat-save.mjs';
import { generateHtmlThumbnail } from './mindspace-thumbnails.mjs';
import { injectOgTags } from './mindspace-og-tags.mjs';
import {
  ensureThumbnailPng,
  rasterizeThumbnailSvgToPng,
  thumbnailPngPathForSvg,
} from './mindspace-thumbnail-png.mjs';
import { scanContent } from './mindspace-content-scan.mjs';
import { renderImageAssetViewerHtml, wantsInlineImageViewer } from './mindspace-asset-preview.mjs';
import { createRechargeService } from './billing-recharge.mjs';
import { createSubscriptionService, createPlanCatalogService, ensurePlanCatalogSchema, PLAN_CATALOG } from './billing-subscription.mjs';
import {
  createWechatPayClient,
  loadWechatPayConfig,
  WECHAT_NOTIFY_SUCCESS_V2,
} from './wechat-pay.mjs';
import { createWechatOAuthService, isWechatUserAgent, loadWechatOAuthConfig } from './wechat-oauth.mjs';
import { createWechatMpService, loadWechatMpConfig } from './wechat-mp.mjs';
import { createScheduleService } from './schedule-service.mjs';
import { startScheduleReminderWorker } from './schedule-reminder-worker.mjs';
import { createLlmProviderService, RELAY_BOOTSTRAP } from './llm-providers.mjs';
import { createSessionSnapshotService } from './session-snapshot.mjs';
import { attachAsrRoutes } from './asr-proxy.mjs';
import { isNativeH5ApiPath } from './policies.mjs';

const __dirname = path.dirname(fileURLToPath(import.meta.url));

function loadEnvFile(filePath) {
  if (!fs.existsSync(filePath)) return;
  for (const line of fs.readFileSync(filePath, 'utf8').split('\n')) {
    const trimmed = line.trim();
    if (!trimmed || trimmed.startsWith('#')) continue;
    const eq = trimmed.indexOf('=');
    if (eq < 0) continue;
    const key = trimmed.slice(0, eq).trim();
    const value = trimmed.slice(eq + 1).trim();
    if (!process.env[key]) process.env[key] = value;
  }
}

loadEnvFile(path.join(__dirname, '../../.env.local'));
loadEnvFile(path.join(__dirname, '.env'));

const PORT = Number(process.env.H5_PORT ?? 8081);
const API_TARGET = process.env.TKMIND_API_TARGET ?? 'https://127.0.0.1:18006';
const API_TARGETS = [
  API_TARGET,
  ...(process.env.TKMIND_API_TARGET_1 ? [process.env.TKMIND_API_TARGET_1] : []),
];
const API_SECRET = process.env.TKMIND_SERVER__SECRET_KEY ?? 'local-dev-secret';
const INTERNAL_AGENT_SECRET = process.env.MINDSPACE_INTERNAL_AGENT_SECRET ?? API_SECRET;
const ACCESS_PASSWORD = process.env.H5_ACCESS_PASSWORD;
const WECHAT_MP_CONFIG = loadWechatMpConfig();
// 无状态前端节点（如 105，MindSpace 经 rclone 挂载）需设 MEMIND_WORKSPACE_MAINTENANCE=0，
// 否则启动时对挂载树做 readdir/递归 fs.watch 会占满 libuv 线程池导致 boot 卡死。
const WORKSPACE_MAINTENANCE_ENABLED = process.env.MEMIND_WORKSPACE_MAINTENANCE !== '0';
const USERS_ROOT = process.env.H5_USERS_ROOT ?? path.join(__dirname, 'users');

const app = express();
app.set('trust proxy', 1);
const isSecureRequest = (req) =>
  req.secure || req.get('x-forwarded-proto')?.split(',')[0]?.trim() === 'https';
const msFlags = mindspaceFlags();

app.use(attachRequestId);
app.use((req, res, next) => {
  res.setHeader('X-Content-Type-Options', 'nosniff');
  res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
  res.setHeader('Permissions-Policy', 'camera=(), microphone=(self), geolocation=()');
  res.setHeader('X-Frame-Options', 'SAMEORIGIN');
  if (isSecureRequest(req)) {
    res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
  }
  next();
});

function csrfOriginCheck(req, res, next) {
  if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) return next();
  const host = req.get('host');
  const origin = req.get('origin');
  const referer = req.get('referer');
  if (!origin && !referer) return next();
  const requestHostname = (host ?? '').split(':')[0];
  const allowed = [origin, referer].some((value) => {
    if (!value) return false;
    try {
      const source = new URL(value);
      if (source.host === host) return true;
      return (
        isLocalDevHostname(requestHostname) && isLocalDevHostname(source.hostname)
      );
    } catch {
      return false;
    }
  });
  if (!allowed) {
    return sendError(res, req, 403, 'csrf_failed', '来源校验失败');
  }
  return next();
}

app.use('/api', csrfOriginCheck);
app.use('/auth', csrfOriginCheck);

const jsonBody = express.json({ limit: '1mb' });
const jsonUnlessMultipart = (req, res, next) => {
  if ((req.headers['content-type'] ?? '').includes('multipart/form-data')) return next();
  return jsonBody(req, res, next);
};
const rawUploadBody = express.raw({
  type: 'application/octet-stream',
  limit: Number(process.env.MINDSPACE_MAX_FILE_BYTES ?? DEFAULT_MAX_FILE_BYTES),
});

const wikiAuth = createWikiAuth(path.join(__dirname, PUBLISH_ROOT_DIR, 'wiki-db'));

let legacyAuth = null;
if (ACCESS_PASSWORD) {
  legacyAuth = createAuthManager({ password: ACCESS_PASSWORD });
}

let userAuth = null;
let tkmindProxy = null;
let sessionSnapshotService = null;
let mindSpace = null;
let mindSpaceAssets = null;
let mindSpaceAudit = null;
let mindSpacePages = null;
let mindSpacePageLiveEdit = null;
let mindSpacePageEditSession = null;
let mindSpacePublications = null;
let plazaPosts = null;
let plazaEvents = null;
let plazaRecommend = null;
let plazaInteractions = null;
let plazaSeo = null;
let plazaOps = null;
let plazaRedis = createNoopPlazaRedis();
let mindSpaceCleanup = null;
let mindSpaceAgentJobs = null;
let mindSpaceAgentRunner = null;
let rechargeService = null;
let subscriptionService = null;
let wechatPayClient = null;
let wechatOAuthService = null;
let wechatMpService = null;
let scheduleService = null;
let scheduleReminderWorker = null;
let llmProviderService = null;
let wordFilterService = null;
let authPool = null;

async function bootstrapUserAuth() {
  try {
    if (!isDatabaseConfigured()) return false;
    const pool = createDbPool();
    authPool = pool;
    await initSchema(pool);
    await ensureMindSpaceConfig(pool, {
      env: process.env,
    });
    scheduleService = createScheduleService(pool, {
      defaultTimezone: process.env.H5_DEFAULT_TIMEZONE || 'Asia/Shanghai',
    });
    mindSpace = createMindSpaceService(pool, {
      maxFileBytes: Number(process.env.MINDSPACE_MAX_FILE_BYTES ?? DEFAULT_MAX_FILE_BYTES),
      aiDailyLimit: Number(process.env.MINDSPACE_FREE_AI_DAILY_LIMIT ?? 10),
      publicPageLimit: Number(process.env.MINDSPACE_FREE_PUBLIC_PAGE_LIMIT ?? 5),
      monthlyViewLimit: Number(process.env.MINDSPACE_FREE_MONTHLY_VIEW_LIMIT ?? 1000),
      scheduleService,
    });
    mindSpaceAssets = createAssetService(pool, {
      h5Root: __dirname,
      storageRoot:
        process.env.MINDSPACE_STORAGE_ROOT ?? path.join(__dirname, 'data', 'mindspace'),
      maxFileBytes: Number(process.env.MINDSPACE_MAX_FILE_BYTES ?? DEFAULT_MAX_FILE_BYTES),
    });
    mindSpacePages = createPageService(pool, {
      h5Root: __dirname,
      storageRoot:
        process.env.MINDSPACE_STORAGE_ROOT ?? path.join(__dirname, 'data', 'mindspace'),
    });
    mindSpacePageLiveEdit = createPageLiveEditService({
      pageService: mindSpacePages,
      resolveUserIdForAgentSession: async (sessionId) => {
        const [rows] = await pool.query(
          `SELECT user_id FROM h5_user_sessions WHERE agent_session_id = ? LIMIT 1`,
          [sessionId],
        );
        return rows[0]?.user_id ?? null;
      },
    });
    mindSpacePublications = createPublicationService(pool, {
      storageRoot:
        process.env.MINDSPACE_STORAGE_ROOT ?? path.join(__dirname, 'data', 'mindspace'),
      publicPageLimit: Number(process.env.MINDSPACE_FREE_PUBLIC_PAGE_LIMIT ?? 5),
    });
    await ensureAlgorithmConfig(pool);
    const plazaAlgorithmConfig = await loadAlgorithmConfig(pool);
    plazaRedis = await createPlazaRedis(process.env.PLAZA_REDIS_URL, pool);
    if (plazaRedis.enabled) {
      console.log('Plaza Redis enabled');
    }
    plazaSeo = createPlazaSeoService(pool);
    plazaInteractions = createPlazaInteractionService(pool, {
      formatPostRow,
      plazaRedis,
    });
    plazaEvents = createPlazaEventService(pool);
    plazaRecommend = createPlazaRecommendService(pool, {
      eventService: plazaEvents,
      formatPostRow,
      loadViewerReactions: (viewerId, postIds) =>
        plazaInteractions.loadViewerReactions(viewerId, postIds),
      algorithmConfig: plazaAlgorithmConfig,
    });
    plazaPosts = createPlazaPostService(pool, {
      loadViewerReactions: (viewerId, postIds) =>
        plazaInteractions.loadViewerReactions(viewerId, postIds),
      plazaRedis,
      algorithmConfig: plazaAlgorithmConfig,
      recommendService: plazaRecommend,
      onPostPublished: (postId) => plazaSeo?.notifyPostPublished(postId),
      loadFeaturedPosts: async (viewerId) => {
        if (!plazaOps) return { homepage_banner: [], trending: [], category_top: {} };
        return plazaOps.loadActiveFeaturedPosts(viewerId);
      },
    });
    plazaOps = createPlazaOpsService(pool, {
      formatPostRow,
      reviewPost: (...args) => plazaPosts.reviewPost(...args),
      invalidateFeedCaches: () => plazaRedis?.invalidateFeedCaches?.(),
    });
    startPlazaTasks({
      pool,
      plazaRedis,
      recalculateHotScores,
      writebackPublications,
    });
    mindSpaceCleanup = createCleanupService(pool, {
      storageRoot:
        process.env.MINDSPACE_STORAGE_ROOT ?? path.join(__dirname, 'data', 'mindspace'),
      h5Root: __dirname,
    });
    await ensurePlanCatalogSchema(pool);
    const planCatalogService = createPlanCatalogService(pool);
    subscriptionService = createSubscriptionService(pool, {
      getPlanAsync: (planType) => planCatalogService.getPlan(planType),
    });
    subscriptionService._planCatalogService = planCatalogService;
    userAuth = createUserAuth(pool, {
      usersRoot: USERS_ROOT,
      h5Root: __dirname,
      defaultSignupBalanceCents: Number(process.env.H5_SIGNUP_BALANCE_CENTS ?? 500),
      subscriptionService,
    });
    wechatPayClient = createWechatPayClient(loadWechatPayConfig());
    wechatOAuthService = createWechatOAuthService(pool, loadWechatOAuthConfig(), { userAuth });
    rechargeService = createRechargeService(pool, {
      userAuth,
      wechatPay: wechatPayClient,
    });
    if (wechatPayClient.enabled) {
      console.log(`WeChat Pay recharge enabled (${wechatPayClient.apiVersion ?? 'unknown'})`);
    }
    if (wechatOAuthService.enabled) {
      console.log('WeChat OAuth login enabled');
    }
    mindSpaceAgentJobs = createAgentJobService(pool, {
      pageService: mindSpacePages,
      storageRoot:
        process.env.MINDSPACE_STORAGE_ROOT ?? path.join(__dirname, 'data', 'mindspace'),
      maxOutputBytes: Number(process.env.MINDSPACE_MAX_FILE_BYTES ?? DEFAULT_MAX_FILE_BYTES),
    });
    mindSpaceAgentRunner = createMindSpaceAgentRunner({
      apiTarget: API_TARGET,
      apiSecret: API_SECRET,
      userAuth,
      agentJobService: mindSpaceAgentJobs,
    });
    mindSpaceAudit = createMindSpaceAuditWriter(pool);
    if (WORKSPACE_MAINTENANCE_ENABLED) {
      startWorkspaceThumbnailWatcher(path.join(__dirname, PUBLISH_ROOT_DIR));
      startWorkspaceAssetSyncWatcher({
        publishRoot: path.join(__dirname, PUBLISH_ROOT_DIR),
        syncUserWorkspaceByDirKey: async (dirKey, options) => {
          let userId = dirKey;
          if (!PUBLISH_KEY_UUID.test(dirKey)) {
            const [rows] = await pool.query(`SELECT id FROM h5_users WHERE username = ? LIMIT 1`, [
              dirKey,
            ]);
            userId = rows[0]?.id;
          }
          if (!userId) return;
          await mindSpaceAssets.syncWorkspaceAssets(userId, options);
        },
      });
      void mindSpaceAssets.expireStaleUploads().catch(() => {});
      setInterval(() => {
        void mindSpaceAssets?.expireStaleUploads().catch(() => {});
      }, 5 * 60 * 1000).unref?.();
    } else {
      console.log('Workspace maintenance daemons disabled (MEMIND_WORKSPACE_MAINTENANCE=0)');
    }
    await userAuth.ensureAdminUser();
    llmProviderService = createLlmProviderService(pool, {
      apiTarget: API_TARGET,
      apiSecret: API_SECRET,
    });
    wordFilterService = createWordFilterService(pool);
    void llmProviderService
      .ensureBootstrapRelay()
      .then((result) => {
        if (result.created) {
          console.log(`LLM relay bootstrap created: ${RELAY_BOOTSTRAP.name}`);
        }
      })
      .catch((err) => {
        console.warn('LLM relay bootstrap skipped:', err instanceof Error ? err.message : err);
      });
    void llmProviderService.syncSelectedToGoosed().catch((err) => {
      console.warn('LLM provider boot sync skipped:', err instanceof Error ? err.message : err);
    });
    sessionSnapshotService = createSessionSnapshotService(pool);
    tkmindProxy = createTkmindProxy({
      apiTarget: API_TARGET,
      apiTargets: API_TARGETS,
      apiSecret: API_SECRET,
      userAuth,
      llmProviderService,
      subscriptionService,
      localFetchAsset: mindSpaceAssets
        ? async (userId, assetId) => {
            const { asset, path: assetPath } = await mindSpaceAssets.readAsset(userId, assetId);
            const buffer = await fs.promises.readFile(assetPath);
            return { buffer, mimeType: asset.mimeType };
          }
        : null,
    });
    wechatMpService = createWechatMpService({
      config: WECHAT_MP_CONFIG,
      userAuth,
      apiFetch: tkmindProxy.apiFetch,
      sessionApiFetch: async (sessionId, pathname, init) => {
        const target = await tkmindProxy.resolveTarget(sessionId);
        return tkmindProxy.apiFetchTo(target, pathname, init);
      },
      scheduleService: process.env.H5_SCHEDULE_ENABLED === '1' ? scheduleService : null,
      applySessionLlmProvider: (sessionId) => tkmindProxy.applySessionLlmProvider(sessionId),
    });
    userAuth.setRechargeNotifier(async ({ userId, title, body }) => {
      if (!wechatMpService?.enabled) return;
      await wechatMpService.sendTextToUser(userId, `${title}\n${body}`.trim());
    });
    if (
      process.env.H5_REMINDER_WORKER_ENABLED === '1' &&
      wechatMpService?.enabled &&
      scheduleService
    ) {
      scheduleReminderWorker = startScheduleReminderWorker({
        scheduleService,
        sendWechatTextToUser: (userId, text) => wechatMpService.sendTextToUser(userId, text),
      });
      console.log('Schedule reminder worker enabled');
    }
    if (subscriptionService) {
      const subExpiryTimer = setInterval(async () => {
        try {
          const { renewed, failed } = await subscriptionService.processAutoRenewals();
          if (renewed > 0) console.log(`Auto-renewed ${renewed} subscription(s)`);
          if (failed > 0) console.log(`Auto-renew failed for ${failed} subscription(s) (balance insufficient)`);
          const n = await subscriptionService.expireStaleSubscriptions();
          if (n > 0) console.log(`Expired ${n} stale subscription(s)`);
        } catch (err) {
          console.warn('Subscription expiry check failed:', err);
        }
      }, 60 * 60 * 1000); // hourly
      subExpiryTimer.unref?.();
    }
    mindSpacePageEditSession = createPageEditSessionService({
      apiTarget: API_TARGET,
      apiSecret: API_SECRET,
      userAuth,
      pageService: mindSpacePages,
      pageLiveEdit: mindSpacePageLiveEdit,
      llmProviderService,
    });
    if (wechatMpService?.enabled) {
      console.log('WeChat MP webhook enabled');
    }
    console.log(`User auth enabled (MySQL), workspace root: ${USERS_ROOT}`);
    return true;
  } catch (err) {
    console.error('User auth bootstrap failed:', err);
    return false;
  }
}

const userAuthReady = bootstrapUserAuth();

function legacySessionToken(req) {
  return parseCookies(req.get('cookie'))[AUTH_COOKIE];
}

function userToken(req) {
  return parseCookies(req.get('cookie'))[USER_COOKIE];
}

function setUserLoginCookies(res, req, token) {
  res.set(
    'Set-Cookie',
    userLoginCookies(token, isSecureRequest(req), resolveCookieDomainForRequest(req)),
  );
}

function clearUserLoginCookies(res, req) {
  res.set(
    'Set-Cookie',
    clearUserSessionCookie(isSecureRequest(req), resolveCookieDomainForRequest(req)),
  );
}

async function attachUserSession(req, _res, next) {
  if (!userAuth) return next();
  const token = userToken(req);
  req.userToken = token;
  req.userSession = token ? await userAuth.verify(token) : null;
  next();
}

app.use(attachUserSession);

// ============ Legacy password auth ============

app.get('/auth/status', async (req, res) => {
  await userAuthReady;
  if (userAuth) {
    const me = await userAuth.getMe(userToken(req));
    if (!me) return res.json({ authenticated: false, mode: 'user' });
    const row = await userAuth.getUserById(me.id);
    const capabilityState = await userAuth.resolveUserCapabilities(row);
    return res.json({
      authenticated: true,
      user: me,
      mode: 'user',
      capabilities: capabilityState.capabilities,
      grantedSkills: capabilityState.grantedSkills ?? [],
      unrestricted: capabilityState.unrestricted,
    });
  }
  if (legacyAuth) {
    return res.json({
      authenticated: legacyAuth.verify(legacySessionToken(req)),
      mode: 'legacy',
    });
  }
  return res.json({ authenticated: false, mode: 'none' });
});

app.post('/auth/login', jsonBody, async (req, res) => {
  await userAuthReady;
  const secure = isSecureRequest(req);

  if (userAuth) {
    const { username, password } = req.body ?? {};
    if (!username || !password) {
      return res.status(400).json({ message: '用户名和密码不能为空' });
    }
    const result = await userAuth.login({ username, password, ip: req.ip });
    if (!result.ok) {
      if (result.retryAfterMs > 0) {
        res.set('Retry-After', String(Math.ceil(result.retryAfterMs / 1000)));
        return res.status(429).json({ message: result.message });
      }
      return res.status(401).json({ message: result.message });
    }
    setUserLoginCookies(res, req, result.token);
    return res.json({ authenticated: true, user: result.user, mode: 'user' });
  }

  if (!legacyAuth) {
    return res.status(503).json({ message: '未配置用户数据库或访问密码' });
  }

  const password = typeof req.body?.password === 'string' ? req.body.password : '';
  const result = legacyAuth.login(password, req.ip);
  if (!result.ok) {
    if (result.retryAfterMs > 0) {
      res.set('Retry-After', String(Math.ceil(result.retryAfterMs / 1000)));
      return res.status(429).json({ message: '尝试次数过多，请稍后再试' });
    }
    return res.status(401).json({ message: '密码错误，请重试' });
  }
  res.set('Set-Cookie', sessionCookie(result.token, secure));
  return res.json({ authenticated: true, mode: 'legacy' });
});

app.post('/auth/register', jsonBody, async (req, res) => {
  await userAuthReady;
  if (!userAuth) {
    return res.status(503).json({ message: '未启用用户注册' });
  }
  const { username, password, displayName, email } = req.body ?? {};
  const result = await userAuth.register({ username, password, displayName, email });
  if (!result.ok) {
    const status = result.message.includes('已存在') ? 409 : 400;
    return res.status(status).json({ message: result.message });
  }
  if (plazaSeo && req.body?.utm_source) {
    void plazaSeo
      .recordAttribution(
        {
          event_type: 'signup',
          utm_source: req.body.utm_source,
          utm_medium: req.body.utm_medium,
          utm_campaign: req.body.utm_campaign,
          ref_id: req.body.ref ?? req.body.ref_id,
          user_id: result.user?.id ?? null,
        },
        plazaClientIp(req),
      )
      .catch(() => {});
  }
  return res.json({ ok: true, user: result.user });
});

app.post('/auth/reset-password', jsonBody, async (req, res) => {
  await userAuthReady;
  if (!userAuth) {
    return res.status(503).json({ message: '未启用用户系统' });
  }
  const { username, email, password } = req.body ?? {};
  const result = await userAuth.resetPassword({ username, email, password });
  if (!result.ok) {
    return res.status(400).json({ message: result.message });
  }
  return res.json({ ok: true });
});

app.get('/auth/wechat/config', async (req, res) => {
  await userAuthReady;
  if (!wechatOAuthService?.enabled) {
    return res.json({
      enabled: false,
      inWechat: isWechatUserAgent(req.get('user-agent') || ''),
      scanEnabled: false,
    });
  }
  return res.json(wechatOAuthService.publicConfig(req));
});

app.get('/auth/wechat/js-sdk-signature', async (req, res) => {
  await userAuthReady;
  if (!userAuth || !wechatMpService?.enabled) {
    return res.status(503).json({ message: '微信 JS-SDK 未启用' });
  }
  const me = await userAuth.getMe(userToken(req));
  if (!me) return res.status(401).json({ message: '未登录' });

  const pageUrl = String(req.query?.url ?? '').split('#')[0];
  if (!pageUrl) {
    return res.status(400).json({ message: '缺少 url' });
  }
  try {
    const publicHost = WECHAT_MP_CONFIG.publicBaseUrl
      ? new URL(WECHAT_MP_CONFIG.publicBaseUrl).host
      : null;
    const target = new URL(pageUrl);
    const requestHost = req.get('x-forwarded-host') || req.get('host') || '';
    if (publicHost && target.host !== publicHost && target.host !== requestHost) {
      return res.status(400).json({ message: 'url 不属于当前 H5 域名' });
    }
    const payload = await wechatMpService.createJsSdkSignature(pageUrl);
    return res.json(payload);
  } catch (err) {
    const message = err instanceof Error ? err.message : '微信 JS-SDK 签名失败';
    console.warn('WeChat JS-SDK signature failed:', message);
    return res.status(502).json({ message });
  }
});

app.get('/auth/wechat/status', async (req, res) => {
  await userAuthReady;
  if (!userAuth || !wechatOAuthService?.enabled) {
    return res.json({ enabled: false, bound: false });
  }
  const me = await userAuth.getMe(userToken(req));
  if (!me) return res.status(401).json({ message: '未登录' });
  const config = loadWechatOAuthConfig();
  const status = await userAuth.getWechatBindingStatus(me.id, config.appId);
  return res.json({ enabled: true, ...status });
});

if (WECHAT_MP_CONFIG.enabled) {
  app.get('/auth/wechat/agent-route', async (req, res) => {
    await userAuthReady;
    if (!userAuth || !wechatMpService?.enabled) {
      return res.status(503).json({ message: '公众号 Agent 未启用' });
    }
    const me = await userAuth.getMe(userToken(req));
    if (!me) return res.status(401).json({ message: '未登录' });
    return res.json(await wechatMpService.getRouteStatusForUser(me.id));
  });

  app.post('/auth/wechat/agent-route/reset', async (req, res) => {
    await userAuthReady;
    if (!userAuth || !wechatMpService?.enabled) {
      return res.status(503).json({ message: '公众号 Agent 未启用' });
    }
    const me = await userAuth.getMe(userToken(req));
    if (!me) return res.status(401).json({ message: '未登录' });
    try {
      const result = await wechatMpService.recreateRouteForUser(me.id);
      if (!result.ok) return res.status(400).json({ message: result.message });
      return res.json(result);
    } catch (err) {
      return res.status(500).json({
        message: err instanceof Error ? err.message : '重建公众号 Agent 路由失败',
      });
    }
  });
}

app.get('/auth/wechat/pending/:token', async (req, res) => {
  await userAuthReady;
  if (!userAuth) return res.status(503).json({ message: '未启用用户系统' });
  const pending = await userAuth.getWechatPendingBind(req.params.token);
  if (!pending) return res.status(404).json({ message: '绑定会话已过期，请重新微信登录' });
  return res.json({
    nickname: pending.nickname,
    avatarUrl: pending.avatar_url,
    returnTo: pending.return_to || '/',
  });
});

app.post('/auth/wechat/register', jsonBody, async (req, res) => {
  await userAuthReady;
  const secure = isSecureRequest(req);
  if (!userAuth) return res.status(503).json({ message: '未启用用户系统' });
  const pendingToken = typeof req.body?.pendingToken === 'string' ? req.body.pendingToken : '';
  if (!pendingToken) return res.status(400).json({ message: '缺少绑定会话' });
  const result = await userAuth.completeWechatRegister({ pendingToken });
  if (!result.ok) return res.status(400).json({ message: result.message });
  if (result.isNewUser && plazaSeo) {
    void plazaSeo
      .recordAttribution(
        {
          event_type: 'signup',
          utm_source: result.utmSource || 'wechat',
          utm_medium: result.utmMedium,
          utm_campaign: result.utmCampaign,
          user_id: result.user?.id ?? null,
        },
        plazaClientIp(req),
      )
      .catch(() => {});
  }
  setUserLoginCookies(res, req, result.token);
  return res.json({
    authenticated: true,
    user: result.user,
    returnTo: result.returnTo || '/',
  });
});

app.post('/auth/wechat/bind', jsonBody, async (req, res) => {
  await userAuthReady;
  const secure = isSecureRequest(req);
  if (!userAuth) return res.status(503).json({ message: '未启用用户系统' });
  const pendingToken = typeof req.body?.pendingToken === 'string' ? req.body.pendingToken : '';
  const username = typeof req.body?.username === 'string' ? req.body.username : '';
  const password = typeof req.body?.password === 'string' ? req.body.password : '';
  if (!pendingToken) return res.status(400).json({ message: '缺少绑定会话' });
  if (!username || !password) {
    return res.status(400).json({ message: '用户名和密码不能为空' });
  }
  const result = await userAuth.completeWechatBindAccount({
    pendingToken,
    username,
    password,
    ip: req.ip,
  });
  if (!result.ok) {
    const status = result.retryAfterMs > 0 ? 429 : 401;
    if (result.retryAfterMs > 0) {
      res.set('Retry-After', String(Math.ceil(result.retryAfterMs / 1000)));
    }
    return res.status(status).json({ message: result.message });
  }
  setUserLoginCookies(res, req, result.token);
  return res.json({
    authenticated: true,
    user: result.user,
    bound: true,
    returnTo: result.returnTo || '/',
  });
});

app.post('/auth/wechat/scan/start', async (req, res) => {
  await userAuthReady;
  if (!wechatOAuthService?.enabled) {
    return res.status(503).json({ message: '微信登录未启用' });
  }
  try {
    const payload = await wechatOAuthService.startScanLogin(req);
    return res.json(payload);
  } catch (err) {
    return res.status(503).json({
      message: err instanceof Error ? err.message : '微信扫码登录不可用',
    });
  }
});

app.get('/auth/wechat/scan/poll', async (req, res) => {
  await userAuthReady;
  if (!wechatOAuthService?.enabled) {
    return res.status(503).json({ message: '微信登录未启用' });
  }
  const state = typeof req.query?.state === 'string' ? req.query.state : '';
  if (!state) return res.status(400).json({ message: '缺少扫码状态' });
  const result = await wechatOAuthService.pollScanLogin(state);
  if (result.status === 'complete' && result.token) {
    setUserLoginCookies(res, req, result.token);
  }
  return res.json(result);
});

app.get('/auth/wechat/authorize', async (req, res) => {
  await userAuthReady;
  if (!wechatOAuthService?.enabled) {
    return res.status(503).json({ message: '微信登录未启用' });
  }
  try {
    let bindUserId = null;
    const intent = typeof req.query?.intent === 'string' ? req.query.intent.trim().toLowerCase() : 'login';
    if (intent === 'bind' && userAuth) {
      const me = await userAuth.getMe(userToken(req));
      if (!me) return res.status(401).json({ message: '请先登录后再绑定微信' });
      bindUserId = me.id;
    }
    const redirectUrl = await wechatOAuthService.buildAuthorizeRedirect(req, { bindUserId });
    return res.redirect(302, redirectUrl);
  } catch (err) {
    console.error('WeChat authorize failed:', err);
    return res.status(500).json({ message: err instanceof Error ? err.message : '微信授权失败' });
  }
});

app.get('/auth/wechat/callback', async (req, res) => {
  await userAuthReady;
  const secure = isSecureRequest(req);
  if (!wechatOAuthService?.enabled || !userAuth) {
    return res.redirect(302, '/?wechat_error=unavailable');
  }
  try {
    const result = await wechatOAuthService.handleCallback({
      code: typeof req.query?.code === 'string' ? req.query.code : '',
      state: typeof req.query?.state === 'string' ? req.query.state : '',
      ip: req.ip,
    });

    if (result.action === 'binding_gate') {
      const params = new URLSearchParams();
      params.set('wechat_pending', result.pendingToken);
      if (result.returnTo && result.returnTo !== '/') {
        params.set('return_to', result.returnTo);
      }
      return res.redirect(302, `/?${params.toString()}`);
    }

    if (result.action === 'poll_error') {
      return res.redirect(
        302,
        `/?wechat_error=${encodeURIComponent(result.message || '微信登录失败')}`,
      );
    }

    if (result.isNewUser && plazaSeo) {
      void plazaSeo
        .recordAttribution(
          {
            event_type: 'signup',
            utm_source: result.utmSource || 'wechat',
            utm_medium: result.utmMedium,
            utm_campaign: result.utmCampaign,
            user_id: result.user?.id ?? null,
          },
          plazaClientIp(req),
        )
        .catch(() => {});
    }

    if (result.authMode === 'open' || result.authMode === 'scan') {
      return res.send(`<!DOCTYPE html><html lang="zh-CN"><head><meta charset="utf-8"><title>微信登录</title></head><body><p>扫码登录成功，请返回电脑继续操作。</p></body></html>`);
    }

    setUserLoginCookies(res, req, result.token);
    return res.redirect(302, result.returnTo || '/');
  } catch (err) {
    console.error('WeChat callback failed:', err);
    const message = encodeURIComponent(err instanceof Error ? err.message : '微信登录失败');
    return res.redirect(302, `/?wechat_error=${message}`);
  }
});

app.get('/auth/me', async (req, res) => {
  await userAuthReady;
  if (!userAuth) return res.status(503).json({ message: '未启用用户系统' });
  const me = await userAuth.getMe(userToken(req));
  if (!me) return res.status(401).json({ message: '未登录' });
  const [paths, capabilityState, subscription] = await Promise.all([
    userAuth.listPathGrants(me.id),
    userAuth.resolveUserCapabilities(await userAuth.getUserById(me.id)),
    subscriptionService ? subscriptionService.getActiveSubscription(me.id) : null,
  ]);
  return res.json({
    user: { ...me, subscription },
    paths,
    capabilities: capabilityState.capabilities,
    grantedSkills: capabilityState.grantedSkills ?? [],
    unrestricted: capabilityState.unrestricted,
  });
});

app.post('/auth/logout', async (req, res) => {
  await userAuthReady;
  const secure = isSecureRequest(req);
  if (userAuth) {
    await userAuth.revoke(userToken(req));
    clearUserLoginCookies(res, req);
  }
  if (legacyAuth) {
    legacyAuth.revoke(legacySessionToken(req));
    res.set('Set-Cookie', clearSessionCookie(secure));
  }
  res.status(204).end();
});


app.get('/auth/usage', async (req, res) => {
  await userAuthReady;
  if (!userAuth) return res.status(503).json({ message: '未启用用户系统' });
  const me = await userAuth.getMe(userToken(req));
  if (!me) return res.status(401).json({ message: '未登录' });
  const records = await userAuth.listUsageRecords({ userId: me.id, limit: 30 });
  res.json({ records });
});

app.get('/auth/notifications', async (req, res) => {
  await userAuthReady;
  if (!userAuth || !scheduleService) {
    return res.status(503).json({ message: '通知服务未启用' });
  }
  const me = await userAuth.getMe(userToken(req));
  if (!me) return res.status(401).json({ message: '未登录' });
  const rawStatus = typeof req.query?.status === 'string' ? req.query.status : 'unread';
  const status = ['all', 'unread', 'read'].includes(rawStatus) ? rawStatus : 'all';
  const limit = Math.min(Math.max(Number(req.query?.limit) || 20, 1), 100);
  try {
    const notifications = await scheduleService.listUserNotifications({ userId: me.id, status, limit });
    res.json({ notifications });
  } catch (err) {
    console.warn('List user notifications failed:', err instanceof Error ? err.message : err);
    res.status(500).json({ message: '通知列表加载失败' });
  }
});

app.get('/auth/notifications/events', async (req, res) => {
  await userAuthReady;
  if (!userAuth || !scheduleService) {
    return res.status(503).json({ message: '通知服务未启用' });
  }
  const me = await userAuth.getMe(userToken(req));
  if (!me) return res.status(401).json({ message: '未登录' });

  res.status(200);
  res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
  res.setHeader('Cache-Control', 'no-cache, no-transform');
  res.setHeader('Connection', 'keep-alive');
  res.flushHeaders?.();

  let closed = false;
  let lastNotificationId = null;

  const sendEvent = (event, data) => {
    if (closed || res.destroyed) return;
    res.write(`event: ${event}\n`);
    res.write(`data: ${JSON.stringify(data)}\n\n`);
  };

  const checkUnread = async () => {
    if (closed) return;
    try {
      const notifications = await scheduleService.listUserNotifications({
        userId: me.id,
        status: 'unread',
        limit: 1,
      });
      const latest = notifications[0] ?? null;
      const nextId = latest?.id ?? null;
      if (nextId && nextId !== lastNotificationId) {
        lastNotificationId = nextId;
        sendEvent('notification', { notification: latest });
      } else if (!nextId) {
        lastNotificationId = null;
      }
    } catch {
      sendEvent('sync', { reason: 'check_failed' });
    }
  };

  sendEvent('ready', { ok: true });
  await checkUnread();

  const checkTimer = setInterval(() => {
    void checkUnread();
  }, 2500);
  const keepaliveTimer = setInterval(() => {
    sendEvent('ping', { at: Date.now() });
  }, 25000);

  req.on('close', () => {
    closed = true;
    clearInterval(checkTimer);
    clearInterval(keepaliveTimer);
  });
});

app.post('/auth/notifications/:id/read', async (req, res) => {
  await userAuthReady;
  if (!userAuth || !scheduleService) {
    return res.status(503).json({ message: '通知服务未启用' });
  }
  const me = await userAuth.getMe(userToken(req));
  if (!me) return res.status(401).json({ message: '未登录' });
  const ok = await scheduleService.markUserNotificationRead({
    userId: me.id,
    notificationId: req.params.id,
  });
  if (!ok) return res.status(404).json({ message: '通知不存在或已读' });
  res.json({ ok: true });
});

app.post('/auth/notifications/read-all', async (req, res) => {
  await userAuthReady;
  if (!userAuth || !scheduleService) {
    return res.status(503).json({ message: '通知服务未启用' });
  }
  const me = await userAuth.getMe(userToken(req));
  if (!me) return res.status(401).json({ message: '未登录' });
  const updated = await scheduleService.markAllUserNotificationsRead({ userId: me.id });
  res.json({ ok: true, updated });
});

app.delete('/auth/notifications/:id', async (req, res) => {
  await userAuthReady;
  if (!userAuth || !scheduleService) {
    return res.status(503).json({ message: '通知服务未启用' });
  }
  const me = await userAuth.getMe(userToken(req));
  if (!me) return res.status(401).json({ message: '未登录' });
  const ok = await scheduleService.deleteUserNotification({
    userId: me.id,
    notificationId: req.params.id,
  });
  if (!ok) return res.status(404).json({ message: '通知不存在' });
  res.status(204).end();
});

app.delete('/auth/notifications', async (req, res) => {
  await userAuthReady;
  if (!userAuth || !scheduleService) {
    return res.status(503).json({ message: '通知服务未启用' });
  }
  const me = await userAuth.getMe(userToken(req));
  if (!me) return res.status(401).json({ message: '未登录' });
  const status = typeof req.query?.status === 'string' ? req.query.status : 'all';
  const deleted = await scheduleService.clearUserNotifications({ userId: me.id, status });
  res.json({ ok: true, deleted });
});

app.get('/auth/billing/ledger', async (req, res) => {
  await userAuthReady;
  if (!userAuth) return res.status(503).json({ message: '未启用用户系统' });
  const me = await userAuth.getMe(userToken(req));
  if (!me) return res.status(401).json({ message: '未登录' });
  const limit = Math.min(Math.max(Number(req.query?.limit) || 30, 1), 100);
  const entries = await userAuth.listBillingLedger({
    userId: me.id,
    limit,
    types: ['recharge', 'adjust', 'refund'],
  });
  res.json({ entries });
});

app.get('/auth/billing/config', async (req, res) => {
  await userAuthReady;
  if (!userAuth || !rechargeService) {
    return res.status(503).json({ message: '未启用计费系统' });
  }
  const me = await userAuth.getMe(userToken(req));
  if (!me) return res.status(401).json({ message: '未登录' });
  const [config, sub] = await Promise.all([
    rechargeService.getBillingConfig(me.id),
    subscriptionService ? subscriptionService.getActiveSubscription(me.id) : null,
  ]);
  return res.json({ ...config, subscription: sub });
});

app.get('/auth/billing/subscription', async (req, res) => {
  await userAuthReady;
  if (!userAuth) return res.status(503).json({ message: '未启用用户系统' });
  const me = await userAuth.getMe(userToken(req));
  if (!me) return res.status(401).json({ message: '未登录' });
  const sub = subscriptionService
    ? await subscriptionService.getActiveSubscription(me.id)
    : null;
  const planCatalog = subscriptionService?._planCatalogService;
  const plans = (!sub && planCatalog)
    ? await planCatalog.listPlans({ includeInactive: false })
    : sub ? undefined : PLAN_CATALOG;
  return res.json({ subscription: sub, plans });
});

app.get('/auth/billing/plans', async (req, res) => {
  await userAuthReady;
  if (!userAuth) return res.status(503).json({ message: '未启用用户系统' });
  const me = await userAuth.getMe(userToken(req));
  if (!me) return res.status(401).json({ message: '未登录' });
  const planCatalog = subscriptionService?._planCatalogService;
  const plans = planCatalog
    ? (await planCatalog.listPlans({ includeInactive: false }))
        .filter((p) => p.priceCents > 0)
        .map((p) => ({ key: p.planType, ...p }))
    : Object.entries(PLAN_CATALOG)
        .filter(([, plan]) => plan.priceCents > 0)
        .map(([key, plan]) => ({ key, ...plan }));
  const [sub, wallet] = await Promise.all([
    subscriptionService ? subscriptionService.getActiveSubscription(me.id) : null,
    userAuth.getUserById(me.id),
  ]);
  return res.json({
    plans,
    subscription: sub,
    balanceCents: wallet ? Number(wallet.balance_cents ?? 0) : 0,
  });
});

app.post('/auth/billing/subscribe', jsonBody, async (req, res) => {
  await userAuthReady;
  if (!userAuth || !subscriptionService) {
    return res.status(503).json({ message: '未启用订阅系统' });
  }
  const me = await userAuth.getMe(userToken(req));
  if (!me) return res.status(401).json({ message: '未登录' });
  if (me.status === 'disabled') return res.status(403).json({ message: '账户已禁用' });
  const { planType, autoRenew = false } = req.body ?? {};
  if (!planType || typeof planType !== 'string') {
    return res.status(400).json({ message: '请选择套餐' });
  }
  const result = await subscriptionService.purchaseSubscription(me.id, planType, Boolean(autoRenew));
  if (!result.ok) {
    if (result.code === 'INSUFFICIENT_BALANCE') {
      return res.status(402).json({
        message: result.message,
        code: result.code,
        balanceCents: result.balanceCents,
        requiredCents: result.requiredCents,
        shortfallCents: result.shortfallCents,
      });
    }
    if (result.code === 'DOWNGRADE_NOT_ALLOWED') {
      return res.status(409).json({
        message: result.message,
        code: result.code,
        currentPlanType: result.currentPlanType,
      });
    }
    return res.status(400).json({ message: result.message });
  }
  return res.json({ subscription: result.subscription, balanceCents: result.balanceCents });
});

app.post('/auth/billing/space-purchase', jsonBody, async (req, res) => {
  await userAuthReady;
  if (!userAuth || !mindSpace) {
    return res.status(503).json({ message: '未启用空间系统' });
  }
  const me = await userAuth.getMe(userToken(req));
  if (!me) return res.status(401).json({ message: '未登录' });
  const sizeMb = Number(req.body?.sizeMb);
  const result = await userAuth.purchaseSpaceQuota(me.id, sizeMb);
  if (!result.ok) {
    if (result.code === 'INSUFFICIENT_BALANCE') {
      return res.status(402).json({
        message: result.message,
        code: result.code,
        details: {
          code: 'INSUFFICIENT_BALANCE',
          balanceCents: result.balanceCents,
          minRechargeCents: result.minRechargeCents,
          suggestedTiers: result.suggestedTiers,
        },
      });
    }
    return res.status(400).json({ message: result.message });
  }
  const quota = await mindSpace.getQuota(me.id);
  return res.json({
    quota: quota ?? result.quota,
    balanceCents: result.balanceCents,
    purchasedMb: sizeMb,
    costCents: sizeMb * 200,
  });
});

app.post('/auth/billing/auto-renew', jsonBody, async (req, res) => {
  await userAuthReady;
  if (!userAuth || !subscriptionService) {
    return res.status(503).json({ message: '未启用订阅系统' });
  }
  const me = await userAuth.getMe(userToken(req));
  if (!me) return res.status(401).json({ message: '未登录' });
  const { enabled } = req.body ?? {};
  if (typeof enabled !== 'boolean') {
    return res.status(400).json({ message: '请传入 enabled: true/false' });
  }
  const result = await subscriptionService.setAutoRenew(me.id, enabled);
  return res.json(result);
});

app.post('/auth/billing/recharge-orders', jsonBody, async (req, res) => {
  await userAuthReady;
  if (!userAuth || !rechargeService) {
    return res.status(503).json({ message: '未启用计费系统' });
  }
  const me = await userAuth.getMe(userToken(req));
  if (!me) return res.status(401).json({ message: '未登录' });
  const amountCents = Number(req.body?.amountCents);
  const payScene = ['native', 'h5', 'jsapi'].includes(req.body?.payScene)
    ? req.body.payScene
    : 'native';
  const result = await rechargeService.createOrder({
    userId: me.id,
    amountCents,
    payScene,
    clientIp: req.ip,
  });
  if (!result.ok) return res.status(400).json({ message: result.message });
  return res.status(201).json({ order: result.order });
});

app.get('/auth/billing/recharge-orders/:orderId', async (req, res) => {
  await userAuthReady;
  if (!userAuth || !rechargeService) {
    return res.status(503).json({ message: '未启用计费系统' });
  }
  const me = await userAuth.getMe(userToken(req));
  if (!me) return res.status(401).json({ message: '未登录' });
  const order = await rechargeService.getOrderForUser(me.id, req.params.orderId);
  if (!order) return res.status(404).json({ message: '订单不存在' });
  let balanceCents = null;
  if (order.status === 'paid') {
    const user = await userAuth.getUserById(me.id);
    balanceCents = user ? Number(user.balance_cents ?? 0) : null;
  }
  return res.json({ order, balanceCents });
});

const wechatNotifyBody = express.raw({
  type: ['application/json', 'text/xml', 'application/xml'],
  limit: '64kb',
});
const wechatMpBody = express.text({
  type: ['text/xml', 'application/xml'],
  limit: '128kb',
});
app.post('/webhooks/wechat-pay/notify', wechatNotifyBody, async (req, res) => {
  await userAuthReady;
  const isV2 = wechatPayClient?.apiVersion === 'v2';
  if (!rechargeService || !wechatPayClient?.enabled) {
    if (isV2) {
      return res
        .status(503)
        .type('text/xml')
        .send(
          '<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[支付未启用]]></return_msg></xml>',
        );
    }
    return res.status(503).json({ code: 'FAIL', message: '支付未启用' });
  }
  try {
    const bodyText = Buffer.isBuffer(req.body) ? req.body.toString('utf8') : String(req.body ?? '');
    await rechargeService.handleWechatNotify({ headers: req.headers, body: bodyText });
    if (isV2) {
      return res.type('text/xml').send(WECHAT_NOTIFY_SUCCESS_V2);
    }
    return res.json({ code: 'SUCCESS', message: '成功' });
  } catch (err) {
    console.error('WeChat notify failed:', err);
    const message = err instanceof Error ? err.message : '处理失败';
    if (isV2) {
      return res
        .status(500)
        .type('text/xml')
        .send(
          `<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[${message}]]></return_msg></xml>`,
        );
    }
    return res.status(500).json({ code: 'FAIL', message });
  }
});

if (WECHAT_MP_CONFIG.enabled) {
  app.get('/webhooks/wechat-mp/messages', async (req, res) => {
    await userAuthReady;
    if (!wechatMpService?.enabled) {
      return res.status(503).send('wechat mp disabled');
    }
    const result = wechatMpService.verifyUrlChallenge(req.query);
    if (!result.ok) {
      console.warn('WeChat MP verify failed:', {
        encryptType: req.query.encrypt_type ?? null,
        timestamp: req.query.timestamp ?? null,
        nonce: req.query.nonce ?? null,
      });
      return res.status(result.status ?? 403).send(result.body ?? 'invalid signature');
    }
    console.log('WeChat MP verify ok:', {
      encryptType: req.query.encrypt_type ?? null,
      timestamp: req.query.timestamp ?? null,
      nonce: req.query.nonce ?? null,
    });
    return res.type('text/plain').send(String(result.body ?? ''));
  });

  app.post('/webhooks/wechat-mp/messages', wechatMpBody, async (req, res) => {
    await userAuthReady;
    if (!wechatMpService?.enabled) {
      return res.status(503).send('wechat mp disabled');
    }
    try {
      const bodyText = String(req.body ?? '');
      const fromUser = bodyText.match(/<FromUserName><!\[CDATA\[([\s\S]*?)\]\]><\/FromUserName>/)?.[1] ?? null;
      const msgType = bodyText.match(/<MsgType><!\[CDATA\[([\s\S]*?)\]\]><\/MsgType>/)?.[1] ?? null;
      const content = bodyText.match(/<Content><!\[CDATA\[([\s\S]*?)\]\]><\/Content>/)?.[1] ?? null;
      console.log('WeChat MP message received:', {
        at: new Date().toISOString(),
        fromUser: fromUser ? `${fromUser.slice(0, 8)}...` : null,
        msgType,
        contentPreview: content ? `${String(content).slice(0, 24)}` : null,
      });
      const result = await wechatMpService.handleInboundMessage(req.body, req.query);
      if (result.task) void result.task;
      if (result.contentType) res.type(result.contentType);
      return res.status(result.status ?? 200).send(result.body ?? 'success');
    } catch (err) {
      console.error('WeChat MP message failed:', err);
      return res.status(500).send('internal error');
    }
  });
}


// ============ Wiki API ============

function wikiCookie(token, secure) {
  const parts = [
    `${wikiAuth.COOKIE_NAME}=${encodeURIComponent(token)}`,
    'Path=/',
    'HttpOnly',
    'SameSite=Lax',
    'Max-Age=604800',
  ];
  if (secure) parts.push('Secure');
  return parts.join('; ');
}

function clearWikiCookie(secure) {
  const parts = [`${wikiAuth.COOKIE_NAME}=`, 'Path=/', 'HttpOnly', 'SameSite=Lax', 'Max-Age=0'];
  if (secure) parts.push('Secure');
  return parts.join('; ');
}

const wikiApi = express.Router();
wikiApi.use(jsonBody);

wikiApi.post('/auth/register', (req, res) => {
  const { username, password, displayName } = req.body || {};
  if (!username || !password) {
    return res.status(400).json({ message: '用户名和密码不能为空' });
  }
  const result = wikiAuth.register(username, password, displayName);
  if (!result.ok) return res.status(409).json({ message: result.message });
  return res.json({ ok: true, user: result.user });
});

wikiApi.post('/auth/login', (req, res) => {
  const { username, password } = req.body || {};
  if (!username || !password) {
    return res.status(400).json({ message: '用户名和密码不能为空' });
  }
  const result = wikiAuth.login(username, password);
  if (!result.ok) return res.status(401).json({ message: result.message });
  const secure = isSecureRequest(req);
  res.set('Set-Cookie', wikiCookie(result.token, secure));
  return res.json({ ok: true, user: result.user });
});

wikiApi.post('/auth/logout', (req, res) => {
  const cookies = parseCookies(req.get('cookie'));
  wikiAuth.revoke(cookies[wikiAuth.COOKIE_NAME]);
  res.set('Set-Cookie', clearWikiCookie(isSecureRequest(req)));
  return res.json({ ok: true });
});

wikiApi.get('/auth/me', (req, res) => {
  const cookies = parseCookies(req.get('cookie'));
  const session = wikiAuth.verify(cookies[wikiAuth.COOKIE_NAME]);
  if (!session) return res.json({ authenticated: false, user: null });
  const user = wikiAuth.getUser(session.username);
  return res.json({ authenticated: true, user });
});

function requireWikiAuth(req, res, next) {
  const cookies = parseCookies(req.get('cookie'));
  const session = wikiAuth.verify(cookies[wikiAuth.COOKIE_NAME]);
  if (!session) return res.status(401).json({ message: '未登录' });
  req.wikiUser = session;
  next();
}

wikiApi.get('/pages', requireWikiAuth, (req, res) => {
  const { q } = req.query;
  if (q) return res.json(wikiAuth.searchPages(req.wikiUser.username, q));
  res.json(wikiAuth.listPages(req.wikiUser.username));
});

wikiApi.get('/pages/:slug', requireWikiAuth, (req, res) => {
  const page = wikiAuth.getPage(req.wikiUser.username, req.params.slug);
  if (!page) return res.status(404).json({ message: '页面不存在' });
  res.json(page);
});

wikiApi.post('/pages/:slug', requireWikiAuth, (req, res) => {
  const { title, content, tags } = req.body || {};
  const page = wikiAuth.savePage(req.wikiUser.username, req.params.slug, title, content, tags);
  res.json(page);
});

wikiApi.delete('/pages/:slug', requireWikiAuth, (req, res) => {
  wikiAuth.deletePage(req.wikiUser.username, req.params.slug);
  res.json({ ok: true });
});

app.use('/wiki-api', wikiApi);

// ============ TKMind API proxy ============

const api = express.Router();
api.use(jsonUnlessMultipart);

api.use(async (req, res, next) => {
  await userAuthReady;
  if (req.path === '/status') return next();
  if (req.path.startsWith('/internal/agent/')) return next();
  if (req.path === '/agent/mindspace_page_patch') return next();
  if (req.path === '/config/blocked-words') return next();

  const plazaPublic = isPlazaPublicRead(req.path, req.method);

  if (userAuth && tkmindProxy) {
    if (req.userSession) {
      const me = await userAuth.getMe(req.userToken);
      if (me) req.currentUser = me;
    }
    if (plazaPublic) return next();
    if (!req.userSession) {
      return res.status(401).json({ message: '未授权，请重新登录' });
    }
    const me = await userAuth.getMe(req.userToken);
    if (!me) return res.status(401).json({ message: '登录已过期' });
    req.currentUser = me;
    return next();
  }

  if (legacyAuth?.verify(legacySessionToken(req))) return next();
  return res.status(401).json({ message: '未授权，请重新登录' });
});

attachAsrRoutes(api, { sendError, sendData });

api.get('/config/blocked-words', async (_req, res) => {
  await userAuthReady;
  if (!wordFilterService) return res.json({ words: [] });
  const words = await wordFilterService.listAllForFrontend();
  res.json({ words });
});

api.get('/status', async (_req, res, next) => {
  await userAuthReady;
  if (userAuth && tkmindProxy) {
    try {
      const upstream = await tkmindProxy.apiFetch('/status', { method: 'GET' });
      const text = await upstream.text();
      return res.status(upstream.status).send(text);
    } catch (err) {
      return res.status(502).json({ message: err instanceof Error ? err.message : '代理失败' });
    }
  }
  return next();
});

api.get('/mindspace/v1/space', async (req, res) => {
  if (!mindSpace || !ensureMindSpaceEnabled(res, req)) return;
  const space = await mindSpace.getSpace(req.currentUser.id);
  if (!space) return sendError(res, req, 404, 'resource_not_found', '用户空间不存在');
  return sendData(res, req, space);
});

api.get('/mindspace/v1/space/quota', async (req, res) => {
  if (!mindSpace) return res.status(503).json({ message: 'MindSpace 未启用' });
  const quota = await mindSpace.getQuota(req.currentUser.id);
  if (!quota) return res.status(404).json({ message: '用户空间不存在' });
  return res.json({ data: quota });
});

api.get('/mindspace/v1/space/categories', async (req, res) => {
  if (!mindSpace) return res.status(503).json({ message: 'MindSpace 未启用' });
  const categories = await mindSpace.listCategories(req.currentUser.id);
  if (!categories) return res.status(404).json({ message: '用户空间不存在' });
  return res.json({ data: categories });
});

api.get('/mindspace/v1/space/cleanup', async (req, res) => {
  if (!mindSpaceCleanup || !ensureMindSpaceEnabled(res, req)) return;
  try {
    const username = req.currentUser.username ?? req.currentUser.slug;
    const items = await mindSpaceCleanup.listCandidates(req.currentUser.id, username);
    const totalBytes = items.reduce((sum, item) => sum + item.sizeBytes, 0);
    return sendData(res, req, { items, totalBytes });
  } catch (error) {
    return mindSpaceError(res, req, error);
  }
});

api.post('/mindspace/v1/space/cleanup', async (req, res) => {
  if (!mindSpaceCleanup || !ensureMindSpaceEnabled(res, req)) return;
  try {
    const username = req.currentUser.username ?? req.currentUser.slug;
    const itemIds = Array.isArray(req.body?.item_ids) ? req.body.item_ids : [];
    const result = await mindSpaceCleanup.runCleanup(req.currentUser.id, username, itemIds);
    const quota = await mindSpace.getQuota(req.currentUser.id);
    await mindSpaceAudit?.write({
      userId: req.currentUser.id,
      action: 'space.cleanup',
      objectType: 'space',
      objectId: req.currentUser.id,
      ip: req.ip,
      detail: { removedCount: result.removedCount, freedBytes: result.freedBytes },
    });
    return sendData(res, req, { ...result, quota });
  } catch (error) {
    return mindSpaceError(res, req, error);
  }
});

function isPlazaPublicRead(path, method) {
  if (!path.startsWith('/plaza/v1/')) return false;
  if (method === 'POST' && path === '/plaza/v1/events') return true;
  if (method !== 'GET') return false;
  return (
    path === '/plaza/v1/feed' ||
    path === '/plaza/v1/categories' ||
    path === '/plaza/v1/seo/sitemap' ||
    /^\/plaza\/v1\/posts\/[^/]+$/.test(path) ||
    /^\/plaza\/v1\/posts\/[^/]+\/comments$/.test(path) ||
    /^\/plaza\/v1\/users\/[^/]+$/.test(path) ||
    /^\/plaza\/v1\/users\/[^/]+\/posts$/.test(path)
  );
}

const PLAZA_SID_COOKIE = 'plaza_sid';

function resolvePlazaSessionId(req, res) {
  const cookies = parseCookies(req.get('cookie'));
  let sessionId = cookies[PLAZA_SID_COOKIE];
  if (!sessionId) {
    sessionId = crypto.randomUUID();
    res.append(
      'Set-Cookie',
      `${PLAZA_SID_COOKIE}=${sessionId}; Path=/; Max-Age=31536000; SameSite=Lax; HttpOnly`,
    );
  }
  return sessionId;
}

function recordPlazaEventsAsync(req, res, events) {
  if (!plazaEvents || !Array.isArray(events) || events.length === 0) return;
  const sessionId = resolvePlazaSessionId(req, res);
  void plazaEvents
    .recordEvents({
      userId: req.currentUser?.id ?? null,
      sessionId,
      events,
    })
    .catch(() => {});
}

function reactionEventType(type) {
  if (type === 'like' || type === 'collect' || type === 'share') return type;
  return null;
}

function ensurePlazaInteractions(res, req) {
  if (!plazaInteractions) {
    sendError(res, req, 503, 'plaza_unavailable', 'Plaza 未启用');
    return false;
  }
  return true;
}

function plazaRouteError(res, req, error) {
  const status = mapPlazaError(error);
  const code = error?.code ?? 'internal_error';
  const message = error instanceof Error ? error.message : 'Plaza 请求失败';
  return sendError(res, req, status, code, message, error?.details);
}

function ensurePlazaEnabled(res, req) {
  if (!plazaPosts) {
    sendError(res, req, 503, 'plaza_unavailable', 'Plaza 未启用');
    return false;
  }
  return true;
}

function plazaClientIp(req) {
  const forwarded = req.headers['x-forwarded-for'];
  if (typeof forwarded === 'string' && forwarded.length > 0) {
    return forwarded.split(',')[0].trim();
  }
  return req.ip;
}


function mindSpaceError(res, req, error) {
  const statusByCode = {
    invalid_filename: 400,
    invalid_file_size: 400,
    category_not_uploadable: 400,
    file_size_mismatch: 409,
    invalid_upload_state: 409,
    file_too_large: 413,
    unsupported_file_type: 415,
    category_not_found: 404,
    upload_not_found: 404,
    asset_not_found: 404,
    asset_in_use: 409,
    page_not_found: 404,
    upload_expired: 410,
    quota_exceeded: 429,
    public_page_limit_exceeded: 429,
    space_unavailable: 423,
    invalid_page_input: 400,
    page_content_too_large: 413,
    source_message_not_found: 404,
    invalid_source_message: 409,
    version_conflict: 409,
    slug_conflict: 409,
    invalid_state_transition: 409,
    invalid_publish_input: 400,
    publication_not_found: 404,
    security_scan_required: 422,
    security_ack_required: 422,
    security_risk_blocked: 422,
    invalid_agent_job_input: 400,
    invalid_agent_job_output: 400,
    agent_job_not_found: 404,
    agent_job_token_invalid: 401,
    agent_job_expired: 410,
    feature_disabled: 503,
    cover_ai_unavailable: 503,
    llm_not_configured: 503,
    cover_ai_failed: 502,
    cover_ai_invalid_output: 422,
    thumbnail_not_supported: 422,
    thumbnail_image_required: 400,
    thumbnail_image_invalid: 400,
    category_not_pageable: 400,
    redaction_not_needed: 409,
    invalid_category_code: 400,
    invalid_page_path: 400,
    empty_page_content: 400,
    static_page_not_found: 404,
    preview_not_supported: 422,
  };
  const code = error?.code ?? 'internal_error';
  const status = statusByCode[code] ?? 500;
  const message =
    status === 500 && (!error?.code || code === 'internal_error')
      ? 'MindSpace 服务异常'
      : error?.message || 'MindSpace 服务异常';
  return sendError(res, req, status, code, message, error?.details);
}

function ensureMindSpaceEnabled(res, req, { upload = false, agent = false } = {}) {
  try {
    assertMindSpaceRoute(msFlags, upload ? 'upload' : agent ? 'agent' : undefined);
    return true;
  } catch (error) {
    mindSpaceError(res, req, error);
    return false;
  }
}

function bearerToken(req) {
  const header = req.get('authorization') ?? '';
  const [scheme, value] = header.split(/\s+/, 2);
  if (scheme?.toLowerCase() !== 'bearer' || !value) return null;
  return value.trim();
}

function requireInternalAgentSecret(req, res) {
  if (bearerToken(req) === INTERNAL_AGENT_SECRET) return true;
  sendError(res, req, 401, 'agent_job_token_invalid', '内部 Agent 凭据无效');
  return false;
}

api.post('/mindspace/v1/agent/jobs', async (req, res) => {
  if (!mindSpaceAgentJobs || !ensureMindSpaceEnabled(res, req, { agent: true })) return;
  try {
    const job = await mindSpaceAgentJobs.createJob(req.currentUser.id, {
      jobType: req.body?.job_type,
      instruction: req.body?.instruction,
      allowedAssetIds: req.body?.allowed_asset_ids,
      outputCategoryId: req.body?.output_category_id,
      outputType: req.body?.output_type,
      idempotencyKey: req.body?.idempotency_key,
      locale: req.body?.locale,
      timezone: req.body?.timezone,
      capabilities: req.body?.capabilities,
    });
    await mindSpaceAudit?.write({
      userId: req.currentUser.id,
      action: 'agent_access',
      objectType: 'agent_job',
      objectId: job.id,
      ip: req.ip,
      detail: {
        jobType: job.jobType,
        assetIds: job.assets.map((asset) => asset.assetId),
      },
    });
    return sendData(res, req, job, 201);
  } catch (error) {
    return mindSpaceError(res, req, error);
  }
});

api.get('/mindspace/v1/agent/jobs/:jobId', async (req, res) => {
  if (!mindSpaceAgentJobs || !ensureMindSpaceEnabled(res, req, { agent: true })) return;
  try {
    return sendData(res, req, await mindSpaceAgentJobs.getJob(req.currentUser.id, req.params.jobId));
  } catch (error) {
    return mindSpaceError(res, req, error);
  }
});

api.get('/mindspace/v1/agent/jobs', async (req, res) => {
  if (!mindSpaceAgentJobs || !ensureMindSpaceEnabled(res, req, { agent: true })) return;
  try {
    const result = await mindSpaceAgentJobs.listJobs(req.currentUser.id, {
      limit: Number(req.query.limit ?? 10),
      offset: Number(req.query.offset ?? 0),
    });
    return res.json({
      data: result.items,
      page: {
        total: result.total,
        offset: result.offset,
        limit: result.limit,
        has_more: result.hasMore,
      },
      request_id: req.requestId,
    });
  } catch (error) {
    return mindSpaceError(res, req, error);
  }
});

api.post('/mindspace/v1/agent/jobs/:jobId/cancel', async (req, res) => {
  if (!mindSpaceAgentJobs || !ensureMindSpaceEnabled(res, req, { agent: true })) return;
  try {
    return sendData(
      res,
      req,
      await mindSpaceAgentJobs.cancelJob(req.currentUser.id, req.params.jobId),
    );
  } catch (error) {
    return mindSpaceError(res, req, error);
  }
});

api.post('/mindspace/v1/agent/jobs/:jobId/retry', async (req, res) => {
  if (!mindSpaceAgentJobs || !ensureMindSpaceEnabled(res, req, { agent: true })) return;
  try {
    return sendData(
      res,
      req,
      await mindSpaceAgentJobs.retryJob(req.currentUser.id, req.params.jobId),
    );
  } catch (error) {
    return mindSpaceError(res, req, error);
  }
});

api.post('/mindspace/v1/agent/jobs/:jobId/run', async (req, res) => {
  if (
    !mindSpaceAgentJobs ||
    !mindSpaceAgentRunner ||
    !ensureMindSpaceEnabled(res, req, { agent: true })
  ) {
    return;
  }
  try {
    const job = await mindSpaceAgentJobs.getJob(req.currentUser.id, req.params.jobId);
    if (job.status !== 'queued') {
      return sendData(res, req, job);
    }
    void mindSpaceAgentRunner.runJob(req.params.jobId).catch((error) => {
      console.error('MindSpace agent job run failed:', error);
    });
    return sendData(res, req, { started: true, jobId: req.params.jobId }, 202);
  } catch (error) {
    return mindSpaceError(res, req, error);
  }
});

api.post('/internal/agent/jobs/:jobId/claim', async (req, res) => {
  if (!mindSpaceAgentJobs || !ensureMindSpaceEnabled(res, req, { agent: true })) return;
  if (!requireInternalAgentSecret(req, res)) return;
  try {
    return sendData(res, req, await mindSpaceAgentJobs.claimJob(req.params.jobId));
  } catch (error) {
    return mindSpaceError(res, req, error);
  }
});

api.get('/internal/agent/jobs/:jobId/assets/:assetId', async (req, res) => {
  if (!mindSpaceAgentJobs || !ensureMindSpaceEnabled(res, req, { agent: true })) return;
  try {
    const asset = await mindSpaceAgentJobs.getAssetForJob(
      req.params.jobId,
      bearerToken(req),
      req.params.assetId,
    );
    res.set('Content-Type', asset.mimeType);
    res.set('Content-Disposition', `inline; filename="${encodeURIComponent(asset.displayName)}"`);
    res.set('Cache-Control', 'private, no-store');
    res.setHeader('X-Request-Id', req.requestId);
    return res.send(await fs.promises.readFile(asset.path));
  } catch (error) {
    return mindSpaceError(res, req, error);
  }
});

api.post('/internal/agent/jobs/:jobId/heartbeat', async (req, res) => {
  if (!mindSpaceAgentJobs || !ensureMindSpaceEnabled(res, req, { agent: true })) return;
  try {
    return sendData(
      res,
      req,
      await mindSpaceAgentJobs.heartbeat(req.params.jobId, bearerToken(req), {
        stage: req.body?.stage,
        message: req.body?.message,
      }),
    );
  } catch (error) {
    return mindSpaceError(res, req, error);
  }
});

api.post('/internal/agent/jobs/:jobId/complete', async (req, res) => {
  if (!mindSpaceAgentJobs || !ensureMindSpaceEnabled(res, req, { agent: true })) return;
  try {
    const job = await mindSpaceAgentJobs.completeJob(req.params.jobId, bearerToken(req), {
      status: req.body?.status,
      errorCode: req.body?.error_code,
      errorMessage: req.body?.error_message,
      outputType: req.body?.output_type,
      title: req.body?.title,
      summary: req.body?.summary,
      content: req.body?.content,
      contentFormat: req.body?.content_format,
      pageType: req.body?.page_type,
      templateId: req.body?.template_id,
      sourceAssetIds: req.body?.source_asset_ids,
    });
    return sendData(res, req, job);
  } catch (error) {
    return mindSpaceError(res, req, error);
  }
});

api.post('/mindspace/v1/uploads', async (req, res) => {
  if (!mindSpaceAssets || !ensureMindSpaceEnabled(res, req, { upload: true })) return;
  try {
    const upload = await mindSpaceAssets.createUpload(req.currentUser.id, {
      categoryId: req.body?.category_id,
      filename: req.body?.filename,
      sizeBytes: req.body?.size_bytes,
      declaredMimeType: req.body?.declared_mime_type,
    });
    return sendData(res, req, upload, 201);
  } catch (error) {
    return mindSpaceError(res, req, error);
  }
});

api.put('/mindspace/v1/uploads/:uploadId/content', rawUploadBody, async (req, res) => {
  if (!mindSpaceAssets) return res.status(503).json({ message: 'MindSpace 未启用' });
  try {
    const result = await mindSpaceAssets.writeUploadContent(
      req.currentUser.id,
      req.params.uploadId,
      req.body,
    );
    return res.json({ data: result });
  } catch (error) {
    return mindSpaceError(res, req, error);
  }
});

api.post('/mindspace/v1/uploads/:uploadId/complete', async (req, res) => {
  if (!mindSpaceAssets) return res.status(503).json({ message: 'MindSpace 未启用' });
  try {
    const asset = await mindSpaceAssets.completeUpload(
      req.currentUser.id,
      req.params.uploadId,
    );
    return res.status(201).json({ data: asset });
  } catch (error) {
    return mindSpaceError(res, req, error);
  }
});

api.delete('/mindspace/v1/uploads/:uploadId', async (req, res) => {
  if (!mindSpaceAssets) return res.status(503).json({ message: 'MindSpace 未启用' });
  try {
    const result = await mindSpaceAssets.cancelUpload(
      req.currentUser.id,
      req.params.uploadId,
    );
    return res.json({ data: result });
  } catch (error) {
    return mindSpaceError(res, req, error);
  }
});

api.get('/mindspace/v1/assets', async (req, res) => {
  if (!mindSpaceAssets) return res.status(503).json({ message: 'MindSpace 未启用' });
  try {
    const assets = await mindSpaceAssets.listAssets(req.currentUser.id, {
      categoryId: typeof req.query.category_id === 'string' ? req.query.category_id : undefined,
      categoryCode:
        typeof req.query.category_code === 'string' ? req.query.category_code : undefined,
    });
    return res.json({ data: assets, page: { next_cursor: null, has_more: false } });
  } catch (error) {
    return mindSpaceError(res, req, error);
  }
});

api.get('/mindspace/v1/assets/:assetId/download', async (req, res) => {
  if (!mindSpaceAssets || !ensureMindSpaceEnabled(res, req)) return;
  try {
    const { asset, path: assetPath } = await mindSpaceAssets.readAsset(
      req.currentUser.id,
      req.params.assetId,
    );
    await mindSpaceAudit?.write({
      userId: req.currentUser.id,
      action: 'asset.download',
      objectType: 'asset',
      objectId: req.params.assetId,
      ip: req.ip,
      riskLevel: asset.riskLevel,
    });
    res.type(asset.mimeType);
    const inline =
      req.query.inline === '1' ||
      req.query.disposition === 'inline' ||
      req.get('sec-fetch-dest') === 'iframe';
    if (inline && asset.mimeType.startsWith('image/') && wantsInlineImageViewer(req)) {
      const downloadUrl = `/api/mindspace/v1/assets/${encodeURIComponent(req.params.assetId)}/download?inline=1`;
      const html = renderImageAssetViewerHtml({ asset, downloadUrl });
      res.set('Content-Type', 'text/html; charset=utf-8');
      res.set('Cache-Control', 'private, no-store');
      res.setHeader('X-Request-Id', req.requestId);
      return res.send(html);
    }
    res.set(
      'Content-Disposition',
      inline
        ? `inline; filename*=UTF-8''${encodeURIComponent(asset.filename)}`
        : `attachment; filename*=UTF-8''${encodeURIComponent(asset.filename)}`,
    );
    res.setHeader('X-Request-Id', req.requestId);
    return res.sendFile(assetPath);
  } catch (error) {
    await mindSpaceAudit?.write({
      userId: req.currentUser.id,
      action: 'asset.download',
      objectType: 'asset',
      objectId: req.params.assetId,
      ip: req.ip,
      result: 'denied',
      riskLevel: error?.code === 'security_risk_blocked' ? 'high' : null,
    });
    return mindSpaceError(res, req, error);
  }
});

api.get('/mindspace/v1/assets/:assetId/thumbnail', async (req, res) => {
  if (!mindSpaceAssets || !ensureMindSpaceEnabled(res, req)) return;
  try {
    const svg = await mindSpaceAssets.renderAssetThumbnail(req.currentUser.id, req.params.assetId);
    res.set('Content-Type', 'image/svg+xml; charset=utf-8');
    res.set('Cache-Control', 'private, max-age=300');
    res.setHeader('X-Request-Id', req.requestId);
    return res.send(svg);
  } catch (error) {
    return mindSpaceError(res, req, error);
  }
});

api.get('/mindspace/v1/assets/:assetId/preview', async (req, res) => {
  if (!mindSpaceAssets || !ensureMindSpaceEnabled(res, req)) return;
  try {
    const html = await mindSpaceAssets.renderAssetPreview(req.currentUser.id, req.params.assetId);
    res.set('Content-Type', 'text/html; charset=utf-8');
    res.set('Cache-Control', 'private, no-store');
    res.setHeader('X-Request-Id', req.requestId);
    return res.send(html);
  } catch (error) {
    return mindSpaceError(res, req, error);
  }
});

api.post('/mindspace/v1/pages/from-asset', async (req, res) => {
  if (!mindSpacePages || !mindSpaceAssets) return res.status(503).json({ message: 'MindSpace 未启用' });
  try {
    const assetId = req.body?.asset_id;
    const existingPage = await mindSpacePages.findPageBySourceAsset(req.currentUser.id, assetId);
    if (existingPage) {
      return sendData(res, req, { kind: 'page', categoryCode: existingPage.categoryCode ?? 'draft', page: existingPage });
    }
    const { asset, path: assetPath } = await mindSpaceAssets.readAsset(
      req.currentUser.id,
      assetId,
    );
    const content = await fs.promises.readFile(assetPath, 'utf8');
    const contentFormat = asset.mimeType === 'text/html' ? 'html' : 'markdown';
    const page = await mindSpacePages.createFromChat(
      req.currentUser.id,
      {
        title: req.body?.title || asset.displayName,
        summary: req.body?.summary,
        content,
        contentFormat,
        templateId: req.body?.template_id,
        categoryCode: 'draft',
      },
      {
        assetId: asset.id,
        snapshot: {
          source_asset_id: asset.id,
          source_category: asset.categoryCode,
          content_mode: contentFormat,
        },
      },
    );
    return res.status(201).json({ data: { kind: 'page', categoryCode: 'draft', page } });
  } catch (error) {
    return mindSpaceError(res, req, error);
  }
});

api.delete('/mindspace/v1/assets/:assetId', async (req, res) => {
  if (!mindSpaceAssets || !ensureMindSpaceEnabled(res, req)) return;
  try {
    const result = await mindSpaceAssets.deleteAsset(req.currentUser.id, req.params.assetId);
    await mindSpaceAudit?.write({
      userId: req.currentUser.id,
      action: 'asset.delete',
      objectType: 'asset',
      objectId: req.params.assetId,
      ip: req.ip,
    });
    return sendData(res, req, result);
  } catch (error) {
    return mindSpaceError(res, req, error);
  }
});

function messageText(message) {
  return (message?.content ?? [])
    .filter((item) => item?.type === 'text' && typeof item.text === 'string')
    .map((item) => item.text)
    .join('')
    .trim();
}

async function resolveOwnedAssistantMessage(userId, sessionId, messageId) {
  if (!sessionId || !messageId) {
    throw Object.assign(new Error('缺少来源会话或消息'), {
      code: 'invalid_page_input',
    });
  }
  if (!(await userAuth.ownsSession(userId, sessionId))) {
    throw Object.assign(new Error('来源会话不存在'), { code: 'source_message_not_found' });
  }
  const upstream = await tkmindProxy.apiFetch(`/sessions/${encodeURIComponent(sessionId)}`, {
    method: 'GET',
  });
  if (!upstream.ok) {
    throw Object.assign(new Error('无法读取来源会话'), { code: 'source_message_not_found' });
  }
  const session = await upstream.json();
  const message = (session.conversation ?? []).find((item) => item.id === messageId);
  if (!message) {
    throw Object.assign(new Error('来源消息不存在'), { code: 'source_message_not_found' });
  }
  const content = messageText(message);
  if (message.role !== 'assistant' || !message.metadata?.userVisible || !content) {
    throw Object.assign(new Error('只有可见的 AI 文本消息可以保存为页面'), {
      code: 'invalid_source_message',
    });
  }
  return { session, message, content };
}

const SAVE_TARGET_CATEGORIES = new Set(['draft', 'oa', 'private', 'public']);

function assertPrivateSaveAllowed(categoryCode, privacyScan, acknowledgedFindingIds) {
  if (categoryCode !== 'private') return;
  if (!privacyScan.allowed) {
    throw Object.assign(new Error('内容含阻断级敏感信息，不能保存到私人区'), {
      code: 'security_risk_blocked',
      details: { findings: privacyScan.findings },
    });
  }
  if (privacyScan.findings.length === 0) return;
  const acknowledged = new Set((acknowledgedFindingIds ?? []).map(String));
  const missing = privacyScan.findings.filter((finding) => !acknowledged.has(finding.id));
  if (missing.length > 0) {
    throw Object.assign(new Error('保存到私人区前需确认敏感信息提示'), {
      code: 'private_ack_required',
      details: { findings: missing },
    });
  }
}

async function resolveChatSaveBundle(user, h5Root, input = {}) {
  const sessionId = input.sessionId ?? input.session_id;
  const messageId = input.messageId ?? input.message_id;
  const selectedLinkIndex = Number(input.selectedLinkIndex ?? input.selected_link_index ?? 0);
  const previewTitle = String(input.previewTitle ?? input.preview_title ?? '').trim();
  const previewSummary = String(input.previewSummary ?? input.preview_summary ?? '').trim();

  const source = await resolveOwnedAssistantMessage(user.id, sessionId, messageId);
  const { analysis, resolvedHtml } = await resolveChatSaveAnalysis({
    content: source.content,
    userId: user.id,
    username: user.username,
    h5Root,
    selectedLinkIndex,
  });
  return {
    source,
    analysis,
    resolvedHtml,
    selectedLinkIndex,
    previewTitle,
    previewSummary,
    previewFrameUrl:
      resolvedHtml != null
        ? buildChatSavePreviewFrameUrl({ sessionId, messageId, selectedLinkIndex })
        : null,
    thumbnailUrl:
      resolvedHtml != null
        ? buildChatSaveThumbnailUrl({
            sessionId,
            messageId,
            selectedLinkIndex,
            previewTitle,
            previewSummary,
          })
        : null,
    localPreviewUrl:
      resolvedHtml?.relativePath != null
        ? buildWorkspaceAssetUrl(user.id, resolvedHtml.relativePath)
        : null,
    localThumbnailUrl:
      resolvedHtml?.relativePath != null
        ? buildWorkspaceThumbnailUrl(user.id, resolvedHtml.relativePath)
        : null,
  };
}

api.get('/mindspace/v1/pages/chat-save-preview', async (req, res) => {
  if (!mindSpacePages) return res.status(503).json({ message: 'MindSpace 未启用' });
  try {
    const bundle = await resolveChatSaveBundle(req.currentUser, __dirname, req.query);
    if (!bundle.resolvedHtml) {
      throw Object.assign(new Error('无法读取链接页面内容'), { code: 'static_page_not_found' });
    }
    const baseHref = buildWorkspaceBaseHref(req.currentUser.id, bundle.resolvedHtml.relativePath);
    const html = injectHtmlBaseHref(bundle.resolvedHtml.content, baseHref);
    res.set('Content-Type', 'text/html; charset=utf-8');
    res.set('Cache-Control', 'private, no-store');
    res.setHeader('X-Request-Id', req.requestId);
    return res.send(html);
  } catch (error) {
    return mindSpaceError(res, req, error);
  }
});

api.get('/mindspace/v1/pages/chat-save-thumbnail', async (req, res) => {
  if (!mindSpacePages) return res.status(503).json({ message: 'MindSpace 未启用' });
  try {
    const bundle = await resolveChatSaveBundle(req.currentUser, __dirname, req.query);
    if (!bundle.resolvedHtml) {
      throw Object.assign(new Error('无法读取链接页面内容'), { code: 'static_page_not_found' });
    }
    const publishDir = resolvePublishDir(__dirname, req.currentUser);
    const thumbRel = workspaceThumbnailRelativePath(bundle.resolvedHtml.relativePath);
    const title =
      bundle.previewTitle ||
      bundle.resolvedHtml.suggestedTitle ||
      bundle.analysis.suggestedTitle;
    const subtitle =
      bundle.previewSummary ||
      bundle.resolvedHtml.suggestedSummary ||
      bundle.analysis.suggestedSummary;
    await ensureWorkspaceHtmlThumbnail(
      publishDir,
      bundle.resolvedHtml.relativePath,
      bundle.resolvedHtml.content,
      { title, subtitle, force: true },
    ).catch(() => {});
    const svg = await generateHtmlThumbnail(publishDir, thumbRel, bundle.resolvedHtml.content, {
      title,
      subtitle,
      contentBaseDir: path.dirname(bundle.resolvedHtml.absolute),
      force: true,
    });
    res.set('Content-Type', 'image/svg+xml; charset=utf-8');
    res.set('Cache-Control', 'private, no-store');
    res.setHeader('X-Request-Id', req.requestId);
    return res.send(svg);
  } catch (error) {
    return mindSpaceError(res, req, error);
  }
});

api.post('/mindspace/v1/pages/analyze-chat-save', async (req, res) => {
  if (!mindSpacePages) return res.status(503).json({ message: 'MindSpace 未启用' });
  try {
    const bundle = await resolveChatSaveBundle(req.currentUser, __dirname, req.body);
    const {
      source,
      analysis,
      resolvedHtml,
      previewTitle,
      previewSummary,
      previewFrameUrl,
      thumbnailUrl,
      localPreviewUrl,
      localThumbnailUrl,
    } = bundle;
    let thumbnailReady = false;
    if (resolvedHtml?.content && resolvedHtml.relativePath) {
      const publishDir = resolvePublishDir(__dirname, req.currentUser);
      try {
        await ensureWorkspaceHtmlThumbnail(publishDir, resolvedHtml.relativePath, resolvedHtml.content, {
          title: previewTitle || resolvedHtml.suggestedTitle,
          subtitle: previewSummary || resolvedHtml.suggestedSummary,
          force: Boolean(previewTitle || previewSummary),
        });
        thumbnailReady = true;
      } catch {
        thumbnailReady = false;
      }
    }
    return sendData(res, req, {
      contentMode: analysis.contentMode,
      links: analysis.links,
      selectedLinkIndex: analysis.selectedLink
        ? analysis.links.findIndex((link) => link.publicUrl === analysis.selectedLink.publicUrl)
        : -1,
      suggestedTitle: resolvedHtml?.suggestedTitle ?? analysis.suggestedTitle,
      suggestedSummary: resolvedHtml?.suggestedSummary ?? analysis.suggestedSummary,
      previewUrl: analysis.previewUrl,
      previewFrameUrl,
      localPreviewUrl,
      localThumbnailUrl,
      thumbnailUrl: resolvedHtml ? thumbnailUrl : null,
      thumbnailReady,
      relativePath: resolvedHtml?.relativePath ?? analysis.relativePath,
      filename: resolvedHtml?.filename ?? analysis.filename,
      hasHtmlContent: Boolean(resolvedHtml),
      privacyScan: scanContent(resolvedHtml?.content ?? source.content),
    });
  } catch (error) {
    return mindSpaceError(res, req, error);
  }
});

api.post('/mindspace/v1/pages/save-from-chat', async (req, res) => {
  if (!mindSpacePages || !mindSpaceAssets) {
    return res.status(503).json({ message: 'MindSpace 未启用' });
  }
  try {
    const categoryCode = String(req.body?.category_code ?? 'draft');
    if (!SAVE_TARGET_CATEGORIES.has(categoryCode)) {
      throw Object.assign(new Error('无效的保存目标'), { code: 'invalid_category_code' });
    }
    const source = await resolveOwnedAssistantMessage(
      req.currentUser.id,
      req.body?.session_id,
      req.body?.message_id,
    );
    const analysis = analyzeChatMessageForSave({
      content: source.content,
      userId: req.currentUser.id,
      username: req.currentUser.username,
      h5Root: __dirname,
      selectedLinkIndex: Number(req.body?.selected_link_index ?? 0),
    });
    const snapshot = {
      session_name: source.session.name,
      message_created: source.message.created,
      role: source.message.role,
      content_mode: analysis.contentMode,
      public_url: analysis.previewUrl,
      relative_path: analysis.relativePath,
    };

    let resolvedHtml = null;
    if (analysis.contentMode === 'static_html') {
      resolvedHtml = await resolveStaticHtmlContent(analysis).catch(() => null);
    }
    const privacyScan = scanContent(resolvedHtml?.content ?? source.content);
    assertPrivateSaveAllowed(categoryCode, privacyScan, req.body?.acknowledged_finding_ids);

    if (categoryCode !== 'draft') {
      let buffer;
      let filename;
      let displayName = req.body?.title;
      if (analysis.contentMode === 'static_html') {
        if (!resolvedHtml) {
          throw Object.assign(new Error('无法读取链接页面内容'), { code: 'static_page_not_found' });
        }
        buffer = Buffer.from(resolvedHtml.content, 'utf8');
        filename = resolvedHtml.filename;
        displayName = displayName || resolvedHtml.suggestedTitle;
      } else {
        buffer = Buffer.from(source.content, 'utf8');
        filename = `${String(displayName || 'chat-export')
          .replace(/[^\w\u4e00-\u9fff-]+/g, '-')
          .slice(0, 48) || 'chat-export'}.md`;
      }
      const asset = await mindSpaceAssets.createChatAsset(req.currentUser.id, {
        categoryCode,
        buffer,
        filename,
        displayName,
        sourceType: 'chat',
      });
      return res.status(201).json({
        data: {
          kind: 'asset',
          categoryCode,
          asset,
        },
      });
    }

    let pageInput = {
      title: req.body?.title,
      summary: req.body?.summary,
      templateId: req.body?.template_id,
      pageType: req.body?.page_type,
      categoryCode: 'draft',
    };
    if (analysis.contentMode === 'static_html') {
      if (!resolvedHtml) {
        throw Object.assign(new Error('无法读取链接页面内容'), { code: 'static_page_not_found' });
      }
      pageInput = {
        ...pageInput,
        title: req.body?.title || resolvedHtml.suggestedTitle,
        summary: req.body?.summary || resolvedHtml.suggestedSummary,
        content: resolvedHtml.content,
        contentFormat: 'html',
        pageType: 'html',
      };
    } else {
      pageInput = {
        ...pageInput,
        content: source.content,
        contentFormat: 'markdown',
      };
    }

    if (analysis.contentMode === 'static_html' && resolvedHtml?.content && analysis.relativePath) {
      const publishDir = resolvePublishDir(__dirname, req.currentUser);
      await ensureWorkspaceHtmlThumbnail(publishDir, analysis.relativePath, resolvedHtml.content, {
        title: pageInput.title,
        subtitle: pageInput.summary,
      }).catch(() => {});
    }

    const page = await mindSpacePages.createFromChat(req.currentUser.id, pageInput, {
      sessionId: req.body.session_id,
      messageId: req.body.message_id,
      snapshot,
    });
    return res.status(201).json({ data: { kind: 'page', categoryCode: 'draft', page } });
  } catch (error) {
    return mindSpaceError(res, req, error);
  }
});

api.post('/mindspace/v1/pages', async (req, res) => {
  if (!mindSpacePages) return res.status(503).json({ message: 'MindSpace 未启用' });
  try {
    const page = await mindSpacePages.createPage(req.currentUser.id, {
      title: req.body?.title,
      summary: req.body?.summary,
      content: req.body?.content,
      contentFormat: req.body?.content_format === 'html' ? 'html' : undefined,
      templateId: req.body?.template_id,
      pageType: req.body?.page_type,
      categoryCode: req.body?.category_code,
    });
    return res.status(201).json({ data: page });
  } catch (error) {
    return mindSpaceError(res, req, error);
  }
});

api.get('/mindspace/v1/pages', async (req, res) => {
  if (!mindSpacePages) return res.status(503).json({ message: 'MindSpace 未启用' });
  try {
    const pages = await mindSpacePages.listPages(req.currentUser.id, {
      status: typeof req.query.status === 'string' ? req.query.status : undefined,
    });
    return res.json({ data: pages, page: { next_cursor: null, has_more: false } });
  } catch (error) {
    return mindSpaceError(res, req, error);
  }
});

api.get('/mindspace/v1/pages/:pageId', async (req, res) => {
  if (!mindSpacePages) return res.status(503).json({ message: 'MindSpace 未启用' });
  try {
    const [page, versions, publication] = await Promise.all([
      mindSpacePages.getPage(req.currentUser.id, req.params.pageId),
      mindSpacePages.listVersions(req.currentUser.id, req.params.pageId),
      mindSpacePublications?.getCurrent(req.currentUser.id, req.params.pageId) ?? null,
    ]);
    return res.json({ data: { ...page, versions, publication } });
  } catch (error) {
    return mindSpaceError(res, req, error);
  }
});

api.delete('/mindspace/v1/pages/:pageId', async (req, res) => {
  if (!mindSpacePages || !ensureMindSpaceEnabled(res, req)) return;
  try {
    const result = await mindSpacePages.deletePage(req.currentUser.id, req.params.pageId);
    const quota = await mindSpace.getQuota(req.currentUser.id);
    await mindSpaceAudit?.write({
      userId: req.currentUser.id,
      action: 'page.delete',
      objectType: 'page',
      objectId: req.params.pageId,
      ip: req.ip,
      detail: {
        offlinedPublicationCount: result.offlinedPublicationCount,
        deletedAssetCount: result.deletedAssetCount,
        freedBytes: result.freedBytes,
      },
    });
    return sendData(res, req, { ...result, quota });
  } catch (error) {
    return mindSpaceError(res, req, error);
  }
});

api.get('/mindspace/v1/pages/:pageId/delete-preview', async (req, res) => {
  if (!mindSpacePages || !ensureMindSpaceEnabled(res, req)) return;
  try {
    return sendData(res, req, await mindSpacePages.getDeletePreview(req.currentUser.id, req.params.pageId));
  } catch (error) {
    return mindSpaceError(res, req, error);
  }
});

api.put('/mindspace/v1/pages/:pageId', async (req, res) => {
  if (!mindSpacePages) return res.status(503).json({ message: 'MindSpace 未启用' });
  try {
    const page = await mindSpacePages.updatePage(req.currentUser.id, req.params.pageId, {
      expectedVersion: req.body?.expected_version,
      title: req.body?.title,
      summary: req.body?.summary,
      content: req.body?.content,
      templateId: req.body?.template_id,
      pageType: req.body?.page_type,
      changeNote: req.body?.change_note,
    });
    return res.json({ data: page });
  } catch (error) {
    return mindSpaceError(res, req, error);
  }
});

api.post('/mindspace/v1/pages/:pageId/live-edit/bind', async (req, res) => {
  if (!mindSpacePageLiveEdit || !mindSpacePages) {
    return res.status(503).json({ message: 'MindSpace 未启用' });
  }
  try {
    const sessionId = String(req.body?.session_id ?? '').trim();
    if (!sessionId) {
      return sendError(res, req, 400, 'invalid_request', '缺少 session_id');
    }
    const owns = await userAuth.ownsSession(req.currentUser.id, sessionId);
    if (!owns) {
      return sendError(res, req, 403, 'forbidden', '无权绑定该 Agent 会话');
    }
    await mindSpacePages.getPage(req.currentUser.id, req.params.pageId);
    const parentSessionId = String(req.body?.parent_session_id ?? '').trim() || null;
    return sendData(
      res,
      req,
      mindSpacePageLiveEdit.bindSession({
        userId: req.currentUser.id,
        sessionId,
        pageId: req.params.pageId,
        parentSessionId,
      }),
    );
  } catch (error) {
    return mindSpaceError(res, req, error);
  }
});

api.post('/mindspace/v1/pages/:pageId/live-edit/fork-session', async (req, res) => {
  if (!mindSpacePageEditSession || !mindSpacePages) {
    return res.status(503).json({ message: 'MindSpace 未启用' });
  }
  try {
    const parentSessionId = String(req.body?.parent_session_id ?? '').trim();
    if (!parentSessionId) {
      return sendError(res, req, 400, 'invalid_request', '缺少 parent_session_id');
    }
    const gate = await userAuth.canUseChat(req.currentUser.id);
    if (!gate.ok) {
      return sendError(res, req, 402, gate.code ?? 'insufficient_balance', gate.message);
    }
    return sendData(
      res,
      req,
      await mindSpacePageEditSession.forkSession({
        userId: req.currentUser.id,
        pageId: req.params.pageId,
        parentSessionId,
        h5ApiBase: String(req.body?.h5_api_base ?? req.body?.h5ApiBase ?? '').trim() || null,
      }),
    );
  } catch (error) {
    if (error?.code === 'forbidden') {
      return sendError(res, req, 403, error.code, error.message);
    }
    if (error?.code === 'invalid_request') {
      return sendError(res, req, 400, error.code, error.message);
    }
    return mindSpaceError(res, req, error);
  }
});

api.post('/mindspace/v1/pages/:pageId/live-edit/close-session', async (req, res) => {
  if (!mindSpacePageEditSession || !mindSpacePages) {
    return res.status(503).json({ message: 'MindSpace 未启用' });
  }
  try {
    const sessionId = String(req.body?.session_id ?? '').trim();
    if (!sessionId) {
      return sendError(res, req, 400, 'invalid_request', '缺少 session_id');
    }
    return sendData(
      res,
      req,
      await mindSpacePageEditSession.closeSession({
        userId: req.currentUser.id,
        pageId: req.params.pageId,
        sessionId,
        parentSessionId: String(req.body?.parent_session_id ?? '').trim() || null,
        summary: String(req.body?.summary ?? ''),
      }),
    );
  } catch (error) {
    if (error?.code === 'forbidden' || error?.code === 'page_binding_mismatch') {
      return sendError(res, req, 403, error.code, error.message);
    }
    if (error?.code === 'invalid_request') {
      return sendError(res, req, 400, error.code, error.message);
    }
    return mindSpaceError(res, req, error);
  }
});

api.get('/mindspace/v1/pages/:pageId/live-edit/revision', async (req, res) => {
  if (!mindSpacePageLiveEdit || !mindSpacePages) {
    return res.status(503).json({ message: 'MindSpace 未启用' });
  }
  try {
    return sendData(
      res,
      req,
      await mindSpacePageLiveEdit.getRevisionSnapshot(req.currentUser.id, req.params.pageId),
    );
  } catch (error) {
    return mindSpaceError(res, req, error);
  }
});

api.post('/agent/mindspace_page_patch', async (req, res) => {
  if (!mindSpacePageLiveEdit) {
    return res.status(503).json({ message: 'MindSpace 未启用' });
  }
  try {
    const result = await mindSpacePageLiveEdit.applyAgentPatch(req.body ?? {});
    return res.json({
      data: {
        pageId: result.page.id,
        title: result.page.title,
        summary: result.page.summary,
        versionNo: result.page.versionNo,
        updatedAt: result.page.updatedAt,
        liveRevision: result.liveRevision,
      },
    });
  } catch (error) {
    if (error?.code === 'forbidden' || error?.code === 'page_binding_mismatch') {
      return sendError(res, req, 403, error.code, error.message);
    }
    if (error?.code === 'invalid_request') {
      return sendError(res, req, 400, error.code, error.message);
    }
    return mindSpaceError(res, req, error);
  }
});

api.get('/mindspace/v1/pages/:pageId/thumbnail', async (req, res) => {
  if (!mindSpacePages) return res.status(503).json({ message: 'MindSpace 未启用' });
  try {
    const svg = await mindSpacePages.renderThumbnail(req.currentUser.id, req.params.pageId);
    res.set('Content-Type', 'image/svg+xml; charset=utf-8');
    res.set('Cache-Control', 'private, max-age=300');
    res.setHeader('X-Request-Id', req.requestId);
    return res.send(svg);
  } catch (error) {
    return mindSpaceError(res, req, error);
  }
});

api.post('/mindspace/v1/pages/:pageId/thumbnail/upload', async (req, res) => {
  if (!mindSpacePages) return res.status(503).json({ message: 'MindSpace 未启用' });
  try {
    const result = await mindSpacePages.uploadThumbnail(req.currentUser.id, req.params.pageId, {
      imageBase64: req.body?.image_base64,
      mimeType: req.body?.mime_type,
      title: req.body?.title,
      summary: req.body?.summary,
      html: req.body?.html,
    });
    return sendData(res, req, result);
  } catch (error) {
    return mindSpaceError(res, req, error);
  }
});

api.post('/mindspace/v1/pages/:pageId/thumbnail/regenerate', async (req, res) => {
  if (!mindSpacePages) return res.status(503).json({ message: 'MindSpace 未启用' });
  try {
    const result = await mindSpacePages.regenerateThumbnail(
      req.currentUser.id,
      req.params.pageId,
      {
        html: req.body?.html,
        title: req.body?.title,
        summary: req.body?.summary,
        useAi: Boolean(req.body?.use_ai),
        instruction: req.body?.instruction,
      },
      {
        suggestCoverMeta: req.body?.use_ai
          ? (input) => {
              if (!authPool) {
                throw Object.assign(new Error('LLM 服务未就绪'), { code: 'llm_not_configured' });
              }
              return suggestCoverMetaWithAi(authPool, {
                ...input,
                encryptionKey:
                  process.env.H5_SETTINGS_ENCRYPTION_KEY ?? process.env.TKMIND_SERVER__SECRET_KEY,
              });
            }
          : null,
      },
    );
    return sendData(res, req, result);
  } catch (error) {
    return mindSpaceError(res, req, error);
  }
});

api.get('/mindspace/v1/pages/:pageId/preview', async (req, res) => {
  if (!mindSpacePages) return res.status(503).json({ message: 'MindSpace 未启用' });
  try {
    const { html, contentFormat } = await mindSpacePages.renderPreview(
      req.currentUser.id,
      req.params.pageId,
    );
    res.set('Content-Type', 'text/html; charset=utf-8');
    res.set(
      'Content-Security-Policy',
      pageInternals.previewContentSecurityPolicy(contentFormat),
    );
    res.set('Cache-Control', 'private, no-store');
    res.set('X-Content-Type-Options', 'nosniff');
    return res.send(html);
  } catch (error) {
    return mindSpaceError(res, req, error);
  }
});

api.post('/mindspace/v1/pages/:pageId/preview-draft', async (req, res) => {
  if (!mindSpacePages) return res.status(503).json({ message: 'MindSpace 未启用' });
  try {
    const { html, contentFormat } = await mindSpacePages.renderDraftPreview(
      req.currentUser.id,
      req.params.pageId,
      {
        title: req.body?.title,
        summary: req.body?.summary,
        content: req.body?.content,
        templateId: req.body?.template_id,
      },
    );
    res.set('Content-Type', 'text/html; charset=utf-8');
    res.set(
      'Content-Security-Policy',
      pageInternals.previewContentSecurityPolicy(contentFormat),
    );
    res.set('Cache-Control', 'private, no-store');
    res.set('X-Content-Type-Options', 'nosniff');
    return res.send(html);
  } catch (error) {
    return mindSpaceError(res, req, error);
  }
});

api.post('/mindspace/v1/pages/:pageId/publish-check', async (req, res) => {
  if (!mindSpacePublications) return res.status(503).json({ message: 'MindSpace 未启用' });
  try {
    const result = await mindSpacePublications.check(req.currentUser.id, req.params.pageId, {
      pageVersionId: req.body?.page_version_id,
      accessMode: req.body?.access_mode,
      urlSlug: req.body?.url_slug,
      password: req.body?.password,
      expiresAt: req.body?.expires_at,
    });
    return sendData(res, req, result);
  } catch (error) {
    return mindSpaceError(res, req, error);
  }
});

api.post('/mindspace/v1/pages/:pageId/redact', async (req, res) => {
  if (!mindSpacePages) return res.status(503).json({ message: 'MindSpace 未启用' });
  try {
    const result = await mindSpacePages.redactPage(req.currentUser.id, req.params.pageId, {
      pageVersionId: req.body?.page_version_id,
      expectedVersion: req.body?.expected_version,
      title: req.body?.title,
      summary: req.body?.summary,
      content: req.body?.content,
    });
    await mindSpaceAudit?.write({
      userId: req.currentUser.id,
      action: 'page.redact',
      objectType: 'page',
      objectId: result.page.id,
      ip: req.ip,
      riskLevel: result.originalScan.riskLevel,
    });
    return sendData(res, req, result);
  } catch (error) {
    return mindSpaceError(res, req, error);
  }
});

api.post('/mindspace/v1/pages/:pageId/publish-fix', async (req, res) => {
  if (!mindSpacePages) return res.status(503).json({ message: 'MindSpace 未启用' });
  try {
    const result = await mindSpacePages.localizePrivateResources(req.currentUser.id, req.params.pageId, {
      pageVersionId: req.body?.page_version_id,
      expectedVersion: req.body?.expected_version,
      title: req.body?.title,
      summary: req.body?.summary,
      content: req.body?.content,
    });
    await mindSpaceAudit?.write({
      userId: req.currentUser.id,
      action: 'page.publish_fix',
      objectType: 'page',
      objectId: result.page.id,
      ip: req.ip,
      riskLevel: result.originalScan.riskLevel,
    });
    return sendData(res, req, result);
  } catch (error) {
    return mindSpaceError(res, req, error);
  }
});

api.post('/mindspace/v1/pages/:pageId/redacted-copy', async (req, res) => {
  if (!mindSpacePages) return res.status(503).json({ message: 'MindSpace 未启用' });
  try {
    const result = await mindSpacePages.redactPage(req.currentUser.id, req.params.pageId, {
      pageVersionId: req.body?.page_version_id,
      expectedVersion: req.body?.expected_version,
      title: req.body?.title,
      summary: req.body?.summary,
      content: req.body?.content,
    });
    await mindSpaceAudit?.write({
      userId: req.currentUser.id,
      action: 'page.redact',
      objectType: 'page',
      objectId: result.page.id,
      ip: req.ip,
      riskLevel: result.originalScan.riskLevel,
    });
    return sendData(res, req, result);
  } catch (error) {
    return mindSpaceError(res, req, error);
  }
});

api.post('/mindspace/v1/pages/:pageId/publish', async (req, res) => {
  if (!mindSpacePublications) return res.status(503).json({ message: 'MindSpace 未启用' });
  try {
    const publication = await mindSpacePublications.publish(
      req.currentUser.id,
      req.params.pageId,
      {
        pageVersionId: req.body?.page_version_id,
        accessMode: req.body?.access_mode,
        urlSlug: req.body?.url_slug,
        password: req.body?.password,
        expiresAt: req.body?.expires_at,
        acknowledgedFindingIds: req.body?.acknowledged_finding_ids,
      },
    );
    await mindSpaceAudit?.write({
      userId: req.currentUser.id,
      action: 'page.publish',
      objectType: 'publication',
      objectId: publication.id,
      ip: req.ip,
    });
    return sendData(res, req, publication, 201);
  } catch (error) {
    return mindSpaceError(res, req, error);
  }
});

api.post('/mindspace/v1/publications/:publicationId/offline', async (req, res) => {
  if (!mindSpacePublications) return res.status(503).json({ message: 'MindSpace 未启用' });
  try {
    const publication = await mindSpacePublications.offline(
      req.currentUser.id,
      req.params.publicationId,
    );
    await mindSpaceAudit?.write({
      userId: req.currentUser.id,
      action: 'page.offline',
      objectType: 'publication',
      objectId: req.params.publicationId,
      ip: req.ip,
    });
    return sendData(res, req, publication);
  } catch (error) {
    return mindSpaceError(res, req, error);
  }
});

api.get('/mindspace/v1/publications/:publicationId/stats', async (req, res) => {
  if (!mindSpacePublications) return res.status(503).json({ message: 'MindSpace 未启用' });
  try {
    return sendData(
      res,
      req,
      await mindSpacePublications.getStats(
        req.currentUser.id,
        req.params.publicationId,
      ),
    );
  } catch (error) {
    return mindSpaceError(res, req, error);
  }
});

api.get('/plaza/v1/categories', async (req, res) => {
  if (!ensurePlazaEnabled(res, req)) return;
  try {
    return sendData(res, req, { categories: await plazaPosts.listCategories() });
  } catch (error) {
    return plazaRouteError(res, req, error);
  }
});

api.get('/plaza/v1/seo/sitemap', async (req, res) => {
  if (!ensurePlazaEnabled(res, req)) return;
  if (!plazaSeo) return sendError(res, req, 503, 'plaza_unavailable', 'Plaza SEO 未启用');
  try {
    const data = await plazaSeo.listSitemapData({
      postLimit: req.query.post_limit,
      userLimit: req.query.user_limit,
    });
    return sendData(res, req, data);
  } catch (error) {
    return plazaRouteError(res, req, error);
  }
});

api.post('/plaza/v1/attribution/events', async (req, res) => {
  if (!plazaSeo) return sendError(res, req, 503, 'plaza_unavailable', 'Plaza 未启用');
  try {
    const result = await plazaSeo.recordAttribution(req.body ?? {}, plazaClientIp(req));
    return sendData(res, req, result, 201);
  } catch (error) {
    return plazaRouteError(res, req, error);
  }
});

api.post('/plaza/v1/posts/:id/reports', async (req, res) => {
  if (!plazaOps) return sendError(res, req, 503, 'plaza_unavailable', 'Plaza 未启用');
  if (!req.currentUser) return sendError(res, req, 401, 'unauthorized', '未授权，请重新登录');
  try {
    const report = await plazaOps.createReport(req.currentUser.id, {
      target_type: 'post',
      target_id: req.params.id,
      reason: req.body?.reason,
      detail: req.body?.detail,
    });
    return sendData(res, req, { report }, 201);
  } catch (error) {
    return plazaRouteError(res, req, error);
  }
});

api.post('/plaza/v1/comments/:id/reports', async (req, res) => {
  if (!plazaOps) return sendError(res, req, 503, 'plaza_unavailable', 'Plaza 未启用');
  if (!req.currentUser) return sendError(res, req, 401, 'unauthorized', '未授权，请重新登录');
  try {
    const report = await plazaOps.createReport(req.currentUser.id, {
      target_type: 'comment',
      target_id: req.params.id,
      reason: req.body?.reason,
      detail: req.body?.detail,
    });
    return sendData(res, req, { report }, 201);
  } catch (error) {
    return plazaRouteError(res, req, error);
  }
});

api.get('/plaza/v1/feed', async (req, res) => {
  if (!ensurePlazaEnabled(res, req)) return;
  try {
    const sessionId = resolvePlazaSessionId(req, res);
    const feed = await plazaPosts.listFeed({
      sort: req.query.sort,
      categorySlug: req.query.category ?? null,
      cursor: req.query.cursor ?? null,
      limit: req.query.limit,
      viewerId: req.currentUser?.id ?? null,
      sessionId,
    });
    return sendData(res, req, feed);
  } catch (error) {
    return plazaRouteError(res, req, error);
  }
});

api.post('/plaza/v1/events', async (req, res) => {
  if (!ensurePlazaEnabled(res, req)) return;
  if (!plazaEvents) return sendError(res, req, 503, 'plaza_unavailable', 'Plaza 未启用');
  try {
    const sessionId =
      String(req.body?.session_id ?? '').trim() || resolvePlazaSessionId(req, res);
    const result = await plazaEvents.recordEvents({
      userId: req.currentUser?.id ?? null,
      sessionId,
      events: req.body?.events ?? [],
    });
    return sendData(res, req, { ...result, session_id: sessionId }, 201);
  } catch (error) {
    return plazaRouteError(res, req, error);
  }
});

api.post('/plaza/v1/posts/:id/reactions', async (req, res) => {
  if (!ensurePlazaInteractions(res, req)) return;
  if (!req.currentUser) return sendError(res, req, 401, 'unauthorized', '未授权，请重新登录');
  try {
    const result = await plazaInteractions.addReaction(
      req.currentUser.id,
      req.params.id,
      req.body?.type,
    );
    const eventType = reactionEventType(result.type);
    if (eventType) {
      recordPlazaEventsAsync(req, res, [{ event_type: eventType, post_id: req.params.id }]);
    }
    return sendData(res, req, result);
  } catch (error) {
    return plazaRouteError(res, req, error);
  }
});

api.delete('/plaza/v1/posts/:id/reactions/:type', async (req, res) => {
  if (!ensurePlazaInteractions(res, req)) return;
  if (!req.currentUser) return sendError(res, req, 401, 'unauthorized', '未授权，请重新登录');
  try {
    const result = await plazaInteractions.removeReaction(
      req.currentUser.id,
      req.params.id,
      req.params.type,
    );
    return sendData(res, req, result);
  } catch (error) {
    return plazaRouteError(res, req, error);
  }
});

api.get('/plaza/v1/posts/:id/comments', async (req, res) => {
  if (!ensurePlazaInteractions(res, req)) return;
  try {
    const comments = await plazaInteractions.listComments(req.params.id, {
      cursor: req.query.cursor ?? null,
      limit: req.query.limit,
      parentId: req.query.parent_id ?? null,
      viewerId: req.currentUser?.id ?? null,
    });
    return sendData(res, req, comments);
  } catch (error) {
    return plazaRouteError(res, req, error);
  }
});

api.post('/plaza/v1/posts/:id/comments', async (req, res) => {
  if (!ensurePlazaInteractions(res, req)) return;
  if (!req.currentUser) return sendError(res, req, 401, 'unauthorized', '未授权，请重新登录');
  try {
    const comment = await plazaInteractions.createComment(req.currentUser.id, req.params.id, req.body ?? {});
    recordPlazaEventsAsync(req, res, [{ event_type: 'comment', post_id: req.params.id }]);
    return sendData(res, req, { comment }, 201);
  } catch (error) {
    return plazaRouteError(res, req, error);
  }
});

api.delete('/plaza/v1/comments/:id', async (req, res) => {
  if (!ensurePlazaInteractions(res, req)) return;
  if (!req.currentUser) return sendError(res, req, 401, 'unauthorized', '未授权，请重新登录');
  try {
    const comment = await plazaInteractions.deleteComment(req.currentUser.id, req.params.id);
    return sendData(res, req, { comment });
  } catch (error) {
    return plazaRouteError(res, req, error);
  }
});

api.post('/plaza/v1/comments/:id/reactions', async (req, res) => {
  if (!ensurePlazaInteractions(res, req)) return;
  if (!req.currentUser) return sendError(res, req, 401, 'unauthorized', '未授权，请重新登录');
  try {
    const liked = req.body?.liked !== false;
    const result = await plazaInteractions.toggleCommentLike(req.currentUser.id, req.params.id, liked);
    return sendData(res, req, result);
  } catch (error) {
    return plazaRouteError(res, req, error);
  }
});

api.get('/plaza/v1/users/:slug', async (req, res) => {
  if (!ensurePlazaInteractions(res, req)) return;
  try {
    const profile = await plazaInteractions.getUserProfile(
      req.params.slug,
      req.currentUser?.id ?? null,
    );
    return sendData(res, req, profile);
  } catch (error) {
    return plazaRouteError(res, req, error);
  }
});

api.get('/plaza/v1/users/:slug/posts', async (req, res) => {
  if (!ensurePlazaInteractions(res, req)) return;
  try {
    const feed = await plazaInteractions.listUserPosts(req.params.slug, {
      cursor: req.query.cursor ?? null,
      limit: req.query.limit,
      viewerId: req.currentUser?.id ?? null,
    });
    return sendData(res, req, feed);
  } catch (error) {
    return plazaRouteError(res, req, error);
  }
});

api.post('/plaza/v1/users/:slug/follow', async (req, res) => {
  if (!ensurePlazaInteractions(res, req)) return;
  if (!req.currentUser) return sendError(res, req, 401, 'unauthorized', '未授权，请重新登录');
  try {
    const result = await plazaInteractions.followUser(req.currentUser.id, req.params.slug);
    return sendData(res, req, result);
  } catch (error) {
    return plazaRouteError(res, req, error);
  }
});

api.delete('/plaza/v1/users/:slug/follow', async (req, res) => {
  if (!ensurePlazaInteractions(res, req)) return;
  if (!req.currentUser) return sendError(res, req, 401, 'unauthorized', '未授权，请重新登录');
  try {
    const result = await plazaInteractions.unfollowUser(req.currentUser.id, req.params.slug);
    return sendData(res, req, result);
  } catch (error) {
    return plazaRouteError(res, req, error);
  }
});

api.get('/plaza/v1/posts/:id', async (req, res) => {
  if (!ensurePlazaEnabled(res, req)) return;
  try {
    const post = await plazaPosts.getPostById(req.params.id, {
      viewerId: req.currentUser?.id ?? null,
    });
    void plazaRedis.recordView(req.params.id, plazaClientIp(req)).catch(() => {});
    recordPlazaEventsAsync(req, res, [{ event_type: 'view', post_id: req.params.id }]);
    return sendData(res, req, { post });
  } catch (error) {
    return plazaRouteError(res, req, error);
  }
});

api.post('/plaza/v1/posts', async (req, res) => {
  if (!ensurePlazaEnabled(res, req)) return;
  if (!req.currentUser) {
    return sendError(res, req, 401, 'unauthorized', '未授权，请重新登录');
  }
  try {
    const post = await plazaPosts.createPost(req.currentUser.id, req.body ?? {});
    return sendData(res, req, { post }, 201);
  } catch (error) {
    return plazaRouteError(res, req, error);
  }
});

api.patch('/plaza/v1/posts/:id', async (req, res) => {
  if (!ensurePlazaEnabled(res, req)) return;
  if (!req.currentUser) {
    return sendError(res, req, 401, 'unauthorized', '未授权，请重新登录');
  }
  try {
    const post = await plazaPosts.updatePost(req.currentUser.id, req.params.id, req.body ?? {});
    return sendData(res, req, { post });
  } catch (error) {
    return plazaRouteError(res, req, error);
  }
});

api.delete('/plaza/v1/posts/:id', async (req, res) => {
  if (!ensurePlazaEnabled(res, req)) return;
  if (!req.currentUser) {
    return sendError(res, req, 401, 'unauthorized', '未授权，请重新登录');
  }
  try {
    const post = await plazaPosts.hidePost(req.currentUser.id, req.params.id);
    return sendData(res, req, { post });
  } catch (error) {
    return plazaRouteError(res, req, error);
  }
});

function runHandlerChain(chain, req, res, next) {
  let index = 0;
  const run = (err) => {
    if (err) return next(err);
    const layer = chain[index++];
    if (!layer) return;
    layer(req, res, (error) => run(error));
  };
  run();
}

api.post('/llm/apply-local-fallback', async (req, res) => {
  // Client calls after creditsExhausted or relay 500 (see useTKMindChat).
  await userAuthReady;
  if (!userAuth || !llmProviderService || !tkmindProxy) {
    return res.status(503).json({ message: '未启用 LLM 配置' });
  }
  const me = await userAuth.getMe(userToken(req));
  if (!me) return res.status(401).json({ message: '未登录' });
  const sessionId = String(req.body?.session_id ?? '').trim();
  if (!sessionId) return res.status(400).json({ message: '缺少 session_id' });
  const owns = await userAuth.ownsSession(me.id, sessionId);
  if (!owns) return res.status(403).json({ message: '无权访问该会话' });
  try {
    const result = await tkmindProxy.applyLocalFallbackForSession(sessionId);
    if (!result.ok) return res.status(503).json(result);
    res.json(result);
  } catch (err) {
    res.status(500).json({
      message: err instanceof Error ? err.message : '切换本地 LLM 失败',
    });
  }
});

api.post('/agent/start', async (req, res, next) => {
  await userAuthReady;
  if (!userAuth || !tkmindProxy) return next();
  return runHandlerChain(tkmindProxy.handlers['POST /agent/start'], req, res, next);
});

api.post('/agent/resume', async (req, res, next) => {
  await userAuthReady;
  if (!userAuth || !tkmindProxy) return next();
  return runHandlerChain(tkmindProxy.handlers['POST /agent/resume'], req, res, next);
});

api.get('/sessions', async (req, res, next) => {
  await userAuthReady;
  if (!userAuth || !tkmindProxy) return next();
  return runHandlerChain(tkmindProxy.handlers['GET /sessions'], req, res, next);
});

// Session detail — serve from DB snapshot cache when fresh, fall through to Goose on miss.
api.get('/sessions/:sessionId', async (req, res, next) => {
  await userAuthReady;
  if (!userAuth || !tkmindProxy) return next();
  const sessionId = req.params.sessionId;
  const owns = await userAuth.ownsSession(req.currentUser.id, sessionId);
  if (!owns) {
    return res.status(403).json({ message: '无权访问该会话' });
  }
  // Hints from the client (session list already has these values).
  const hintMc = req.query.hint_mc ? Number(req.query.hint_mc) : null;
  const hintUa = req.query.hint_ua ? String(req.query.hint_ua) : null;

  try {
    if (sessionSnapshotService?.isEnabled()) {
      const snapshot = await sessionSnapshotService.get(sessionId);
      if (snapshot) {
        const mcMatch = hintMc == null || snapshot.meta.synced_msg_count === hintMc;
        const uaMatch = hintUa == null || snapshot.meta.source_updated_at === hintUa;
        if (mcMatch && uaMatch) {
          // Cache hit — reconstruct a Goose-compatible session response.
          const cachedGooseSession = {
            ...snapshot.session,
            // Embed only userVisible messages so getSession callers still work.
            conversation: snapshot.messages,
          };
          return res.json(cachedGooseSession);
        }
      }
    }
  } catch {
    // Snapshot read error: fall through silently to Goose.
  }

  // Cache miss — proxy to Goose and write-through on success.
  try {
    const target = await tkmindProxy.resolveTarget(sessionId);
    const upstream = await tkmindProxy.apiFetchTo(
      target,
      `/sessions/${encodeURIComponent(sessionId)}`,
      { method: 'GET' },
    );
    if (!upstream.ok) {
      const text = await upstream.text().catch(() => '');
      return res.status(upstream.status).send(text);
    }
    const gooseSession = await upstream.json();
    // Write-through: persist snapshot async, don't block the response.
    if (sessionSnapshotService?.isEnabled()) {
      const messages = (gooseSession.conversation ?? [])
        .filter((m) => m.metadata?.userVisible);
      void sessionSnapshotService
        .save(sessionId, req.currentUser.id, gooseSession, messages)
        .catch(() => {});
    }
    return res.json(gooseSession);
  } catch (err) {
    return res.status(502).json({ message: err instanceof Error ? err.message : '读取会话失败' });
  }
});

api.delete('/sessions/:sessionId', async (req, res, next) => {
  await userAuthReady;
  if (!userAuth || !tkmindProxy) return next();
  const sessionId = req.params.sessionId;
  const owns = await userAuth.ownsSession(req.currentUser.id, sessionId);
  if (!owns) {
    return res.status(403).json({ message: '无权访问该会话' });
  }
  try {
    const deleteTarget = await tkmindProxy.resolveTarget(sessionId);
    const upstream = await tkmindProxy.apiFetchTo(deleteTarget, `/sessions/${encodeURIComponent(sessionId)}`, {
      method: 'DELETE',
    });
    if (!upstream.ok && upstream.status !== 404) {
      const text = await upstream.text().catch(() => '');
      return res.status(upstream.status).send(text || '删除会话失败');
    }
    await userAuth.unregisterAgentSession(req.currentUser.id, sessionId);
    // Remove snapshot so it doesn't linger after deletion.
    void sessionSnapshotService?.remove(sessionId).catch(() => {});
    return res.status(204).end();
  } catch (err) {
    return res.status(500).json({ message: err instanceof Error ? err.message : '删除会话失败' });
  }
});

api.get('/sessions/:sessionId/events', async (req, res, next) => {
  await userAuthReady;
  if (!userAuth || !tkmindProxy) return next();
  const sessionId = req.params.sessionId;
  const owns = await userAuth.ownsSession(req.currentUser.id, sessionId);
  if (!owns) {
    return res.status(403).json({ message: '无权访问该会话' });
  }
  // After Finish, async-refresh snapshot so next open is a cache hit.
  const onAfterFinish = sessionSnapshotService?.isEnabled()
    ? (sid, uid) =>
        sessionSnapshotService.refresh(sid, uid, async (pathname, init) => {
          const target = await tkmindProxy.resolveTarget(sid);
          return tkmindProxy.apiFetchTo(target, pathname, init);
        })
    : null;
  return tkmindProxy.proxySessionEvents(req, res, sessionId, { onAfterFinish });
});

api.use(async (req, res, next) => {
  await userAuthReady;
  if (!userAuth || !tkmindProxy) return next();

  if (isNativeH5ApiPath(req.path)) {
    return sendError(res, req, 404, 'not_found', `接口不存在：${req.method} ${req.path}`);
  }

  const sessionMatch = req.path.match(/^\/sessions\/([^/]+)/);
  if (sessionMatch) {
    const sessionId = sessionMatch[1];
    if (req.path.endsWith('/events') && req.method === 'GET') {
      return next();
    }
    const owns = await userAuth.ownsSession(req.currentUser.id, sessionId);
    if (!owns) {
      return res.status(403).json({ message: '无权访问该会话' });
    }
    if (req.path.endsWith('/reply') && req.method === 'POST') {
      const gate = await userAuth.canUseChat(req.currentUser.id);
      if (!gate.ok) {
        return res.status(402).json({
          message: gate.message,
          code: gate.code,
          balanceCents: gate.balanceCents,
          minRechargeCents: gate.minRechargeCents,
          suggestedTiers: gate.suggestedTiers,
        });
      }
      try {
        await tkmindProxy.reconcileSessionPolicyForUser(req.currentUser.id, sessionId);
      } catch (err) {
        console.warn(
          'Session policy sync before reply failed:',
          err instanceof Error ? err.message : err,
        );
        return res.status(500).json({
          message:
            err instanceof Error
              ? `会话策略同步失败：${err.message}`
              : '会话策略同步失败',
        });
      }
      if (llmProviderService) {
        try {
          await tkmindProxy.applySessionLlmProvider(sessionId);
        } catch (err) {
          console.warn(
            'LLM provider sync before reply skipped:',
            err instanceof Error ? err.message : err,
          );
        }
      }
    }
  }

  if (req.method === 'POST' && req.path === '/agent/resume' && req.body?.session_id) {
    const owns = await userAuth.ownsSession(req.currentUser.id, req.body.session_id);
    if (!owns) {
      return res.status(403).json({ message: '无权访问该会话' });
    }
  }

  if (req.body?.working_dir) {
    const allowed = await userAuth.isPathAllowed(req.currentUser.id, req.body.working_dir);
    if (!allowed) {
      return res.status(403).json({ message: '工作目录不在授权范围内' });
    }
  }

  return tkmindProxy.proxyFallback(req, res);
});


api.use(
  createProxyMiddleware({
    target: API_TARGET,
    changeOrigin: true,
    secure: false,
    pathRewrite: { '^/api': '' },
    on: {
      proxyReq: (proxyReq) => {
        proxyReq.setHeader('X-Secret-Key', API_SECRET);
      },
    },
  }),
);

app.use('/api', api);

function publishedPageCsp(html, { embed = false, raw = false } = {}) {
  const isFullHtml = /^\s*<!doctype html/i.test(html) || /^\s*<html[\s>]/i.test(html);
  if (embed && isFullHtml) {
    return publishedPageCspForEmbed(true);
  }
  if (raw && isFullHtml) {
    return "default-src 'none'; style-src 'unsafe-inline' https:; img-src data: https:; font-src https: data:; base-uri 'none'; form-action 'self'; frame-ancestors 'self'; script-src 'unsafe-inline'";
  }
  if (isFullHtml) {
    return "default-src 'none'; style-src 'unsafe-inline' https:; img-src data: https:; font-src https: data:; base-uri 'none'; form-action 'self'; frame-ancestors 'self'; script-src 'none'";
  }
  return "default-src 'none'; style-src 'unsafe-inline'; img-src data:; font-src 'none'; base-uri 'none'; form-action 'self'; frame-ancestors 'self'";
}

function appendQueryParam(url, key, value) {
  const separator = url.includes('?') ? '&' : '?';
  return `${url}${separator}${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
}

function resolveRequestOrigin(req) {
  const host = (req.headers['x-forwarded-host'] || req.headers.host || '').toString().split(',')[0].trim();
  if (!host) return '';
  const isLocalHost = /^(localhost|127\.0\.0\.1|\[::1\]|192\.168\.|10\.|100\.)/i.test(host);
  const fwdProto = (req.headers['x-forwarded-proto'] || '').toString().split(',')[0].trim();
  const proto = isLocalHost ? fwdProto || req.protocol || 'http' : 'https';
  return `${proto}://${host}`;
}

function detectPublishedPageTitle(html) {
  const match = String(html ?? '').match(/<title[^>]*>([^<]+)<\/title>/i);
  return match?.[1]?.replace(/\s+/g, ' ').trim() || 'MindSpace 页面';
}

function publishedPageShellHtml({ iframeUrl, shareUrl, title }) {
  const iframeSrc = escapePublicHtml(iframeUrl);
  const safeShareUrl = escapePublicHtml(shareUrl);
  const safeTitle = escapePublicHtml(title);
  const serializedShareUrl = JSON.stringify(shareUrl).replace(/</g, '\\u003c');
  const serializedTitle = JSON.stringify(title).replace(/</g, '\\u003c');
  return `<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
  <meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; img-src data:; font-src 'none'; connect-src 'self' https://cdn.jsdelivr.net; script-src 'unsafe-inline' https://cdn.jsdelivr.net; frame-src 'self'; base-uri 'none'; form-action 'none'">
  <title>${safeTitle}</title>
  <style>
    * { box-sizing: border-box; }
    html, body { margin: 0; min-height: 100%; background: #f6f1e7; }
    body { font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, 'SF Pro Text', sans-serif; color: #18211d; }
    .publication-shell { position: relative; min-height: 100vh; }
    .publication-frame { display: block; width: 100%; min-height: 100vh; border: 0; background: #fff; }
    .publication-share-fab {
      position: fixed;
      right: 18px;
      bottom: calc(18px + env(safe-area-inset-bottom, 0px));
      z-index: 50;
      border: 0;
      border-radius: 999px;
      padding: 14px 18px;
      background: linear-gradient(135deg, #2f6f57, #1f4c3c);
      color: #fffdf7;
      font: 700 14px/1 ui-sans-serif, sans-serif;
      letter-spacing: .04em;
      box-shadow: 0 20px 48px rgba(24, 33, 29, .24);
      cursor: pointer;
    }
    .publication-share-sheet[hidden] { display: none; }
    .publication-share-sheet {
      position: fixed;
      inset: 0;
      z-index: 60;
      display: grid;
      place-items: end center;
      padding: 16px;
      background: rgba(7, 12, 10, 0.56);
    }
    .publication-share-panel {
      width: min(480px, 100%);
      padding: 18px 18px 22px;
      border-radius: 24px 24px 20px 20px;
      background: #fffaf2;
      box-shadow: 0 24px 80px rgba(24, 33, 29, 0.24);
    }
    .publication-share-header {
      display: flex;
      align-items: flex-start;
      justify-content: space-between;
      gap: 12px;
      margin-bottom: 16px;
    }
    .publication-share-header p {
      margin: 0;
      color: #9b6518;
      font-size: 11px;
      font-weight: 800;
      letter-spacing: .16em;
      text-transform: uppercase;
    }
    .publication-share-header h2 {
      margin: 6px 0 0;
      font: 500 28px/1.1 Georgia, 'Times New Roman', serif;
      color: #18211d;
    }
    .publication-share-header strong {
      display: block;
      margin-top: 6px;
      color: #68716c;
      font-size: 13px;
      font-weight: 500;
    }
    .publication-share-close {
      width: 36px;
      height: 36px;
      border: 0;
      border-radius: 999px;
      background: rgba(24, 33, 29, 0.08);
      color: #18211d;
      font-size: 24px;
      line-height: 1;
      cursor: pointer;
    }
    .publication-share-grid {
      display: grid;
      grid-template-columns: repeat(3, minmax(0, 1fr));
      gap: 12px;
    }
    .publication-share-option {
      display: grid;
      gap: 8px;
      justify-items: center;
      padding: 14px 10px;
      border: 1px solid rgba(24, 33, 29, 0.08);
      border-radius: 18px;
      background: rgba(255, 255, 255, 0.72);
      color: #18211d;
      font: inherit;
      cursor: pointer;
    }
    .publication-share-option:disabled {
      opacity: .6;
      cursor: wait;
    }
    .publication-share-option span {
      display: grid;
      place-items: center;
      width: 44px;
      height: 44px;
      border-radius: 14px;
      color: #fff;
      font-size: 18px;
      font-weight: 700;
    }
    .publication-share-option[data-action="wechat"] span { background: linear-gradient(145deg, #07c160, #06ad56); }
    .publication-share-option[data-action="copy"] span { background: linear-gradient(145deg, #2f6f57, #245845); }
    .publication-share-option[data-action="capture"] span { background: linear-gradient(145deg, #8f6b2f, #6f5121); }
    .publication-share-link, .publication-share-message {
      margin-top: 16px;
      padding: 12px 14px;
      border-radius: 14px;
      font-size: 13px;
    }
    .publication-share-link {
      background: rgba(24, 33, 29, 0.05);
    }
    .publication-share-link code {
      display: block;
      margin-top: 6px;
      word-break: break-all;
      color: #445049;
      font-size: 12px;
    }
    .publication-share-message {
      display: none;
      color: #245845;
      background: rgba(47, 111, 87, 0.12);
    }
    .publication-share-message.is-visible {
      display: block;
    }
    @media (max-width: 480px) {
      .publication-share-sheet {
        padding: 0;
        place-items: end stretch;
      }
      .publication-share-panel {
        width: 100%;
        border-radius: 24px 24px 0 0;
      }
      .publication-share-fab {
        right: 14px;
        bottom: calc(14px + env(safe-area-inset-bottom, 0px));
      }
    }
  </style>
</head>
<body>
  <div class="publication-shell">
    <!-- Game-like publications need script execution, but not same-origin sandbox escape. -->
    <iframe class="publication-frame" title="${safeTitle}" src="${iframeSrc}" sandbox="allow-scripts"></iframe>
  </div>
  <button type="button" class="publication-share-fab" id="publication-share-fab">分享</button>
  <div class="publication-share-sheet" id="publication-share-sheet" hidden>
    <section class="publication-share-panel" role="dialog" aria-modal="true" aria-labelledby="publication-share-title">
      <header class="publication-share-header">
        <div>
          <p>Share</p>
          <h2 id="publication-share-title">分享页面</h2>
          <strong>${safeTitle}</strong>
        </div>
        <button type="button" class="publication-share-close" id="publication-share-close" aria-label="关闭">×</button>
      </header>
      <div class="publication-share-grid">
        <button type="button" class="publication-share-option" data-action="wechat"><span>微</span>微信</button>
        <button type="button" class="publication-share-option" data-action="copy"><span>链</span>复制链接</button>
        <button type="button" class="publication-share-option" data-action="capture"><span>图</span>保存长图</button>
      </div>
      <div class="publication-share-link">
        分享链接
        <code>${safeShareUrl}</code>
      </div>
      <div class="publication-share-message" id="publication-share-message" role="status"></div>
    </section>
  </div>
  <script>
    (function () {
      var shareUrl = ${serializedShareUrl};
      var shareTitle = ${serializedTitle};
      var fab = document.getElementById('publication-share-fab');
      var sheet = document.getElementById('publication-share-sheet');
      var close = document.getElementById('publication-share-close');
      var message = document.getElementById('publication-share-message');
      var frame = document.querySelector('.publication-frame');
      var actionButtons = sheet ? Array.prototype.slice.call(sheet.querySelectorAll('.publication-share-option')) : [];
      var html2canvasLoader = null;
      function setBusy(busy) {
        actionButtons.forEach(function (button) {
          button.disabled = !!busy;
          button.setAttribute('aria-busy', busy ? 'true' : 'false');
        });
      }
      function setMessage(text, isError) {
        if (!message) return;
        message.textContent = text || '';
        message.classList.toggle('is-visible', Boolean(text));
        message.style.color = isError ? '#8b2d20' : '#245845';
        message.style.background = isError ? 'rgba(139, 45, 32, 0.12)' : 'rgba(47, 111, 87, 0.12)';
      }
      function openSheet() {
        if (sheet) sheet.hidden = false;
        setMessage('');
      }
      function closeSheet() {
        if (sheet) sheet.hidden = true;
      }
      async function copyText(text) {
        if (navigator.clipboard && navigator.clipboard.writeText) {
          try {
            await navigator.clipboard.writeText(text);
            return;
          } catch {
            // Fall back to execCommand for browsers where clipboard API exists but is blocked.
          }
        }
        var textarea = document.createElement('textarea');
        textarea.value = text;
        textarea.style.position = 'fixed';
        textarea.style.opacity = '0';
        textarea.style.left = '-9999px';
        document.body.appendChild(textarea);
        textarea.focus();
        textarea.select();
        var copied = document.execCommand('copy');
        textarea.remove();
        if (!copied) throw new Error('复制失败，请手动复制链接');
      }
      function loadHtml2Canvas() {
        if (window.html2canvas) return Promise.resolve(window.html2canvas);
        if (html2canvasLoader) return html2canvasLoader;
        html2canvasLoader = new Promise(function (resolve, reject) {
          var script = document.createElement('script');
          script.src = 'https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js';
          script.async = true;
          script.onload = function () {
            if (!window.html2canvas) {
              reject(new Error('无法加载页面截图能力'));
              return;
            }
            resolve(window.html2canvas);
          };
          script.onerror = function () { reject(new Error('无法加载页面截图能力')); };
          document.body.appendChild(script);
        }).catch(function (error) {
          html2canvasLoader = null;
          throw error;
        });
        return html2canvasLoader;
      }
      async function saveLongImage() {
        if (!frame || !frame.contentDocument || !frame.contentDocument.body) {
          throw new Error('页面内容暂时不可访问');
        }
        var renderer = await loadHtml2Canvas();
        var doc = frame.contentDocument;
        var body = doc.body;
        var width = Math.max(doc.documentElement ? doc.documentElement.scrollWidth : 0, body.scrollWidth, 320);
        var height = Math.max(doc.documentElement ? doc.documentElement.scrollHeight : 0, body.scrollHeight, 320);
        var canvas = await renderer(body, {
          useCORS: true,
          scale: 2,
          backgroundColor: '#ffffff',
          width: width,
          height: height,
          windowWidth: width,
          windowHeight: height,
          x: 0,
          y: 0,
          scrollX: 0,
          scrollY: 0
        });
        var link = document.createElement('a');
        var timestamp = new Date().toISOString().replace(/[T:]/g, '-').replace(/\..+/, '');
        link.href = canvas.toDataURL('image/png');
        link.download = 'mindspace-public-page-' + timestamp + '.png';
        document.body.appendChild(link);
        link.click();
        link.remove();
      }
      if (fab) fab.addEventListener('click', openSheet);
      if (close) close.addEventListener('click', closeSheet);
      if (sheet) {
        sheet.addEventListener('click', function (event) {
          if (event.target === sheet) closeSheet();
        });
        actionButtons.forEach(function (button) {
          button.addEventListener('click', async function () {
            var action = button.getAttribute('data-action');
            try {
              setBusy(true);
              setMessage('');
              if (action === 'wechat') {
                await copyText([shareTitle, shareUrl].filter(Boolean).join('\\n'));
                setMessage('文案已复制，请打开微信粘贴分享');
                return;
              }
              if (action === 'copy') {
                await copyText(shareUrl);
                setMessage('链接已复制');
                return;
              }
              if (action === 'capture') {
                setMessage('正在生成长图，请稍候');
                await saveLongImage();
                setMessage('长图已保存');
              }
            } catch (error) {
              setMessage(error && error.message ? error.message : '操作失败，请稍后再试', true);
            } finally {
              setBusy(false);
            }
          });
        });
      }
      function syncFrameHeight() {
        if (!frame) return;
        var doc = frame.contentDocument;
        var body = doc && doc.body;
        var root = doc && doc.documentElement;
        var height = Math.max(
          body ? body.scrollHeight : 0,
          body ? body.offsetHeight : 0,
          root ? root.scrollHeight : 0,
          root ? root.offsetHeight : 0,
          window.innerHeight,
          320
        );
        frame.style.height = height + 'px';
      }
      if (frame) {
        frame.addEventListener('load', function () {
          syncFrameHeight();
          window.setTimeout(syncFrameHeight, 300);
        });
      }
      window.addEventListener('message', function (event) {
        if (!event || !event.data || event.data.type !== 'plaza:embed-section' || !frame) return;
        if (event.data.height) {
          frame.style.height = Math.max(Number(event.data.height) || 0, window.innerHeight, 320) + 'px';
        }
      });
      window.addEventListener('resize', syncFrameHeight);
    })();
  </script>
</body>
</html>`;
}

function sendPublishedPage(req, res, result, { embed = false, raw = false } = {}) {
  let html = result.html;
  if (embed) {
    html = preparePublicationHtmlForEmbed(html);
    allowPlazaEmbedFrame(res);
  } else if (raw) {
    html = stripPublicationHtmlCspMeta(html);
  }
  const isFullHtml = /^\s*<!doctype html/i.test(html) || /^\s*<html[\s>]/i.test(html);
  const canWrapWithShell = !embed && !raw && isFullHtml && result.publication?.accessMode !== 'password';
  if (canWrapWithShell) {
    const title = detectPublishedPageTitle(html);
    const rawUrl = appendQueryParam(req.originalUrl || req.url || '', 'view', 'raw');
    res.set('Content-Type', 'text/html; charset=utf-8');
    res.set(
      'Content-Security-Policy',
      "default-src 'none'; style-src 'unsafe-inline'; img-src data:; font-src 'none'; connect-src 'self' https://cdn.jsdelivr.net; script-src 'unsafe-inline' https://cdn.jsdelivr.net; frame-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'self'",
    );
    res.set(
      'Cache-Control',
      result.publication.accessMode === 'public' ? 'public, max-age=60' : 'private, no-store',
    );
    return res.send(
      publishedPageShellHtml({
        iframeUrl: rawUrl,
        shareUrl: req.originalUrl ? new URL(req.originalUrl, resolveRequestOrigin(req) || 'http://localhost').toString() : '',
        title,
      }),
    );
  }
  res.set('Content-Type', 'text/html; charset=utf-8');
  res.set('Content-Security-Policy', publishedPageCsp(html, { embed, raw }));
  res.set(
    'Cache-Control',
    result.publication.accessMode === 'public' ? 'public, max-age=60' : 'private, no-store',
  );
  return res.send(html);
}

function passwordGateHtml(action) {
  return `<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; form-action 'self'; base-uri 'none'">
  <title>受保护页面</title>
  <style>
    * { box-sizing: border-box; }
    body { min-height: 100vh; margin: 0; display: grid; place-items: center; padding: 24px; color: #17221d; background: radial-gradient(circle at top right, #d9e9df, transparent 42rem), #f5f0e5; font-family: ui-sans-serif, sans-serif; }
    form { width: min(420px, 100%); padding: 36px; border: 1px solid #d6d0c3; border-radius: 28px; background: #fffdf7; box-shadow: 0 24px 70px rgba(35, 48, 40, .12); }
    p { color: #68716c; line-height: 1.7; }
    input, button { width: 100%; margin-top: 14px; padding: 14px 16px; border-radius: 12px; font: inherit; }
    input { border: 1px solid #c9c6bc; }
    button { border: 0; color: white; background: #2f6f57; cursor: pointer; font-weight: 800; }
  </style>
</head>
<body>
  <form method="post" action="${action}">
    <h1>此页面受密码保护</h1>
    <p>请输入发布者提供的访问密码。密码不会写入链接或浏览器日志。</p>
    <input type="password" name="password" minlength="8" maxlength="128" required autocomplete="current-password">
    <button type="submit">访问页面</button>
  </form>
</body>
</html>`;
}

function escapePublicHtml(value) {
  return String(value ?? '')
    .replaceAll('&', '&amp;')
    .replaceAll('<', '&lt;')
    .replaceAll('>', '&gt;')
    .replaceAll('"', '&quot;')
    .replaceAll("'", '&#039;');
}

function publicHomepageHtml(data) {
  const cards = data.pages
    .map(
      (page) => `\
<a class="card" href="${escapePublicHtml(page.publicUrl)}">
  <div class="card-top">
    <span>${escapePublicHtml(page.templateId)}</span>
    <strong>${Number(page.viewCount)} 次浏览</strong>
  </div>
  <h2>${escapePublicHtml(page.title)}</h2>
  <p>${escapePublicHtml(page.summary || '暂无摘要')}</p>
  <div class="card-foot">
    <span>${new Date(page.publishedAt).toLocaleDateString('zh-CN')}</span>
    <span>打开页面</span>
  </div>
</a>`,
    )
    .join('');
  return `<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; img-src data:; font-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'self'">
  <title>${escapePublicHtml(data.owner.displayName)} · MindSpace</title>
  <style>
    :root { color: #17221d; background: #f5f0e5; font-family: 'Iowan Old Style', 'Palatino Linotype', serif; }
    * { box-sizing: border-box; }
    body { margin: 0; min-height: 100vh; background:
      radial-gradient(circle at top right, rgba(79, 137, 112, .18), transparent 32rem),
      linear-gradient(180deg, #f9f6ef 0%, #f1eadc 100%);
      color: #17221d;
    }
    main { width: min(1120px, calc(100% - 32px)); margin: 0 auto; padding: 48px 0 72px; }
    .hero { padding: 28px 0 32px; border-bottom: 1px solid rgba(23, 34, 29, .12); }
    .eyebrow { margin: 0 0 12px; color: #8f6b2f; font: 700 12px/1.4 ui-sans-serif, sans-serif; letter-spacing: .18em; text-transform: uppercase; }
    h1 { margin: 0; font-size: clamp(40px, 8vw, 82px); line-height: .96; letter-spacing: -.05em; }
    .lead { max-width: 760px; margin: 18px 0 0; color: #56615b; font: 18px/1.8 ui-sans-serif, sans-serif; }
    .stats { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 22px; }
    .stats span { border: 1px solid rgba(23, 34, 29, .12); border-radius: 999px; padding: 10px 14px; background: rgba(255,255,255,.6); font: 600 13px/1 ui-sans-serif, sans-serif; color: #405048; }
    .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 18px; margin-top: 34px; }
    .card { display: flex; flex-direction: column; gap: 16px; min-height: 230px; padding: 22px; border-radius: 24px; text-decoration: none; color: inherit; background: rgba(255,253,247,.92); border: 1px solid rgba(23, 34, 29, .08); box-shadow: 0 20px 60px rgba(45, 53, 47, .08); transition: transform .18s ease, box-shadow .18s ease; }
    .card:hover { transform: translateY(-2px); box-shadow: 0 24px 70px rgba(45, 53, 47, .12); }
    .card-top, .card-foot { display: flex; align-items: center; justify-content: space-between; gap: 12px; font: 600 12px/1.4 ui-sans-serif, sans-serif; color: #6a746f; text-transform: uppercase; letter-spacing: .08em; }
    .card h2 { margin: 0; font-size: 28px; line-height: 1.08; letter-spacing: -.03em; }
    .card p { margin: 0; color: #56615b; font: 15px/1.75 ui-sans-serif, sans-serif; }
    .empty { margin-top: 32px; padding: 28px; border-radius: 24px; background: rgba(255,253,247,.76); border: 1px dashed rgba(23, 34, 29, .18); color: #56615b; font: 16px/1.7 ui-sans-serif, sans-serif; }
    @media (max-width: 640px) {
      main { width: min(100% - 24px, 1120px); padding: 24px 0 48px; }
      .hero { padding-top: 12px; }
      .grid { gap: 14px; }
      .card { min-height: 0; border-radius: 20px; }
      .card h2 { font-size: 24px; }
    }
  </style>
</head>
<body>
  <main>
    <section class="hero">
      <p class="eyebrow">MindSpace Public</p>
      <h1>${escapePublicHtml(data.owner.displayName)}</h1>
      <p class="lead">这里展示 ${escapePublicHtml(data.owner.displayName)} 当前公开发布且在线的 MindSpace 页面。</p>
      <div class="stats">
        <span>${Number(data.pageCount)} 个公开页面</span>
        <span>${Number(data.totalViews)} 次累计浏览</span>
        <span>/u/${escapePublicHtml(data.owner.slug)}</span>
      </div>
    </section>
    ${
      data.pages.length
        ? `<section class="grid">${cards}</section>`
        : '<section class="empty">这个主页还没有公开页面。稍后再来看看，或者直接访问作者分享给你的专属链接。</section>'
    }
  </main>
</body>
</html>`;
}

async function resolvePublishedRoute(req, res, password = null) {
  await userAuthReady;
  if (!mindSpacePublications) return res.status(503).send('MindSpace 未启用');
  try {
    const viewer = req.userSession && userAuth ? await userAuth.getMe(req.userToken) : null;
    const result = await mindSpacePublications.resolvePublic(
      req.params.ownerSlug,
      req.params.urlSlug,
      viewer?.id,
      password,
      {
        userAgent: req.get('user-agent'),
        referrer: req.get('referer'),
      },
    );
    return sendPublishedPage(req, res, result, {
      embed: isPlazaEmbedRequest(req.query),
      raw: String(req.query.view ?? '').toLowerCase() === 'raw',
    });
  } catch (error) {
    if (error?.code === 'publication_password_required') {
      return res.status(password ? 403 : 200).send(passwordGateHtml(req.originalUrl));
    }
    if (error?.code === 'publication_login_required') {
      return res.status(401).send('请先登录 TKMind 后再访问此页面');
    }
    if (error?.code === 'publication_not_found') return res.status(404).send('页面不存在或已下线');
    return res.status(500).send('页面加载失败');
  }
}

app.use(async (req, res, next) => {
  const thumbnailMatch = /^\/u\/([^/]+)\/pages\/([^/]+)\.thumbnail\.png$/.exec(req.path);
  if (!thumbnailMatch) return next();
  const ownerSlug = thumbnailMatch[1];
  const urlSlug = thumbnailMatch[2];
  await userAuthReady;
  if (!authPool || !mindSpacePages) return res.status(503).send('MindSpace 未启用');
  try {
    const [rows] = await authPool.query(
      `SELECT pr.user_id, pr.page_id
       FROM h5_publish_records pr
       JOIN h5_users u ON u.id = pr.user_id
       WHERE COALESCE(u.slug, u.username) = ?
         AND pr.url_slug = ?
         AND pr.status = 'online'
       ORDER BY pr.published_at DESC
       LIMIT 1`,
      [ownerSlug, urlSlug],
    );
    const row = rows[0];
    if (!row) return res.status(404).send('缩略图不存在');
    const svg = await mindSpacePages.renderThumbnail(row.user_id, row.page_id);
    res.set('Content-Type', 'image/png');
    res.set('Cache-Control', 'public, max-age=300');
    return res.send(rasterizeThumbnailSvgToPng(svg));
  } catch (error) {
    return res.status(500).send('缩略图加载失败');
  }
});

app.get('/u/:ownerSlug/pages/:urlSlug', async (req, res) => {
  return resolvePublishedRoute(req, res);
});

app.get('/u/:ownerSlug', async (req, res) => {
  await userAuthReady;
  if (!mindSpacePublications) return res.status(503).send('MindSpace 未启用');
  try {
    const data = await mindSpacePublications.getPublicHomepage(req.params.ownerSlug);
    res.set('Content-Type', 'text/html; charset=utf-8');
    res.set(
      'Content-Security-Policy',
      "default-src 'none'; style-src 'unsafe-inline'; img-src data:; font-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'self'",
    );
    res.set('Cache-Control', 'public, max-age=60');
    return res.send(publicHomepageHtml(data));
  } catch (error) {
    if (error?.code === 'publication_not_found') return res.status(404).send('主页不存在');
    return res.status(500).send('主页加载失败');
  }
});

app.post(
  '/u/:ownerSlug/pages/:urlSlug',
  express.urlencoded({ extended: false, limit: '2kb' }),
  async (req, res) => resolvePublishedRoute(req, res, req.body?.password),
);

app.get('/s/:token', async (req, res) => {
  await userAuthReady;
  if (!mindSpacePublications) return res.status(503).send('MindSpace 未启用');
  try {
    const viewer = req.userSession && userAuth ? await userAuth.getMe(req.userToken) : null;
    return sendPublishedPage(
      req,
      res,
      await mindSpacePublications.resolvePrivateLink(req.params.token, viewer?.id, {
        userAgent: req.get('user-agent'),
        referrer: req.get('referer'),
      }),
      {
        embed: isPlazaEmbedRequest(req.query),
        raw: String(req.query.view ?? '').toLowerCase() === 'raw',
      },
    );
  } catch (error) {
    if (error?.code === 'publication_not_found') return res.status(404).send('页面不存在或已下线');
    return res.status(500).send('页面加载失败');
  }
});

const USERNAME_SLUG = /^[a-z0-9_]{2,32}$/;

async function resolvePublishDirKey(segment) {
  const lower = String(segment ?? '').trim().toLowerCase();
  if (!lower) return null;
  if (PUBLISH_KEY_UUID.test(lower)) return lower;
  if (USERNAME_SLUG.test(lower)) {
    if (authPool) {
      const [rows] = await authPool.query(`SELECT id FROM h5_users WHERE username = ? LIMIT 1`, [lower]);
      if (rows[0]?.id) return String(rows[0].id).toLowerCase();
    }
    return lower;
  }
  return null;
}

/**
 * Send a file, injecting Open Graph tags for .html so forwarded links unfurl with a cover.
 * Non-HTML files (assets, etc.) are streamed unchanged via res.sendFile.
 */
function sendPublishFile(req, res, filePath) {
  if (!filePath.toLowerCase().endsWith('.html')) {
    res.sendFile(filePath, (err) => {
      if (err && !res.headersSent) res.status(404).json({ message: '文件不存在' });
    });
    return;
  }
  let html;
  try {
    html = fs.readFileSync(filePath, 'utf8');
  } catch {
    res.status(404).json({ message: '文件不存在' });
    return;
  }
  const embed = isPlazaEmbedRequest(req.query);
  if (embed) {
    html = preparePublicationHtmlForEmbed(html);
    allowPlazaEmbedFrame(res);
    res.set('Content-Security-Policy', publishedPageCsp(html, { embed }));
  }
  const host = (req.headers['x-forwarded-host'] || req.headers.host || '').toString().split(',')[0].trim();
  // Share cards (esp. WeChat) require https og:image. The edge only serves public domains
  // over https, but the proxy chain forwards X-Forwarded-Proto: http to the node — so for a
  // public host we force https and ignore the (wrong) forwarded scheme. Loopback/LAN stays http.
  const isLocalHost = /^(localhost|127\.0\.0\.1|\[::1\]|192\.168\.|10\.|100\.)/i.test(host);
  const fwdProto = (req.headers['x-forwarded-proto'] || '').toString().split(',')[0].trim();
  const proto = isLocalHost ? fwdProto || req.protocol || 'http' : 'https';
  const origin = host ? `${proto}://${host}` : '';
  const cleanPath = req.originalUrl.split('?')[0].split('#')[0];
  // Distinguish an explicit file URL (.../space.html) from a directory that resolves to
  // index.html (.../<uuid> or .../<uuid>/) so relative covers and the thumbnail resolve correctly.
  const servedName = path.basename(filePath);
  const urlLast = decodeURIComponent(cleanPath.split('/').filter(Boolean).pop() ?? '');
  const isImplicitIndex = urlLast.toLowerCase() !== servedName.toLowerCase();
  const pageUrl = origin
    ? `${origin}${isImplicitIndex && !cleanPath.endsWith('/') ? `${cleanPath}/` : cleanPath}`
    : '';
  const pageDirUrl = !origin
    ? ''
    : isImplicitIndex
      ? `${origin}${cleanPath.endsWith('/') ? cleanPath : `${cleanPath}/`}`
      : `${origin}${cleanPath.slice(0, cleanPath.lastIndexOf('/') + 1)}`;
  // Guaranteed fallback cover: the page's feed thumbnail, served on demand as PNG.
  let fallbackImageUrl = '';
  const svgSibling = filePath.replace(/\.[^./]+$/, '.thumbnail.svg');
  if (pageDirUrl && fs.existsSync(svgSibling)) {
    const pngName = path.basename(thumbnailPngPathForSvg(svgSibling));
    fallbackImageUrl = `${pageDirUrl}${pngName}`;
  }
  try {
    html = injectOgTags(html, { origin, pageUrl, pageDirUrl, fallbackImageUrl });
  } catch {
    // On any parse failure, fall back to the original HTML — never break page delivery.
  }
  res.set('Content-Type', 'text/html; charset=utf-8');
  res.send(html);
}

const MISPLACED_PUBLIC_HTML_NAME = /^[a-z0-9][a-z0-9._-]{0,127}\.html$/i;

async function recoverMisplacedPublicHtml(targetDir, resolvedRoot, rest) {
  if (rest.length !== 2 || rest[0] !== PUBLIC_ZONE_DIR) return null;
  const filename = rest[1];
  if (!MISPLACED_PUBLIC_HTML_NAME.test(filename)) return null;

  const destination = path.resolve(targetDir, PUBLIC_ZONE_DIR, filename);
  if (!destination.startsWith(`${resolvedRoot}${path.sep}`)) return null;
  const candidates = [
    path.resolve(__dirname, filename),
    path.resolve(__dirname, PUBLIC_ZONE_DIR, filename),
  ];
  for (const candidate of candidates) {
    if (candidate === destination) continue;
    if (!candidate.startsWith(`${__dirname}${path.sep}`)) continue;
    if (!fs.existsSync(candidate) || !fs.statSync(candidate).isFile()) continue;
    fs.mkdirSync(path.dirname(destination), { recursive: true });
    fs.copyFileSync(candidate, destination);
    await ensureWorkspaceHtmlThumbnail(targetDir, `${PUBLIC_ZONE_DIR}/${filename}`).catch(() => {});
    console.warn(
      `[MindSpace] recovered misplaced public HTML ${path.relative(__dirname, candidate)} -> ${path.relative(__dirname, destination)}`,
    );
    return destination;
  }
  return null;
}

async function serveUserPublishFile(req, res, next) {
  const parts = req.path.split('/').filter(Boolean);
  if (parts.length < 1) {
    res.status(404).json({ message: '未找到页面' });
    return;
  }

  const dirKey = await resolvePublishDirKey(parts[0]);
  if (!dirKey) {
    res.status(404).json({ message: '未找到页面' });
    return;
  }

  if (parts[0].toLowerCase() !== dirKey && USERNAME_SLUG.test(parts[0])) {
    const rest = parts.slice(1).map(encodeURIComponent).join('/');
    const target = `/${PUBLISH_ROOT_DIR}/${dirKey}${rest ? `/${rest}` : '/'}`;
    res.redirect(301, target);
    return;
  }

  const [username, ...rest] = [dirKey, ...parts.slice(1)];
  const targetDir = path.join(__dirname, PUBLISH_ROOT_DIR, username);
  const resolvedRoot = path.resolve(targetDir);
  const filePath = path.join(targetDir, ...rest);
  const resolvedPath = path.resolve(filePath);

  if (!resolvedPath.startsWith(`${resolvedRoot}${path.sep}`) && resolvedPath !== resolvedRoot) {
    res.status(403).json({ message: '禁止访问' });
    return;
  }

  if (!fs.existsSync(targetDir)) {
    res.status(404).json({ message: '用户不存在' });
    return;
  }

  // On-demand cover: rasterize <base>.thumbnail.svg → .thumbnail.png the first time a
  // forwarded link's og:image is fetched (and refresh it when the SVG changes).
  if (/\.thumbnail\.png$/i.test(resolvedPath)) {
    const svgSibling = resolvedPath.replace(/\.png$/i, '.svg');
    if (fs.existsSync(svgSibling)) {
      const pngPath = ensureThumbnailPng(svgSibling);
      if (pngPath && fs.existsSync(pngPath)) {
        res.set('Cache-Control', 'public, max-age=300');
        res.sendFile(pngPath);
        return;
      }
    }
  }

  if (!fs.existsSync(resolvedPath)) {
    if (rest.length === 1 && rest[0].toLowerCase().endsWith('.html')) {
      const publicFallback = path.resolve(targetDir, PUBLIC_ZONE_DIR, rest[0]);
      if (
        publicFallback.startsWith(`${resolvedRoot}${path.sep}`) &&
        fs.existsSync(publicFallback) &&
        fs.statSync(publicFallback).isFile()
      ) {
        const canonical = `/${PUBLISH_ROOT_DIR}/${encodeURIComponent(username)}/${PUBLIC_ZONE_DIR}/${encodeURIComponent(rest[0])}`;
        res.redirect(301, canonical);
        return;
      }
    }
    if (rest.length === 2 && rest[0] === PUBLIC_ZONE_DIR && rest[1].toLowerCase().endsWith('.html')) {
      const similarRelativePath = await resolveClosestHtmlRelativePath(targetDir, `${PUBLIC_ZONE_DIR}/${rest[1]}`);
      if (similarRelativePath) {
        const canonical = `/${PUBLISH_ROOT_DIR}/${encodeURIComponent(username)}/${similarRelativePath
          .split('/')
          .map((part) => encodeURIComponent(part))
          .join('/')}`;
        res.redirect(301, canonical);
        return;
      }
    }
    const recoveredPublicHtml = await recoverMisplacedPublicHtml(targetDir, resolvedRoot, rest);
    if (recoveredPublicHtml) {
      sendPublishFile(req, res, recoveredPublicHtml);
      return;
    }
    res.status(404).json({ message: '文件不存在' });
    return;
  }

  if (fs.statSync(resolvedPath).isDirectory()) {
    const indexPath = path.join(resolvedPath, 'index.html');
    if (fs.existsSync(indexPath)) {
      sendPublishFile(req, res, indexPath);
      return;
    }
    res.status(404).json({ message: '目录中没有 index.html' });
    return;
  }

  sendPublishFile(req, res, resolvedPath);
}

app.use(`/${PUBLISH_ROOT_DIR}`, async (req, res, next) => {
  await userAuthReady;
  return serveUserPublishFile(req, res, next);
});

app.use('/temp', (req, res) => {
  res.redirect(301, `/${PUBLISH_ROOT_DIR}${req.url}`);
});

// Serve user public pages: /user/<username>/path/to/file
// These are public-facing pages (like travel guides) stored in user directories
// Accessed via URL like https://goo.tkmind.cn/user/john/vietnam-guide/
app.use('/user', async (req, res, next) => {
  await userAuthReady;
  // Path format: /user/<username>/<rest...>
  const parts = req.path.split('/').filter(Boolean);
  if (parts.length < 1) return next();

  const [username, ...rest] = parts;
  const targetDir = path.join(USERS_ROOT, username);

  // Security: ensure resolved path is within the user's directory
  const filePath = path.join(targetDir, ...rest);
  const resolvedRoot = path.resolve(targetDir);
  const resolvedPath = path.resolve(filePath);

  if (!resolvedPath.startsWith(resolvedRoot + path.sep) && resolvedPath !== resolvedRoot) {
    return res.status(403).json({ message: '禁止访问' });
  }

  // Check if the user directory exists
  if (!fs.existsSync(targetDir)) {
    return res.status(404).json({ message: '用户不存在' });
  }

  if (!fs.existsSync(resolvedPath)) {
    return res.status(404).json({ message: '文件不存在' });
  }

  // If it's a directory, serve index.html
  if (fs.statSync(resolvedPath).isDirectory()) {
    const indexPath = path.join(resolvedPath, 'index.html');
    if (fs.existsSync(indexPath)) {
      return res.sendFile(indexPath);
    }
    return res.status(404).json({ message: '目录中没有 index.html' });
  }

  res.sendFile(resolvedPath, (err) => {
    if (err) res.status(404).json({ message: '文件不存在' });
  });
});

app.get(`/${PUBLISH_ROOT_DIR}/wiki/*`, (_req, res) => {
  res.sendFile(path.join(__dirname, PUBLISH_ROOT_DIR, 'wiki', 'index.html'));
});

app.get('/temp/wiki/*', (req, res) => {
  res.redirect(301, `/${PUBLISH_ROOT_DIR}/wiki${req.url.slice('/temp/wiki'.length)}`);
});

app.use(
  '/plaza-covers',
  express.static(path.join(__dirname, 'public/plaza-covers'), { maxAge: '7d' }),
);

app.get(/^\/MP_verify_[A-Za-z0-9]+\.txt$/, (req, res) => {
  const fileName = path.basename(req.path);
  const filePath = path.join(__dirname, 'public', fileName);
  if (!fs.existsSync(filePath)) return res.status(404).end();
  res.type('text/plain').sendFile(filePath);
});

app.use(express.static(path.join(__dirname, 'dist'), { index: 'index.html' }));
app.get('*', (_req, res) => {
  res.sendFile(path.join(__dirname, 'dist', 'index.html'));
});

userAuthReady.then((enabled) => {
  app.listen(PORT, '127.0.0.1', () => {
    console.log(`TKMind H5 @ http://127.0.0.1:${PORT}`);
    console.log(`Proxy -> ${API_TARGETS.join(', ')}`);
    console.log(`Auth  -> ${enabled ? 'multi-user (MySQL)' : legacyAuth ? 'legacy password' : 'disabled'}`);
    console.log(`Wiki  @ http://127.0.0.1:${PORT}/${PUBLISH_ROOT_DIR}/wiki`);
  });
});
