码桶

发现社区成员的开源项目

奶狗 /

ng-webot

公开
main
ng-webot/routes/market.js
market.js27.4 KB
/**
 * 插件市场路由 — 开源单用户版
 * 提供:列表 / 一键安装(全部免费)/ 启用禁用 / 规则增删改查
 */
const express = require('express');
const router = express.Router();
const multer = require('multer');
const fs = require('fs');
const path = require('path');
const db = require('../lib/db');
const Auth = require('../lib/auth');
const Bot = require('../lib/bot');
const plugins = require('../lib/plugins');

const MARKET_DIR = path.join(__dirname, '..', 'plugins', 'market');

// 上传配置:限制 500KB,只允许 .js
const pluginUpload = multer({
  dest: path.join(require('os').tmpdir(), 'ngbot-market-uploads'),
  limits: { fileSize: 500 * 1024 },
  fileFilter: (_, file, cb) => {
    if (path.extname(file.originalname).toLowerCase() !== '.js') {
      cb(new Error('仅支持 .js 插件文件'));
    } else {
      cb(null, true);
    }
  },
});

/** 读取插件市场配置(开源版:仅保留上架/下架状态,全部免费) */
async function getMarketConfig(marketId) {
  const row = await db.row(
    'SELECT listed FROM plugin_market WHERE market_id=?',
    [marketId]
  );
  return {
    is_free: true,
    price: 0,
    access: 0,
    listed: row ? (row.listed ?? 1) : 1,
  };
}

/** 当前用户 + 校验 bot 归属 */
async function owns(req, res) {
  const u = await Auth.currentUser(req);
  if (!u) { res.status(401).json({ ok: false, msg: '未登录' }); return null; }
  const botId = parseInt(req.body.bot_id || req.query.bot_id || 0, 10);
  if (!botId) { res.status(400).json({ ok: false, msg: '缺少 bot_id' }); return null; }
  const bot = await Bot.owned(botId, u.id);
  if (!bot) { res.status(403).json({ ok: false, msg: '机器人不存在或无权访问' }); return null; }
  const user = await db.row('SELECT id FROM users WHERE id=?', [u.id]);
  return { user, bot };
}

/** 市场列表(支持搜索 + 分页) */
router.post('/list', async (req, res) => {
  const ctx = await owns(req, res); if (!ctx) return;
  const now = Math.floor(Date.now() / 1000);
  const search = (req.body.search || '').trim().toLowerCase();
  const page = Math.max(1, parseInt(req.body.page, 10) || 1);
  const pageSize = Math.min(50, Math.max(1, parseInt(req.body.pageSize, 10) || 12));
  const defs = plugins.loadAllDefinitions();
  const rows = await db.rows('SELECT market_id, enabled, expires_at FROM plugins WHERE bot_id=?', [ctx.bot.id]);
  const rowMap = {};
  rows.forEach(p => rowMap[p.market_id] = p);
  let pluginsOut = await Promise.all(defs.map(async d => {
    const cfg = await getMarketConfig(d.id);
    const row = rowMap[d.id];
    const expiresAt = row ? (row.expires_at || 0) : 0;
    // 强制安装模式:目录内所有插件默认已安装并启用,无需点击「获取」;
    // 仅当用户在 plugins 表中明确停用(enabled=0)时才视为未启用,但仍标记为已安装。
    const installed = true;
    const enabled = !row || row.enabled !== 0;
    return {
      id: d.id, name: d.name, version: d.version, author: d.author,
      category: d.category || '其他',
      description: d.description, usage: d.usage || '',
      builtin: !!d.builtin,
      configurable: d.configurable !== false,
      settingsSchema: Array.isArray(d.settingsSchema) ? d.settingsSchema : null,
      customConfig: d.customConfig || null,
      entry: d.dir + '/index.js',
      installed, enabled, expires_at: expiresAt, expired: expiresAt && expiresAt < now,
      is_free: cfg.is_free,
      access: cfg.access,
      price: cfg.price,
      listed: cfg.listed,
      force_enabled: false,
    };
  }));
  // 已下架的插件对用户不可见
  pluginsOut = pluginsOut.filter(p => p.listed !== 0);
  // 搜索过滤
  if (search) {
    pluginsOut = pluginsOut.filter(p =>
      p.name.toLowerCase().includes(search) ||
      (p.description || '').toLowerCase().includes(search) ||
      p.id.toLowerCase().includes(search)
    );
  }
  const total = pluginsOut.length;
  const totalPages = Math.ceil(total / pageSize);
  const paged = pluginsOut.slice((page - 1) * pageSize, page * pageSize);
  res.json({
    ok: true,
    plugins: paged,
    total, page, pageSize, totalPages,
  });
});

