Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -131,5 +131,5 @@ jobs:
node-version: '22'
cache: npm
- run: npm ci
- name: Live MySQL concurrent upload and memory admission
run: node --test --test-concurrency=1 tests/uploadAdmission.mysql.integration.test.js tests/userMemoryAdmission.mysql.integration.test.js
- name: Live MySQL persistence concurrency acceptance
run: node --test --test-concurrency=1 tests/uploadAdmission.mysql.integration.test.js tests/userMemoryAdmission.mysql.integration.test.js tests/dataSourceTestAtomic.mysql.integration.test.js
17 changes: 10 additions & 7 deletions config/size-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@
"src/datasourceCrypto.js": 55,
"src/datasourceLifecycle.js": 15,
"src/datasourcePool.js": 24,
"src/dataSourceRepository.js": 184,
"src/dataSourceRepository.js": 180,
"src/derivedMetrics.js": 391,
"src/dialects/common.js": 25,
"src/dialects/index.js": 27,
Expand Down Expand Up @@ -129,7 +129,7 @@
"src/localIndicatorValidation.js": 38,
"src/mcp/audit.js": 103,
"src/mcp/auth.js": 54,
"src/mcp/config.js": 73,
"src/mcp/config.js": 57,
"src/mcp/connectorPackage.js": 115,
"src/mcp/index.js": 93,
"src/mcp/modeGate.js": 46,
Expand Down Expand Up @@ -160,7 +160,7 @@
"src/platformDatabase.js": 61,
"src/platformLogRepository.js": 215,
"src/presentationFallbacks.js": 63,
"src/processArtifacts.js": 173,
"src/processArtifacts.js": 89,
"src/promptSanitizer.js": 123,
"src/queryContract.js": 358,
"src/queryContractCompiler.js": 2285,
Expand Down Expand Up @@ -203,7 +203,7 @@
"src/turnRouter.js": 143,
"src/uploads.js": 505,
"src/uploadAdmission.js": 94,
"src/userMemory.js": 642,
"src/userMemory.js": 637,
"src/userMemoryDistiller.js": 499,
"src/userMemoryMaintenance.js": 156,
"src/userMemoryModel.js": 138,
Expand All @@ -212,11 +212,14 @@
"src/userRepository.js": 141,
"src/valueClarification.js": 186,
"src/workflow.js": 230,
"src/workspace.js": 464,
"src/workspaceTransforms.js": 189,
"src/workspace.js": 566,
"src/workspaceRepository.js": 287,
"src/xlsx.js": 369,
"src/xlsxReader.js": 480,
"src/platformAsyncLogRepository.js": 168
"src/skillAudits.js": 119
"src/skillAudits.js": 119,
"src/platformAsyncLogRepository.js": 168,
"src/dataSourceRow.js": 25,
"src/mcp/configValues.js": 44,
"src/processArtifactDraft.js": 119
}
46 changes: 11 additions & 35 deletions src/dataSourceRepository.js
Original file line number Diff line number Diff line change
@@ -1,44 +1,22 @@
import { mapRow } from './dataSourceRow.js';
import { normalizeDataSource } from './dialects/index.js';
import { dataSourceTestState } from './datasourceLifecycle.js';

function nowIso() {
return new Date().toISOString();
}

function parseJson(value, fallback) {
if (value === null || value === undefined || value === '') {
return fallback;
}
try {
return JSON.parse(value);
} catch {
return fallback;
}
}

function asJson(value) {
return JSON.stringify(value ?? {});
}

function asBool(value) {
return value === 1 || value === true;
}

