feat: MCP 端点鉴权改用后台 Token 管理,废弃 mcp_kv 配置
build-and-deploy / build-and-deploy (push) Successful in 42s
build-and-deploy / build-and-deploy (push) Successful in 42s
- tokenService 支持 mcpEnabled 标记,新增 getMcpTokenValues 与迁移逻辑 - /mcp 鉴权改为校验 Token 表中标记为 MCP 访问的生效令牌 - 启动时自动把旧 mcp_token 配置迁成一条 MCP Token,保证旧客户端不中断 - admin 页面 Token 表新增 MCP 访问列与生成/编辑开关 - 新增 tokenService 测试(58 项全部通过)
This commit is contained in:
+16
-3
@@ -117,7 +117,7 @@
|
|||||||
<table class="data-table" id="token-table">
|
<table class="data-table" id="token-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>名称</th><th>关联项目</th><th>权限</th><th>Token</th>
|
<th>名称</th><th>关联项目</th><th>权限</th><th>MCP 访问</th><th>Token</th>
|
||||||
<th>状态</th><th>过期时间</th><th>创建时间</th><th>操作</th>
|
<th>状态</th><th>过期时间</th><th>创建时间</th><th>操作</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -229,6 +229,12 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="checkbox-label" style="display:flex;align-items:center;gap:8px;cursor:pointer">
|
||||||
|
<input type="checkbox" id="input-token-mcp" style="width:16px;height:16px">
|
||||||
|
<span>作为 MCP 访问令牌(用于 /mcp 端点鉴权)</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
<p class="hint">生成后 Token 明文仅展示一次,请及时复制并妥善保管。</p>
|
<p class="hint">生成后 Token 明文仅展示一次,请及时复制并妥善保管。</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
@@ -486,7 +492,7 @@
|
|||||||
function renderTokens(tokens) {
|
function renderTokens(tokens) {
|
||||||
var tbody = document.getElementById("token-tbody");
|
var tbody = document.getElementById("token-tbody");
|
||||||
if (!tokens.length) {
|
if (!tokens.length) {
|
||||||
tbody.innerHTML = '<tr><td colspan="8" style="text-align:center;color:#999;padding:30px">暂无 Token</td></tr>';
|
tbody.innerHTML = '<tr><td colspan="9" style="text-align:center;color:#999;padding:30px">暂无 Token</td></tr>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
tbody.innerHTML = tokens.map(function (t) {
|
tbody.innerHTML = tokens.map(function (t) {
|
||||||
@@ -499,6 +505,9 @@
|
|||||||
"<td>" + esc(t.name) + "</td>" +
|
"<td>" + esc(t.name) + "</td>" +
|
||||||
"<td>" + esc(t.projectId === "all" ? "全部项目" : t.projectId) + "</td>" +
|
"<td>" + esc(t.projectId === "all" ? "全部项目" : t.projectId) + "</td>" +
|
||||||
'<td><span class="dim-chip">' + (t.permission === "readwrite" ? "读写" : "只读") + "</span></td>" +
|
'<td><span class="dim-chip">' + (t.permission === "readwrite" ? "读写" : "只读") + "</span></td>" +
|
||||||
|
'<td>' + (t.mcpEnabled
|
||||||
|
? '<span class="dim-chip" style="color:#0a7a3c;border-color:#0a7a3c">MCP</span>'
|
||||||
|
: '<span style="color:#aaa">—</span>') + "</td>" +
|
||||||
'<td><div class="token-reveal">' +
|
'<td><div class="token-reveal">' +
|
||||||
'<span class="token-value token-mask" data-id="' + t.id + '">' + masked + "</span>" +
|
'<span class="token-value token-mask" data-id="' + t.id + '">' + masked + "</span>" +
|
||||||
'<button class="token-btn" onclick="toggleTokenVis(this)">显示</button>' +
|
'<button class="token-btn" onclick="toggleTokenVis(this)">显示</button>' +
|
||||||
@@ -508,7 +517,7 @@
|
|||||||
"<td>" + fmtDate(t.expiresAt) + "</td>" +
|
"<td>" + fmtDate(t.expiresAt) + "</td>" +
|
||||||
"<td>" + fmtDate(t.createdAt) + "</td>" +
|
"<td>" + fmtDate(t.createdAt) + "</td>" +
|
||||||
'<td style="white-space:nowrap">' +
|
'<td style="white-space:nowrap">' +
|
||||||
'<button class="row-btn" onclick=\'editToken(' + JSON.stringify({ id: t.id, name: t.name, projectId: t.projectId, permission: t.permission }).replace(/'/g, "'") + ")'>编辑</button> " +
|
'<button class="row-btn" onclick=\'editToken(' + JSON.stringify({ id: t.id, name: t.name, projectId: t.projectId, permission: t.permission, mcpEnabled: !!t.mcpEnabled }).replace(/'/g, "'") + ")'>编辑</button> " +
|
||||||
(t.enabled
|
(t.enabled
|
||||||
? '<button class="row-btn danger" onclick="revokeToken(\'' + t.id + "')\">吊销</button>"
|
? '<button class="row-btn danger" onclick="revokeToken(\'' + t.id + "')\">吊销</button>"
|
||||||
: "") +
|
: "") +
|
||||||
@@ -559,6 +568,7 @@
|
|||||||
document.getElementById("input-token-project").value = "all";
|
document.getElementById("input-token-project").value = "all";
|
||||||
document.getElementById("input-token-permission").value = "readonly";
|
document.getElementById("input-token-permission").value = "readonly";
|
||||||
document.getElementById("input-token-expiry").value = "90";
|
document.getElementById("input-token-expiry").value = "90";
|
||||||
|
document.getElementById("input-token-mcp").checked = false;
|
||||||
document.getElementById("btn-save-token").textContent = "生成";
|
document.getElementById("btn-save-token").textContent = "生成";
|
||||||
loadProjectsForSelect();
|
loadProjectsForSelect();
|
||||||
openModal("modal-token");
|
openModal("modal-token");
|
||||||
@@ -570,6 +580,7 @@
|
|||||||
document.getElementById("input-token-name").value = t.name;
|
document.getElementById("input-token-name").value = t.name;
|
||||||
document.getElementById("input-token-project").value = t.projectId;
|
document.getElementById("input-token-project").value = t.projectId;
|
||||||
document.getElementById("input-token-permission").value = t.permission;
|
document.getElementById("input-token-permission").value = t.permission;
|
||||||
|
document.getElementById("input-token-mcp").checked = !!(t.mcpEnabled);
|
||||||
document.getElementById("btn-save-token").textContent = "保存";
|
document.getElementById("btn-save-token").textContent = "保存";
|
||||||
loadProjectsForSelect();
|
loadProjectsForSelect();
|
||||||
openModal("modal-token");
|
openModal("modal-token");
|
||||||
@@ -585,6 +596,7 @@
|
|||||||
name: name,
|
name: name,
|
||||||
projectId: document.getElementById("input-token-project").value,
|
projectId: document.getElementById("input-token-project").value,
|
||||||
permission: document.getElementById("input-token-permission").value,
|
permission: document.getElementById("input-token-permission").value,
|
||||||
|
mcpEnabled: document.getElementById("input-token-mcp").checked,
|
||||||
});
|
});
|
||||||
closeModal("modal-token");
|
closeModal("modal-token");
|
||||||
toast("Token 已更新", "success");
|
toast("Token 已更新", "success");
|
||||||
@@ -594,6 +606,7 @@
|
|||||||
name: name,
|
name: name,
|
||||||
projectId: document.getElementById("input-token-project").value,
|
projectId: document.getElementById("input-token-project").value,
|
||||||
permission: document.getElementById("input-token-permission").value,
|
permission: document.getElementById("input-token-permission").value,
|
||||||
|
mcpEnabled: document.getElementById("input-token-mcp").checked,
|
||||||
expiresInDays: parseInt(document.getElementById("input-token-expiry").value) || 0,
|
expiresInDays: parseInt(document.getElementById("input-token-expiry").value) || 0,
|
||||||
});
|
});
|
||||||
closeModal("modal-token");
|
closeModal("modal-token");
|
||||||
|
|||||||
+19
-13
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* MCP HTTP 传输层
|
* MCP HTTP 传输层
|
||||||
* 以 Streamable HTTP 协议在 /mcp 端点暴露 MCP 服务。
|
* 以 Streamable HTTP 协议在 /mcp 端点暴露 MCP 服务。
|
||||||
* 鉴权:基于 data/mcp.json 中配置的 token,要求请求头 Authorization: Bearer <token>。
|
* 鉴权:基于后台 Token 管理中标记为「MCP 访问」的令牌,要求请求头 Authorization: Bearer <token>。
|
||||||
*/
|
*/
|
||||||
const crypto = require("crypto");
|
const crypto = require("crypto");
|
||||||
const express = require("express");
|
const express = require("express");
|
||||||
@@ -11,7 +11,7 @@ const {
|
|||||||
} = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
} = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
||||||
const { isInitializeRequest } = require("@modelcontextprotocol/sdk/types.js");
|
const { isInitializeRequest } = require("@modelcontextprotocol/sdk/types.js");
|
||||||
const { createMcpServer } = require("./tools");
|
const { createMcpServer } = require("./tools");
|
||||||
const { loadMcpConfig } = require("../persistence/duckdbAdapter");
|
const { getMcpTokenValues } = require("../services/tokenService");
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
@@ -19,25 +19,31 @@ const router = express.Router();
|
|||||||
const transports = new Map();
|
const transports = new Map();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 校验请求携带的 Bearer token 是否与配置中的 token 匹配
|
* 校验请求携带的 Bearer token 是否命中任意一个生效的 MCP 访问令牌
|
||||||
* @param {string} [header] - Authorization 请求头
|
* @param {string} [header] - Authorization 请求头
|
||||||
* @returns {Promise<boolean>} 是否鉴权通过
|
* @returns {Promise<boolean>} 是否鉴权通过
|
||||||
*/
|
*/
|
||||||
async function isAuthorized(header) {
|
async function isAuthorized(header) {
|
||||||
const mcp = await loadMcpConfig();
|
const provided = header && header.startsWith("Bearer ") ? header.slice(7).trim() : "";
|
||||||
// 未配置 token 时视为未启用鉴权
|
const expectedList = await getMcpTokenValues();
|
||||||
if (!mcp.token) {
|
// 未配置 MCP 令牌时视为未启用鉴权
|
||||||
|
if (expectedList.length === 0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const expected = mcp.token;
|
if (!provided) {
|
||||||
const provided = header && header.startsWith("Bearer ") ? header.slice(7).trim() : "";
|
|
||||||
if (!provided || provided.length !== expected.length) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// 使用常量时间比较,避免时序侧信道
|
// 与任一生效令牌做常量时间比较,避免时序侧信道
|
||||||
const a = Buffer.from(provided);
|
for (const expected of expectedList) {
|
||||||
const b = Buffer.from(expected);
|
if (provided.length === expected.length) {
|
||||||
return crypto.timingSafeEqual(a, b);
|
const a = Buffer.from(provided);
|
||||||
|
const b = Buffer.from(expected);
|
||||||
|
if (crypto.timingSafeEqual(a, b)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+4
-1
@@ -12,6 +12,7 @@ const tokenRoutes = require("./routes/tokens");
|
|||||||
const { loadSystem, saveSystem } = require("./persistence/duckdbAdapter");
|
const { loadSystem, saveSystem } = require("./persistence/duckdbAdapter");
|
||||||
const duckdbAdapter = require("./persistence/duckdbAdapter");
|
const duckdbAdapter = require("./persistence/duckdbAdapter");
|
||||||
const userService = require("./services/userService");
|
const userService = require("./services/userService");
|
||||||
|
const tokenService = require("./services/tokenService");
|
||||||
const versionInfo = require("./version");
|
const versionInfo = require("./version");
|
||||||
|
|
||||||
// MCP 服务(Model Context Protocol,供 AI 自动操作)
|
// MCP 服务(Model Context Protocol,供 AI 自动操作)
|
||||||
@@ -95,10 +96,12 @@ app.put("/api/mode", async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 启动服务:先初始化 DuckDB(自动迁移 JSON 存量数据),再创建默认管理员账号
|
* 启动服务:先初始化 DuckDB(自动迁移 JSON 存量数据),
|
||||||
|
* 迁移历史 MCP token 至 Token 表,再创建默认管理员账号
|
||||||
*/
|
*/
|
||||||
async function start() {
|
async function start() {
|
||||||
await duckdbAdapter.init();
|
await duckdbAdapter.init();
|
||||||
|
await tokenService.migrateLegacyMcpToken();
|
||||||
await userService.ensureDefaultAdmin();
|
await userService.ensureDefaultAdmin();
|
||||||
app.listen(PORT, () => {
|
app.listen(PORT, () => {
|
||||||
console.log(`Registry 服务已启动: http://localhost:${PORT}`);
|
console.log(`Registry 服务已启动: http://localhost:${PORT}`);
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
/**
|
/**
|
||||||
* API Token 服务
|
* API Token 服务
|
||||||
* 负责 API 访问令牌的增删改查与吊销,持久化到 data/api-tokens.json
|
* 负责 API 访问令牌的增删改查与吊销,持久化到 data/api-tokens.json
|
||||||
* (与 MCP token 独立,MCP token 由 data/mcp.json 管理)
|
* 支持 mcpEnabled 标记:标记后该 token 用于 MCP /mcp 端点鉴权(取代旧的 mcp.json / mcp_kv)
|
||||||
*/
|
*/
|
||||||
const { loadApiTokens, saveApiTokens } = require("../persistence/duckdbAdapter");
|
const { loadApiTokens, saveApiTokens, loadMcpConfig, saveMcpConfig } = require("../persistence/duckdbAdapter");
|
||||||
const { generateToken } = require("./userService");
|
const { generateToken } = require("./userService");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -25,7 +25,7 @@ async function findById(id) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建 Token
|
* 创建 Token
|
||||||
* @param {Object} data - { name, projectId, permission, expiresInDays }
|
* @param {Object} data - { name, projectId, permission, mcpEnabled, expiresInDays }
|
||||||
* @returns {Promise<Object>} 创建的 Token(含明文 value)
|
* @returns {Promise<Object>} 创建的 Token(含明文 value)
|
||||||
*/
|
*/
|
||||||
async function createToken(data) {
|
async function createToken(data) {
|
||||||
@@ -44,6 +44,7 @@ async function createToken(data) {
|
|||||||
name: data.name.trim(),
|
name: data.name.trim(),
|
||||||
projectId: data.projectId || "all",
|
projectId: data.projectId || "all",
|
||||||
permission: data.permission === "readwrite" ? "readwrite" : "readonly",
|
permission: data.permission === "readwrite" ? "readwrite" : "readonly",
|
||||||
|
mcpEnabled: !!data.mcpEnabled,
|
||||||
value: tokenValue,
|
value: tokenValue,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
createdAt: now.toISOString(),
|
createdAt: now.toISOString(),
|
||||||
@@ -57,7 +58,7 @@ async function createToken(data) {
|
|||||||
/**
|
/**
|
||||||
* 更新 Token 元信息
|
* 更新 Token 元信息
|
||||||
* @param {string} id
|
* @param {string} id
|
||||||
* @param {Object} data - { name, projectId, permission, enabled }
|
* @param {Object} data - { name, projectId, permission, mcpEnabled, enabled }
|
||||||
* @returns {Promise<Object>}
|
* @returns {Promise<Object>}
|
||||||
*/
|
*/
|
||||||
async function updateToken(id, data) {
|
async function updateToken(id, data) {
|
||||||
@@ -67,6 +68,7 @@ async function updateToken(id, data) {
|
|||||||
if (data.name !== undefined) tokens[idx].name = data.name.trim();
|
if (data.name !== undefined) tokens[idx].name = data.name.trim();
|
||||||
if (data.projectId !== undefined) tokens[idx].projectId = data.projectId;
|
if (data.projectId !== undefined) tokens[idx].projectId = data.projectId;
|
||||||
if (data.permission !== undefined) tokens[idx].permission = data.permission;
|
if (data.permission !== undefined) tokens[idx].permission = data.permission;
|
||||||
|
if (data.mcpEnabled !== undefined) tokens[idx].mcpEnabled = !!data.mcpEnabled;
|
||||||
if (data.enabled !== undefined) tokens[idx].enabled = !!data.enabled;
|
if (data.enabled !== undefined) tokens[idx].enabled = !!data.enabled;
|
||||||
await saveApiTokens(tokens);
|
await saveApiTokens(tokens);
|
||||||
return tokens[idx];
|
return tokens[idx];
|
||||||
@@ -113,6 +115,48 @@ async function validateToken(value) {
|
|||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取启用了 MCP 访问的生效 Token 值列表(用于 /mcp 端点鉴权)
|
||||||
|
* @returns {Promise<string[]>} 生效的 MCP token 明文值
|
||||||
|
*/
|
||||||
|
async function getMcpTokenValues() {
|
||||||
|
const now = Date.now();
|
||||||
|
return (await loadApiTokens())
|
||||||
|
.filter((t) => t.mcpEnabled && t.enabled && (!t.expiresAt || new Date(t.expiresAt).getTime() > now))
|
||||||
|
.map((t) => t.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 一次性迁移历史 MCP 鉴权配置(旧 mcp.json / mcp_kv)到 Token 表
|
||||||
|
* 兼容线上已有用旧 token 连接的 MCP 客户端:把旧 token 转为一条启用 mcpEnabled 的 Token,并清除旧配置
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async function migrateLegacyMcpToken() {
|
||||||
|
const tokens = await loadApiTokens();
|
||||||
|
if (tokens.some((t) => t.mcpEnabled)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const mcp = await loadMcpConfig();
|
||||||
|
if (!mcp || !mcp.token) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const now = new Date();
|
||||||
|
tokens.push({
|
||||||
|
id: generateToken(8),
|
||||||
|
name: "MCP(历史迁移)",
|
||||||
|
projectId: "all",
|
||||||
|
permission: "readwrite",
|
||||||
|
mcpEnabled: true,
|
||||||
|
value: mcp.token,
|
||||||
|
enabled: true,
|
||||||
|
createdAt: now.toISOString(),
|
||||||
|
expiresAt: null,
|
||||||
|
});
|
||||||
|
await saveApiTokens(tokens);
|
||||||
|
await saveMcpConfig({ enabled: false, token: "" });
|
||||||
|
console.log("[tokenService] 已迁移历史 MCP token 至 Token 表,旧 mcp_kv 配置已清空");
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getAllTokens,
|
getAllTokens,
|
||||||
findById,
|
findById,
|
||||||
@@ -121,4 +165,6 @@ module.exports = {
|
|||||||
revokeToken,
|
revokeToken,
|
||||||
deleteToken,
|
deleteToken,
|
||||||
validateToken,
|
validateToken,
|
||||||
|
getMcpTokenValues,
|
||||||
|
migrateLegacyMcpToken,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
/**
|
||||||
|
* Token 服务单元测试
|
||||||
|
* 覆盖:mcpEnabled 标记、MCP token 列表过滤、历史 MCP 配置迁移
|
||||||
|
* 运行:npm test
|
||||||
|
*/
|
||||||
|
const test = require("node:test");
|
||||||
|
const assert = require("node:assert");
|
||||||
|
const fs = require("node:fs");
|
||||||
|
const os = require("node:os");
|
||||||
|
const path = require("node:path");
|
||||||
|
|
||||||
|
// 使用临时数据目录,避免污染真实 data/
|
||||||
|
const tmpDataDir = fs.mkdtempSync(path.join(os.tmpdir(), "registry-token-test-"));
|
||||||
|
process.env.REGISTRY_DATA_DIR = tmpDataDir;
|
||||||
|
|
||||||
|
const { init } = require("../src/persistence/duckdbAdapter");
|
||||||
|
const tokenService = require("../src/services/tokenService");
|
||||||
|
|
||||||
|
test.before(async () => {
|
||||||
|
await init();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("createToken 保存 mcpEnabled 标记", async () => {
|
||||||
|
const mcpToken = await tokenService.createToken({
|
||||||
|
name: "mcp-key",
|
||||||
|
projectId: "all",
|
||||||
|
permission: "readwrite",
|
||||||
|
mcpEnabled: true,
|
||||||
|
expiresInDays: 30,
|
||||||
|
});
|
||||||
|
assert.strictEqual(mcpToken.mcpEnabled, true);
|
||||||
|
const plainToken = await tokenService.createToken({
|
||||||
|
name: "plain",
|
||||||
|
mcpEnabled: false,
|
||||||
|
});
|
||||||
|
assert.strictEqual(plainToken.mcpEnabled, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("getMcpTokenValues 仅返回生效的 MCP token 明文", async () => {
|
||||||
|
const values = await tokenService.getMcpTokenValues();
|
||||||
|
assert.ok(values.length >= 1);
|
||||||
|
// 明文值以 reg_ 开头
|
||||||
|
assert.ok(values.every((v) => typeof v === "string" && v.startsWith("reg_")));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("updateToken 可切换 mcpEnabled", async () => {
|
||||||
|
const plain = await tokenService.createToken({ name: "toggle", mcpEnabled: false });
|
||||||
|
const before = await tokenService.getMcpTokenValues();
|
||||||
|
assert.ok(!before.includes(plain.value));
|
||||||
|
await tokenService.updateToken(plain.id, { mcpEnabled: true });
|
||||||
|
const after = await tokenService.getMcpTokenValues();
|
||||||
|
assert.ok(after.includes(plain.value));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("吊销或过期后 MCP token 不再生效", async () => {
|
||||||
|
const t = await tokenService.createToken({ name: "will-revoke", mcpEnabled: true, expiresInDays: 30 });
|
||||||
|
const ok = await tokenService.getMcpTokenValues();
|
||||||
|
assert.ok(ok.includes(t.value));
|
||||||
|
await tokenService.revokeToken(t.id);
|
||||||
|
const revoked = await tokenService.getMcpTokenValues();
|
||||||
|
assert.ok(!revoked.includes(t.value));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("migrateLegacyMcpToken 把历史 mcp_kv 配置迁移为 Token 记录", async () => {
|
||||||
|
// 清理当前 token 表,模拟空 MCP 标记状态
|
||||||
|
const { loadApiTokens, saveApiTokens, saveMcpConfig, loadMcpConfig } = require("../src/persistence/duckdbAdapter");
|
||||||
|
await saveApiTokens([]);
|
||||||
|
await saveMcpConfig({ enabled: true, token: "legacy-mcp-token-abc" });
|
||||||
|
await tokenService.migrateLegacyMcpToken();
|
||||||
|
|
||||||
|
const tokens = await loadApiTokens();
|
||||||
|
const migrated = tokens.find((t) => t.mcpEnabled);
|
||||||
|
assert.ok(migrated, "应生成一条 mcpEnabled 的 Token");
|
||||||
|
assert.strictEqual(migrated.value, "legacy-mcp-token-abc");
|
||||||
|
const values = await tokenService.getMcpTokenValues();
|
||||||
|
assert.ok(values.includes("legacy-mcp-token-abc"));
|
||||||
|
// 旧配置已清空
|
||||||
|
const mcp = await loadMcpConfig();
|
||||||
|
assert.strictEqual(mcp.token, "");
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user