/** 插件安装(开源版:全部免费一键安装) */
router.post('/redeem', async (req, res) => {
  const ctx = await owns(req, res); if (!ctx) return;
  const defs = plugins.loadAllDefinitions();
  const def = defs.find(d => d.id === req.body.market_id);
  if (!def) return res.status(404).json({ ok: false, msg: '插件不存在' });

  const cfg = await getMarketConfig(def.id);
  if (cfg.listed === 0) return res.status(400).json({ ok: false, msg: '该插件已下架,暂不可安装' });
  const now = Math.floor(Date.now() / 1000);
  const row = await db.row('SELECT id, enabled, expires_at FROM plugins WHERE bot_id=? AND market_id=?', [ctx.bot.id, def.id]);

  if (!row) {
    await db.exec(
      'INSERT INTO plugins (bot_id, market_id, name, version, description, author, entry, enabled, installed_at, expires_at) VALUES (?,?,?,?,?,?,?,1,?,0)',
      [ctx.bot.id, def.id, def.name, def.version, def.description || '', def.author || '', def.dir + '/index.js', now]
    );
  } else {
    await db.exec('UPDATE plugins SET enabled=1, expires_at=0 WHERE id=?', [row.id]);
  }
  plugins.invalidate(ctx.bot.id);
  res.json({ ok: true, msg: '已安装,永久免费使用!' });
});

/** 启用/禁用 */
router.post('/toggle', async (req, res) => {
  const ctx = await owns(req, res); if (!ctx) return;
  const enabled = req.body.enabled ? 1 : 0;
  const defs = plugins.loadAllDefinitions();
  const def = defs.find(d => d.id === req.body.market_id);
  if (!def) return res.status(404).json({ ok: false, msg: '插件不存在' });
  const now = Math.floor(Date.now() / 1000);
  const cfg = await getMarketConfig(def.id);
  const row = await db.row('SELECT id, enabled, expires_at FROM plugins WHERE bot_id=? AND market_id=?', [ctx.bot.id, def.id]);

  if (enabled) {
    if (!row) {
      await db.exec(
        'INSERT INTO plugins (bot_id, market_id, name, version, description, author, entry, enabled, installed_at, expires_at) VALUES (?,?,?,?,?,?,?,1,?,0)',
        [ctx.bot.id, def.id, def.name, def.version, def.description || '', def.author || '', def.dir + '/index.js', now]
      );
    } else if (row.expires_at !== 0 && row.expires_at < now) {
      await db.exec('UPDATE plugins SET enabled=1, expires_at=0 WHERE id=?', [row.id]);
    } else {
      await db.exec('UPDATE plugins SET enabled=1 WHERE id=?', [row.id]);
    }
  } else {
    if (row) await db.exec('UPDATE plugins SET enabled=0 WHERE id=?', [row.id]);
  }
  plugins.invalidate(ctx.bot.id);
  res.json({ ok: true });
});

/* ---------------- 规则(关键词/指令回复) ---------------- */

router.post('/rules', async (req, res) => {
  const ctx = await owns(req, res); if (!ctx) return;
  // 内置功能(如 reply 关键词回复)无需安装记录:规则按 bot 维度统一管理
  if (plugins.BUILTIN_IDS.includes(req.body.market_id)) {
    const rules = await db.rows('SELECT id, type, match, reply, enabled FROM plugin_rules WHERE bot_id=? ORDER BY id DESC', [ctx.bot.id]);
    return res.json({ ok: true, rules });
  }
  const plugin = await db.row('SELECT id FROM plugins WHERE bot_id=? AND market_id=?', [ctx.bot.id, req.body.market_id]);
  if (!plugin) return res.status(404).json({ ok: false, msg: '插件未安装' });
  const rules = await db.rows('SELECT id, type, match, reply, enabled FROM plugin_rules WHERE bot_id=? AND plugin_id=? ORDER BY id DESC', [ctx.bot.id, plugin.id]);
  res.json({ ok: true, rules });
});