export class DataSourceRepository {
constructor(db, asyncStore = db) {
this.db = db;
this.asyncStore = asyncStore;
}

mapRow(row) {
return row ? {
...row,
options: parseJson(row.optionsJson, {}),
lastTestOk: row.lastTestOk === null || row.lastTestOk === undefined
? null
: asBool(row.lastTestOk),
} : null;
}
mapRow(row) { return mapRow(row); }

async listDataSourcesAsync() {
const rows = await this.asyncStore.query(`SELECT id, code, name, db_type AS dbType,
Expand Down Expand Up @@ -84,17 +62,15 @@ export class DataSourceRepository {
async updateDataSourceTestAsync(id, { ok, message }, source = null) {
const current = source ?? await this.getDataSourceAsync(id);
if (!current) return null;
const unchanged = (await this.asyncStore.query(`SELECT COUNT(*) AS count FROM data_sources
WHERE id=? AND code=? AND db_type=? AND host=? AND port=? AND database_name=?
AND username=? AND encrypted_password=? AND options_json=?`, [
Number(id), current.code, current.dbType, current.host, Number(current.port),
current.databaseName, current.username, current.encryptedPassword ?? '',
JSON.stringify(current.options ?? {}),
]))[0]?.count;
if (Number(unchanged) > 0) {
await this.asyncStore.execute(`UPDATE data_sources SET last_test_at=?, last_test_ok=?,
last_test_message=? WHERE id=?`, [nowIso(), ok ? 1 : 0, String(message ?? ''), Number(id)]);
}
const fields = ['code', 'db_type', 'host', 'database_name', 'username', 'encrypted_password', 'options_json'];
const match = fields.map(field => this.asyncStore.backend === 'mysql'
? `BINARY ${field} = BINARY ?` : `${field} = ?`).join(' AND ');
await this.asyncStore.execute(`UPDATE data_sources SET last_test_at=?, last_test_ok=?,
last_test_message=? WHERE id=? AND port=? AND ${match}`, [
nowIso(), ok ? 1 : 0, String(message ?? ''), Number(id), Number(current.port),
current.code, current.dbType, current.host, current.databaseName, current.username,
current.encryptedPassword ?? '', JSON.stringify(current.options ?? {}),
]);
return this.getDataSourceAsync(id);
}

Expand Down
24 changes: 24 additions & 0 deletions src/dataSourceRow.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
function parseJson(value, fallback) {
if (value === null || value === undefined || value === '') {
return fallback;
}
try {
return JSON.parse(value);
} catch {
return fallback;
}
}

function asBool(value) {
return value === 1 || value === true;
}

export function mapRow(row) {
return row ? {
...row,
options: parseJson(row.optionsJson, {}),
lastTestOk: row.lastTestOk === null || row.lastTestOk === undefined
? null
: asBool(row.lastTestOk),
} : null;
}
67 changes: 17 additions & 50 deletions src/mcp/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,88 +2,55 @@
// 默认拒绝原则:配置无效时回退到只读模式(disabled 关闭 / read 最小可见)。
import { callPlatformDatabase } from '../platformDatabaseAccess.js';

export const DEFAULT_MCP_CONFIG = {
enabled: false,
mode: 'read',
toolOverrides: {},
tenantScope: [],
rateLimit: { rpm: 60, burst: 10 },
audit: { enabled: true, level: 'full' },
sessionMode: 'stateful',
};
import { coerce } from './configValues.js';
export { DEFAULT_MCP_CONFIG, getDefaultMcpConfig } from './configValues.js';

const VALID_MODES = ['disabled', 'read', 'standard', 'full'];
const CACHE_TTL = 5000;

let cache = { value: null, at: 0 };

export function getDefaultMcpConfig() {
return JSON.parse(JSON.stringify(DEFAULT_MCP_CONFIG));
}

function coerce(raw) {
const cfg = getDefaultMcpConfig();
if (!raw || typeof raw !== 'object') {
return cfg;
}
if (typeof raw.enabled === 'boolean') cfg.enabled = raw.enabled;
if (VALID_MODES.includes(raw.mode)) cfg.mode = raw.mode;
if (raw.toolOverrides && typeof raw.toolOverrides === 'object') {
cfg.toolOverrides = { ...raw.toolOverrides };
}
if (Array.isArray(raw.tenantScope)) cfg.tenantScope = raw.tenantScope.map(String);
if (raw.rateLimit && typeof raw.rateLimit === 'object') {
const rpm = Number(raw.rateLimit.rpm);
const burst = Number(raw.rateLimit.burst);
if (Number.isFinite(rpm) && rpm > 0) cfg.rateLimit.rpm = rpm;
if (Number.isFinite(burst) && burst > 0) cfg.rateLimit.burst = burst;
}
if (raw.audit && typeof raw.audit === 'object') {
if (typeof raw.audit.enabled === 'boolean') cfg.audit.enabled = raw.audit.enabled;
if (['full', 'summary', 'off'].includes(raw.audit.level)) cfg.audit.level = raw.audit.level;
}
if (raw.sessionMode === 'stateless' || raw.sessionMode === 'stateful') {
cfg.sessionMode = raw.sessionMode;
}
return cfg;
let cache = new WeakMap();
function entryFor(database) {
if (!cache.has(database)) cache.set(database, { value: null, at: 0 });
return cache.get(database);
}

export function getMcpConfig(database) {
const entry = entryFor(database);
const now = Date.now();
if (cache.value && now - cache.at < CACHE_TTL) {
return cache.value;
if (entry.value && now - entry.at < CACHE_TTL) {
return entry.value;
}
const record = database.getPlatformSetting('mcp', null);
const raw = record && record.value && typeof record.value === 'object' ? record.value : null;
const cfg = coerce(raw);
cache = { value: cfg, at: now };
cache.set(database, { value: cfg, at: now });
return cfg;
}

export function setMcpConfig(database, patch) {
const next = coerce(patch && typeof patch === 'object' ? patch : {});
database.savePlatformSetting('mcp', next);
cache = { value: next, at: Date.now() };
cache.set(database, { value: next, at: Date.now() });
return next;
}

export async function getMcpConfigAsync(database) {
const entry = entryFor(database);
const now = Date.now();
if (cache.value && now - cache.at < CACHE_TTL) return cache.value;
if (entry.value && now - entry.at < CACHE_TTL) return entry.value;
const record = await callPlatformDatabase(database, 'getPlatformSettingAsync', 'getPlatformSetting', 'mcp', null);
const cfg = coerce(record?.value);
cache = { value: cfg, at: now };
if (cache.get(database) !== entry) return cache.get(database)?.value ?? cfg;
cache.set(database, { value: cfg, at: now });
return cfg;
}

export async function setMcpConfigAsync(database, patch) {
const next = coerce(patch && typeof patch === 'object' ? patch : {});
await callPlatformDatabase(database, 'savePlatformSettingAsync', 'savePlatformSetting', 'mcp', next);
cache = { value: next, at: Date.now() };
cache.set(database, { value: next, at: Date.now() });
return next;
}

// 仅用于测试或进程内重置,避免缓存陈旧。
export function _resetMcpConfigCache() {
cache = { value: null, at: 0 };
cache = new WeakMap();
}
43 changes: 43 additions & 0 deletions src/mcp/configValues.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
export const DEFAULT_MCP_CONFIG = {
enabled: false,
mode: 'read',
toolOverrides: {},
tenantScope: [],
rateLimit: { rpm: 60, burst: 10 },
audit: { enabled: true, level: 'full' },
sessionMode: 'stateful',
};

const VALID_MODES = ['disabled', 'read', 'standard', 'full'];


export function getDefaultMcpConfig() {
return JSON.parse(JSON.stringify(DEFAULT_MCP_CONFIG));
}

export function coerce(raw) {
const cfg = getDefaultMcpConfig();
if (!raw || typeof raw !== 'object') {
return cfg;
}
if (typeof raw.enabled === 'boolean') cfg.enabled = raw.enabled;
if (VALID_MODES.includes(raw.mode)) cfg.mode = raw.mode;
if (raw.toolOverrides && typeof raw.toolOverrides === 'object') {
cfg.toolOverrides = { ...raw.toolOverrides };
}
if (Array.isArray(raw.tenantScope)) cfg.tenantScope = raw.tenantScope.map(String);
if (raw.rateLimit && typeof raw.rateLimit === 'object') {
const rpm = Number(raw.rateLimit.rpm);
const burst = Number(raw.rateLimit.burst);
if (Number.isFinite(rpm) && rpm > 0) cfg.rateLimit.rpm = rpm;
if (Number.isFinite(burst) && burst > 0) cfg.rateLimit.burst = burst;
}
if (raw.audit && typeof raw.audit === 'object') {
if (typeof raw.audit.enabled === 'boolean') cfg.audit.enabled = raw.audit.enabled;
if (['full', 'summary', 'off'].includes(raw.audit.level)) cfg.audit.level = raw.audit.level;
}
if (raw.sessionMode === 'stateless' || raw.sessionMode === 'stateful') {
cfg.sessionMode = raw.sessionMode;
}
return cfg;
}
Loading
Loading