码桶
发现社区成员的开源项目
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;