router.post('/rule_add', async (req, res) => {
  const ctx = await owns(req, res); if (!ctx) return;
  const { market_id, type, match, reply } = req.body;
  if (!['keyword', 'command'].includes(type)) return res.status(400).json({ ok: false, msg: 'type 必须为 keyword 或 command' });
  if (!match || !reply) return res.status(400).json({ ok: false, msg: 'match 和 reply 不能为空' });
  // 内置功能:无需安装记录,plugin_id 记为 0
  let pluginId = 0;
  if (!plugins.BUILTIN_IDS.includes(market_id)) {
    const plugin = await db.row('SELECT id FROM plugins WHERE bot_id=? AND market_id=?', [ctx.bot.id, market_id]);
    if (!plugin) return res.status(404).json({ ok: false, msg: '插件未安装' });
    pluginId = plugin.id;
  }
  await db.exec(
    'INSERT INTO plugin_rules (bot_id, plugin_id, type, match, reply, enabled, created_at) VALUES (?,?,?,?,?,1,?)',
    [ctx.bot.id, pluginId, type, String(match), String(reply), Math.floor(Date.now() / 1000)]
  );
  res.json({ ok: true });
});

router.post('/rule_del', async (req, res) => {
  const ctx = await owns(req, res); if (!ctx) return;
  await db.exec('DELETE FROM plugin_rules WHERE bot_id=? AND id=?', [ctx.bot.id, parseInt(req.body.rule_id, 10)]);
  res.json({ ok: true });
});

router.post('/rule_toggle', async (req, res) => {
  const ctx = await owns(req, res); if (!ctx) return;
  const enabled = req.body.enabled ? 1 : 0;
  await db.exec('UPDATE plugin_rules SET enabled=? WHERE bot_id=? AND id=?', [enabled, ctx.bot.id, parseInt(req.body.rule_id, 10)]);
  res.json({ ok: true });
});

/* ---------------- RSS 订阅源配置 ---------------- */

// 确保 plugin_rss 表存在(兼容早期库未建表的情况)
async function ensureRssTable() {
  await db.exec(`CREATE TABLE IF NOT EXISTS plugin_rss (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    bot_id INTEGER NOT NULL,
    title VARCHAR(255),
    feed_url TEXT NOT NULL,
    created_at INTEGER NOT NULL
  )`);
}

router.post('/rss_list', async (req, res) => {
  const ctx = await owns(req, res); if (!ctx) return;
  await ensureRssTable();
  const list = await db.rows('SELECT id, title, feed_url FROM plugin_rss WHERE bot_id=? ORDER BY id DESC', [ctx.bot.id]);
  res.json({ ok: true, feeds: list });
});

router.post('/rss_add', async (req, res) => {
  const ctx = await owns(req, res); if (!ctx) return;
  await ensureRssTable();
  const url = String(req.body.feed_url || '').trim();
  const title = String(req.body.title || '').trim();
  if (!/^https?:\/\//i.test(url)) {
    return res.status(400).json({ ok: false, msg: '请输入有效的 http(s) 订阅源地址' });
  }
  await db.exec(
    'INSERT INTO plugin_rss (bot_id, title, feed_url, created_at) VALUES (?,?,?,?)',
    [ctx.bot.id, title, url, Math.floor(Date.now() / 1000)]
  );
  res.json({ ok: true });
});

router.post('/rss_del', async (req, res) => {
  const ctx = await owns(req, res); if (!ctx) return;
  await db.exec('DELETE FROM plugin_rss WHERE bot_id=? AND id=?', [ctx.bot.id, parseInt(req.body.id, 10)]);
  res.json({ ok: true });
});

/* ---------------- 插件自定义设置 ---------------- */

/** 获取某插件在某机器人上的所有设置 */
router.post('/plugin_settings', async (req, res) => {
  try {
    const ctx = await owns(req, res); if (!ctx) return;
    const pluginId = String(req.body.plugin_id || '').trim();
    if (!pluginId) return res.status(400).json({ ok: false, msg: '缺少 plugin_id' });
    const rows = await db.rows(
      'SELECT config_key, config_value FROM plugin_settings WHERE bot_id=? AND plugin_id=?',
      [ctx.bot.id, pluginId]
    );
    const cfg = {};
    rows.forEach(r => { cfg[r.config_key] = r.config_value; });
    res.json({ ok: true, settings: cfg });
  } catch (err) {
    console.error('[market] plugin_settings 失败:', err.message);
    res.status(500).json({ ok: false, msg: '读取失败:' + err.message });
  }
});

