feat: 设置页 MCP 管理卡片(方案B)— DB 真开关 + 即时生效 + env 仅首次种子

- app_settings 表(key/value):MCP 开关与令牌存 DB,不属于任何用户、不参与同步协议
- 配置读取/写入收敛到 core/mcp.js(readMcpConfig/saveMcpToken/setMcpEnabled)
- 状态语义:未配置→404(端点视为不存在);已配置关闭→503(可重新开启);令牌错→401
- env MCP_AUTH_TOKEN 仅首次种子(库里从没有过令牌时迁入 DB),不设也行;后台改动即时生效无需重启
- 管理 API(登录态):GET /api/mcp/status(掩码)、POST /api/mcp/token(生成/重置,明文仅一次)、GET /api/mcp/token(复制用)、POST /api/mcp/enabled
- 设置页新增「AI 接入 (MCP)」卡片:开关 toggle、掩码展示、一键生成并复制(writeClipboard 兜底)、重置令牌;未登录徽章「本设备请先登录」
- 入口接线:node-server.js 与 worker.js 均挂 /mcp(seedToken 透传);sw.js 放行裸 /mcp(v2→v3)
- 两个适配器补 getSetting/setSetting(node:sqlite 与 D1 同语义)
- 测试:server/test/mcp.test.js 39 项(未配置关闭/鉴权/JSON-RPC 全链路/开关热切换/令牌轮换);npm test 全绿(107+72+39)
- 线上 E2E:生产令牌 initialize/tools 7 个/2026-09 大盘(出勤11天/82h/挣得1599);公网 HTTPS 链路通;UI puppeteer 17/17(含关→503→开→200 热切换,全程无重启)
- 文档:README + server/README(环境变量表/接口表/数据模型/MCP 章节)
This commit is contained in:
OpenClaw Agent
2026-09-13 10:24:33 +08:00
parent 9702d1949a
commit 26306eee52
14 changed files with 1006 additions and 12 deletions
+35
View File
@@ -5,6 +5,9 @@
import {
hashPassword, verifyPassword, newToken, hashToken, bearerFrom, SESSION_TTL_MS
} from './auth.js';
import {
readMcpConfig, saveMcpToken, setMcpEnabled, newMcpToken, maskToken
} from './mcp.js';
import {
BadRequest, normalizeRecord, normalizeAdjust, normalizeSettings,
recordRowToClient, adjustRowToClient, DAY_RE, MONTH_RE
@@ -234,6 +237,38 @@ export async function handleApi(request, store, opts = {}) {
}, 200, { 'content-disposition': 'attachment; filename="work-hours-backup.json"' });
}
// ---------- MCP 接入管理(登录态即可操作;单人系统无特权分级) ----------
// 开关与令牌存 DB,改动即时生效;令牌明文只在 POST /api/mcp/token 的响应里出现一次。
if (path.startsWith('/api/mcp/')) {
await requireAuth(store, request);
const sub = path.slice('/api/mcp/'.length);
if (sub === 'status' && method === 'GET') {
const cfg = await readMcpConfig(store, opts.mcpSeedToken);
return json({
ok: true, configured: cfg.configured, enabled: cfg.enabled,
masked: cfg.configured ? maskToken(cfg.token) : null
});
}
if (sub === 'token' && method === 'POST') {
const token = newMcpToken();
await saveMcpToken(store, token);
return json({ ok: true, token, enabled: true, note: '明文仅此一次展示,请立即保存' });
}
if (sub === 'token' && method === 'GET') {
const cfg = await readMcpConfig(store, opts.mcpSeedToken);
if (!cfg.configured) return err('尚未配置 MCP 令牌,请先生成', 404);
return json({ ok: true, token: cfg.token });
}
if (sub === 'enabled' && method === 'POST') {
const body = await readJson(request);
if (typeof body.enabled !== 'boolean') return err('enabled 必须是布尔值');
await setMcpEnabled(store, body.enabled);
return json({ ok: true, enabled: body.enabled });
}
return err('接口不存在: ' + path, 404);
}
return err('接口不存在: ' + path, 404);
} catch (e) {
const status = e.status || (e instanceof BadRequest ? 400 : 500);