/** 保存某插件在某机器人上的设置(全量覆盖) */
router.post('/plugin_settings_save', async (req, res) => {
  try {
    const ctx = await owns(req, res); if (!ctx) return;
    const pluginId = String(req.body.plugin_id || '').trim();
    if (!pluginId) return res.status(400).json({ ok: false, msg: '缺少 plugin_id' });
    const data = req.body.data;
    if (!data || typeof data !== 'object') return res.status(400).json({ ok: false, msg: '缺少 data(键值对对象)' });

    // 全量替换:删旧写新
    await db.exec('DELETE FROM plugin_settings WHERE bot_id=? AND plugin_id=?', [ctx.bot.id, pluginId]);
    const entries = Object.entries(data);
    for (const [key, value] of entries) {
      if (String(value).trim() === '') continue;
      await db.exec(
        'INSERT INTO plugin_settings (bot_id, plugin_id, config_key, config_value) VALUES (?,?,?,?)',
        [ctx.bot.id, pluginId, String(key), String(value)]
      );
    }

    // 通知插件引擎刷新缓存
    plugins.invalidate(ctx.bot.id);
    res.json({ ok: true });
  } catch (err) {
    console.error('[market] plugin_settings_save 失败:', err.message);
    res.status(500).json({ ok: false, msg: '保存失败:' + err.message });
  }
});

/* ---------------- 用户插件市场(上传售卖) ---------------- */

/** 确保 market 目录存在 */
function ensureMarketDir() {
  if (!fs.existsSync(MARKET_DIR)) fs.mkdirSync(MARKET_DIR, { recursive: true });
}

/** 上传插件(始终安装给自己用,挂售可选) */
router.post('/upload', pluginUpload.single('plugin_file'), async (req, res) => {
  try {
    const u = await Auth.currentUser(req);
    if (!u) return res.status(401).json({ ok: false, msg: '未登录' });

    const { name, description, category, price, bot_id, for_sale } = req.body;
    if (!name || !String(name).trim()) return res.status(400).json({ ok: false, msg: '请输入插件名称' });
    if (!req.file) return res.status(400).json({ ok: false, msg: '请上传插件 .js 文件' });

    // 目标机器人
    const botId = parseInt(bot_id || 0, 10);
    const bot = botId ? await Bot.owned(botId, u.id) : null;

    const wantSale = for_sale === 'true' || for_sale === '1';
    const numPrice = 0; // 开源版:全部免费分享

    // 读取代码,验证
    const code = String(fs.readFileSync(req.file.path, 'utf-8')).trim();
    if (!code) { fs.unlinkSync(req.file.path); return res.status(400).json({ ok: false, msg: '插件文件为空' }); }

    // 生成 market_id
    const safeName = String(name).trim().replace(/[^a-zA-Z0-9\u4e00-\u9fa5_-]/g, '').slice(0, 32);
    const marketId = 'mp-' + safeName + '-' + Date.now().toString(36);

    ensureMarketDir();
    const pluginDir = path.join(MARKET_DIR, marketId);
    fs.mkdirSync(pluginDir, { recursive: true });
    fs.writeFileSync(path.join(pluginDir, 'index.js'), code, 'utf-8');

    const now = Math.floor(Date.now() / 1000);
    const status = wantSale ? 'pending' : 'private';
    const kind = wantSale ? 'market' : 'self';

    await db.exec(
      `INSERT INTO marketplace_listings (seller_user_id, market_id, name, description, category, price, version, status, created_at, updated_at)
       VALUES (?,?,?,?,?,?,?,?,?,?)`,
      [u.id, marketId, String(name).trim(), String(description || '').trim(),
       String(category || '其他').trim(), numPrice, '1.0.0', status, now, now]
    );
    await db.exec("UPDATE marketplace_listings SET kind=? WHERE market_id=? AND seller_user_id=?", [kind, marketId, u.id]);

    // 始终安装到自己的机器人(如果有选)
    let installMsg = '';
    if (bot) {
      const entry = 'market/' + marketId + '/index.js';
      await db.exec(
        'INSERT INTO plugins (bot_id, market_id, name, version, description, author, entry, enabled, installed_at, expires_at) VALUES (?,?,?,?,?,?,?,1,?,0)',
        [botId, marketId, String(name).trim(), '1.0.0', String(description || '').trim(),
         u.username || '', entry, now]
      );
      plugins.invalidate(botId);
      installMsg = ',已安装到「' + (bot.name || '机器人' + botId) + '」';
    }

    // 清理临时文件
    try { fs.unlinkSync(req.file.path); } catch (_) {}

    if (wantSale) {
      res.json({ ok: true, msg: '上传成功' + installMsg + ',等待管理员审核通过后上架售卖', kind: 'market' });
    } else {
      res.json({ ok: true, msg: '上传成功' + installMsg + '(仅你自己可用)', kind: 'self' });
    }
  } catch (err) {
    console.error('[market] upload err:', err.message);
    if (req.file) try { fs.unlinkSync(req.file.path); } catch (_) {}
    res.status(500).json({ ok: false, msg: '上传失败:' + err.message });
  }
});

/** 获取主用户 ID(兼容 me 对象和直接用户) */
function userIdOf(u) { return u?.id || u?.user_id; }

/** 市场挂售列表(已审核通过的),支持搜索+分页 */
router.post('/listings', async (req, res) => {
  try {
    const u = await Auth.currentUser(req);
    const search = (req.body.search || '').trim().toLowerCase();
    const page = Math.max(1, parseInt(req.body.page, 10) || 1);
    const pageSize = Math.min(50, Math.max(1, parseInt(req.body.pageSize, 10) || 12));

    let where = `WHERE l.status = 'approved' AND l.kind = 'market'`;
    const params = [];
    if (search) {
      where += ` AND (l.name LIKE ? OR l.description LIKE ? OR l.market_id LIKE ?)`;
      const q = `%${search}%`;
      params.push(q, q, q);
    }
    // 先查总数
    const countRow = await db.row(
      `SELECT COUNT(*) AS total FROM marketplace_listings l ${where}`, params
    );
    const total = countRow?.total || 0;
    const totalPages = Math.ceil(total / pageSize);
    const offset = (page - 1) * pageSize;

    const listings = await db.rows(
      `SELECT l.*, u.username AS seller_name, u.profile_url AS seller_url
       FROM marketplace_listings l LEFT JOIN users u ON u.id = l.seller_user_id
       ${where}
       ORDER BY l.downloads DESC, l.created_at DESC
       LIMIT ${pageSize} OFFSET ${offset}`,
      params
    );
    // 标记当前用户是否已购买
    const userId = u ? userIdOf(u) : 0;
    const boughtSet = new Set();
    if (userId) {
      const bought = await db.rows('SELECT listing_id FROM purchase_records WHERE buyer_user_id=?', [userId]);
      for (const r of bought) boughtSet.add(r.listing_id);
    }
    res.json({
      ok: true,
      listings: listings.map(l => ({ ...l, bought: boughtSet.has(l.id) })),
      total, page, pageSize, totalPages,
    });
  } catch (err) {
    console.error('[market] listings err:', err.message);
    res.status(500).json({ ok: false, msg: '查询失败' });
  }
});

/** 获取挂售插件(开源版:免费获取) */
router.post('/listings/buy', async (req, res) => {
  try {
    const u = await Auth.currentUser(req);
    if (!u) return res.status(401).json({ ok: false, msg: '未登录' });

    const listingId = parseInt(req.body.listing_id, 10);
    if (!listingId) return res.status(400).json({ ok: false, msg: '缺少 listing_id' });

    const listing = await db.row('SELECT * FROM marketplace_listings WHERE id=? AND status=?', [listingId, 'approved']);
    if (!listing) return res.status(404).json({ ok: false, msg: '插件不存在或已下架' });
    if (listing.seller_user_id === u.id) return res.status(400).json({ ok: false, msg: '不能获取自己的插件' });

    // 检查是否已获取
    const bought = await db.row('SELECT id FROM purchase_records WHERE listing_id=? AND buyer_user_id=?', [listingId, u.id]);
    if (bought) return res.status(400).json({ ok: false, msg: '您已获取过此插件' });

    const now = Math.floor(Date.now() / 1000);

    // 记录获取(免费,price=0)
    await db.exec(
      'INSERT INTO purchase_records (listing_id, buyer_user_id, seller_user_id, price, created_at) VALUES (?,?,?,?,?)',
      [listingId, u.id, listing.seller_user_id, 0, now]
    );
    // 增加下载量
    await db.exec('UPDATE marketplace_listings SET downloads = downloads + 1 WHERE id=?', [listingId]);

    // 自动安装到用户选择的机器人(如果有 bot_id)
    const botId = parseInt(req.body.bot_id || 0, 10);
    if (botId) {
      const bot = await Bot.owned(botId, u.id);
      if (bot) {
        const entry = 'market/' + listing.market_id + '/index.js';
        const existing = await db.row('SELECT id FROM plugins WHERE bot_id=? AND market_id=?', [botId, listing.market_id]);
        if (!existing) {
          await db.exec(
            'INSERT INTO plugins (bot_id, market_id, name, version, description, author, entry, enabled, installed_at, expires_at) VALUES (?,?,?,?,?,?,?,1,?,0)',
            [botId, listing.market_id, listing.name, listing.version, listing.description || '',
             listing.seller_name || '', entry, now]
          );
        } else {
          await db.exec('UPDATE plugins SET enabled=1, expires_at=0 WHERE id=?', [existing.id]);
        }
        plugins.invalidate(botId);
        return res.json({ ok: true, msg: '获取成功!插件已安装到当前机器人。' });
      }
    }

    res.json({ ok: true, msg: '获取成功!' });
  } catch (err) {
    console.error('[market] buy err:', err.message);
    res.status(500).json({ ok: false, msg: '获取失败:' + err.message });
  }
});

// 按 kind 查询某用户的插件列表(self=自用 / market=挂售)
async function loadUserListings(uid, kind, search, page, pageSize) {
  let where = 'WHERE seller_user_id=? AND kind=?';
  const params = [uid, kind];
  if (search) {
    where += ' AND (name LIKE ? OR description LIKE ? OR market_id LIKE ?)';
    const q = `%${search}%`;
    params.push(q, q, q);
  }
  const countRow = await db.row(`SELECT COUNT(*) AS total FROM marketplace_listings ${where}`, params);
  const total = countRow?.total || 0;
  const totalPages = Math.ceil(total / pageSize);
  const offset = (page - 1) * pageSize;
  const listings = await db.rows(
    `SELECT * FROM marketplace_listings ${where} ORDER BY created_at DESC LIMIT ${pageSize} OFFSET ${offset}`, params
  );
  return { listings, total, totalPages };
}

/** 我的插件列表(含自用+挂售) + 销量/收入,支持搜索+分页 */
router.post('/my-plugins', async (req, res) => {
  try {
    const u = await Auth.currentUser(req);
    if (!u) return res.status(401).json({ ok: false, msg: '未登录' });

    const search = (req.body.search || '').trim().toLowerCase();
    const page = Math.max(1, parseInt(req.body.page, 10) || 1);
    const pageSize = Math.min(50, Math.max(1, parseInt(req.body.pageSize, 10) || 12));

    let where = 'WHERE seller_user_id=? AND kind=?';
    const params = [u.id, 'market'];
    if (search) {
      where += ' AND (name LIKE ? OR description LIKE ? OR market_id LIKE ?)';
      const q = `%${search}%`;
      params.push(q, q, q);
    }
    const countRow = await db.row(
      `SELECT COUNT(*) AS total FROM marketplace_listings ${where}`, params
    );
    const total = countRow?.total || 0;
    const totalPages = Math.ceil(total / pageSize);
    const offset = (page - 1) * pageSize;

    const listings = await db.rows(
      `SELECT * FROM marketplace_listings ${where} ORDER BY created_at DESC LIMIT ${pageSize} OFFSET ${offset}`,
      params
    );
    // 查询销量和收入
    const earnings = await db.row(
      'SELECT COUNT(*) AS total_sales, COALESCE(SUM(price), 0) AS total_earnings FROM purchase_records WHERE seller_user_id=?',
      [u.id]
    );
    res.json({
      ok: true,
      listings,
      total, page, pageSize, totalPages,
      total_sales: earnings?.total_sales || 0,
      total_earnings: earnings?.total_earnings || 0,
    });
  } catch (err) {
    console.error('[market] my-plugins err:', err.message);
    res.status(500).json({ ok: false, msg: '查询失败' });
  }
});

/** 旧接口兼容 */
router.post('/my-listings', async (req, res) => {
  req.url = '/my-plugins';
  return router.handle(req, res);
});

/** 我的自用插件列表(kind='self',独立于挂售市场) */
router.post('/my-self-plugins', async (req, res) => {
  try {
    const u = await Auth.currentUser(req);
    if (!u) return res.status(401).json({ ok: false, msg: '未登录' });
    const search = (req.body.search || '').trim().toLowerCase();
    const page = Math.max(1, parseInt(req.body.page, 10) || 1);
    const pageSize = Math.min(50, Math.max(1, parseInt(req.body.pageSize, 10) || 12));
    const { listings, total, totalPages } = await loadUserListings(u.id, 'self', search, page, pageSize);
    res.json({ ok: true, listings, total, page, pageSize, totalPages, total_sales: 0, total_earnings: 0 });
  } catch (err) {
    console.error('[market] my-self-plugins err:', err.message);
    res.status(500).json({ ok: false, msg: '查询失败' });
  }
});

/** 切换挂售状态(开启/关闭) */
router.post('/listing-toggle', async (req, res) => {
  try {
    const u = await Auth.currentUser(req);
    if (!u) return res.status(401).json({ ok: false, msg: '未登录' });

    const listingId = parseInt(req.body.listing_id, 10);
    const listing = await db.row("SELECT * FROM marketplace_listings WHERE id=? AND seller_user_id=? AND kind='market'", [listingId, u.id]);
    if (!listing) return res.status(404).json({ ok: false, msg: '挂售记录不存在' });

    const newStatus = listing.status === 'approved' ? 'paused' : 'approved';
    await db.exec('UPDATE marketplace_listings SET status=?, updated_at=? WHERE id=?',
      [newStatus, Math.floor(Date.now() / 1000), listingId]);
    res.json({ ok: true, status: newStatus, msg: newStatus === 'approved' ? '已上架' : '已下架' });
  } catch (err) {
    res.status(500).json({ ok: false, msg: '操作失败' });
  }
});

/** 将自用插件转为挂售(private → pending,需审核) */
router.post('/listing-to-sale', async (req, res) => {
  try {
    const u = await Auth.currentUser(req);
    if (!u) return res.status(401).json({ ok: false, msg: '未登录' });

    const listingId = parseInt(req.body.listing_id, 10);
    const price = 0; // 开源版:全部免费分享
    const category = String(req.body.category || '其他').trim();

    const listing = await db.row('SELECT * FROM marketplace_listings WHERE id=? AND seller_user_id=?', [listingId, u.id]);
    if (!listing) return res.status(404).json({ ok: false, msg: '插件不存在' });
    if (listing.status !== 'private' && listing.status !== 'rejected') {
      return res.status(400).json({ ok: false, msg: '当前状态不可转为挂售' });
    }

    await db.exec("UPDATE marketplace_listings SET status=?, price=?, category=?, kind=?, updated_at=? WHERE id=?",
      ['pending', price, category, 'market', Math.floor(Date.now() / 1000), listingId]);
    res.json({ ok: true, msg: '已提交审核,通过后即可上架售卖' });
  } catch (err) {
    res.status(500).json({ ok: false, msg: '操作失败:' + err.message });
  }
});

/** 删除挂售 */
router.post('/listing-remove', async (req, res) => {
  try {
    const u = await Auth.currentUser(req);
    if (!u) return res.status(401).json({ ok: false, msg: '未登录' });

    const listingId = parseInt(req.body.listing_id, 10);
    const listing = await db.row('SELECT * FROM marketplace_listings WHERE id=? AND seller_user_id=?', [listingId, u.id]);
    if (!listing) return res.status(404).json({ ok: false, msg: '挂售记录不存在' });

    await db.exec('DELETE FROM marketplace_listings WHERE id=?', [listingId]);
    // 清理文件
    const dir = path.join(MARKET_DIR, listing.market_id);
    if (fs.existsSync(dir)) {
      try { fs.rmSync(dir, { recursive: true, force: true }); } catch (_) {}
    }
    res.json({ ok: true, msg: '已删除' });
  } catch (err) {
    res.status(500).json({ ok: false, msg: '删除失败' });
  }
});

module.exports = router;