import test from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { spawn } from 'node:child_process'; import { syncPublicDocxDownloads } from './mindspace-public-finish-sync.mjs'; function copyDocxGenerateSkill(root) { const srcDir = path.join(process.cwd(), 'skills', 'docx-generate'); const destDir = path.join(root, '.agents', 'skills', 'docx-generate'); fs.mkdirSync(destDir, { recursive: true }); for (const name of ['generate_docx.py', 'SKILL.md']) { fs.copyFileSync(path.join(srcDir, name), path.join(destDir, name)); } } function summarySections(caseName) { return [ { heading: '一、项目概述', paragraphs: [ `${caseName}项目面向医疗服务流程中的核心痛点,通过数字化与 AI 能力重构诊前、诊中、诊后体验。`, '本摘要基于创新三角评估框架,从创新、成果、市场、团队四个维度提炼关键结论。', ], }, { heading: '二、解决方案与技术创新', paragraphs: [ '项目以数据驱动和智能辅助决策为主线,形成可复制的技术体系与流程再造方案。', '在技术创新、商业模式与政策吻合度方面均具备行业示范价值。', ], }, { heading: '三、成果与价值', paragraphs: [ '核心指标显示候诊效率、临床质量或安全水平获得显著提升,患者与医护双侧受益。', '项目已具备向医联体与区域平台输出的标准化能力。', ], }, { heading: '四、壁垒、风险与未来展望', paragraphs: [ '竞争壁垒来自临床数据积累、流程嵌入深度与专科Know-how。', '未来可在专科深化、基层赋能与支付模式创新方向持续扩展。', ], }, ]; } function startSandbox(root, envOverrides = {}) { const child = spawn(process.execPath, ['mindspace-sandbox-mcp.mjs', root], { cwd: process.cwd(), env: { ...process.env, ALLOWED_TOOLS: 'private_data_info,private_data_schema,private_data_query,private_data_execute', PRIVATE_DATA_MAX_BYTES: String(1024 * 1024), ...envOverrides, }, stdio: ['pipe', 'pipe', 'pipe'], }); const pending = new Map(); let nextId = 1; let buffer = ''; child.stdout.setEncoding('utf8'); child.stdout.on('data', (chunk) => { buffer += chunk; let idx; while ((idx = buffer.indexOf('\n')) >= 0) { const line = buffer.slice(0, idx); buffer = buffer.slice(idx + 1); if (!line.trim()) continue; const msg = JSON.parse(line); pending.get(msg.id)?.(msg); pending.delete(msg.id); } }); const request = (method, params = {}) => new Promise((resolve) => { const id = nextId++; pending.set(id, resolve); child.stdin.write(JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n'); }); return { child, request }; } test('sandbox MCP exposes and protects the user private data space', async (t) => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mindspace-sandbox-')); const server = startSandbox(root); t.after(() => server.child.kill()); await server.request('initialize'); const listed = await server.request('tools/list'); assert.deepEqual( listed.result.tools.map((tool) => tool.name), ['private_data_info', 'private_data_schema', 'private_data_query', 'private_data_execute'], ); const create = await server.request('tools/call', { name: 'private_data_execute', arguments: { sql: `CREATE TABLE surveys (id INTEGER PRIMARY KEY, title TEXT NOT NULL); INSERT INTO surveys (title) VALUES ('满意度调查');`, }, }); assert.equal(create.result.isError, false); const query = await server.request('tools/call', { name: 'private_data_query', arguments: { sql: 'SELECT id, title FROM surveys ORDER BY id' }, }); assert.equal(query.result.isError, false); assert.deepEqual(JSON.parse(query.result.content[0].text), [{ id: 1, title: '满意度调查' }]); assert.equal(fs.existsSync(path.join(root, '.mindspace', 'private-data.sqlite')), true); const rejected = await server.request('tools/call', { name: 'private_data_query', arguments: { sql: "ATTACH DATABASE '/tmp/other.sqlite' AS other" }, }); assert.equal(rejected.result.isError, true); assert.match(rejected.result.content[0].text, /不允许|只允许/); const dotCommand = await server.request('tools/call', { name: 'private_data_execute', arguments: { sql: `.open /tmp/other.sqlite` }, }); assert.equal(dotCommand.result.isError, true); assert.match(dotCommand.result.content[0].text, /dot command/); }); test('sandbox MCP exposes schedule tools only when schedule env is configured', async (t) => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mindspace-sandbox-schedule-')); const server = startSandbox(root, { ALLOWED_TOOLS: 'schedule_create_item,schedule_create_reminder,schedule_list_items', PRIVATE_DATA_USER_ID: 'user-1', DATABASE_URL: 'mysql://boot:pass@127.0.0.1:3306/test', }); t.after(() => server.child.kill()); await server.request('initialize'); const listed = await server.request('tools/list'); assert.deepEqual( listed.result.tools.map((tool) => tool.name), ['schedule_create_item', 'schedule_create_reminder', 'schedule_list_items'], ); }); test('generate_docx writes public Word files for Mark-style summary request', async (t) => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mindspace-sandbox-docx-')); fs.mkdirSync(path.join(root, 'public'), { recursive: true }); copyDocxGenerateSkill(root); const server = startSandbox(root, { ALLOWED_TOOLS: 'generate_docx,write_file,list_dir', }); t.after(() => server.child.kill()); await server.request('initialize'); const listed = await server.request('tools/list'); assert.ok(listed.result.tools.some((tool) => tool.name === 'generate_docx')); const cases = [ { docx: 'public/协和智慧门诊研究摘要.docx', html: 'public/协和智慧门诊研究摘要.html', title: '北京协和医院「智慧门诊 + AI 预问诊」研究摘要', }, { docx: 'public/广州妇儿CDSS研究摘要.docx', html: 'public/广州妇儿CDSS研究摘要.html', title: '广州妇儿中心「数据驱动 + AI 临床决策」研究摘要', }, ]; for (const item of cases) { const generated = await server.request('tools/call', { name: 'generate_docx', arguments: { output_path: item.docx, title: item.title, sections: summarySections(item.title), }, }); assert.equal(generated.result.isError, false, generated.result.content?.[0]?.text); assert.match(generated.result.content[0].text, /已生成 public\/.+\.docx/); assert.ok(fs.statSync(path.join(root, item.docx)).size > 500); } for (const item of cases) { const docxName = path.basename(item.docx); await server.request('tools/call', { name: 'write_file', arguments: { path: item.html, content: `
下载 Word`, }, }); } const listedPublic = await server.request('tools/call', { name: 'list_dir', arguments: { path: 'public' }, }); assert.equal(listedPublic.result.isError, false); assert.match(listedPublic.result.content[0].text, /协和智慧门诊研究摘要\.docx/); assert.match(listedPublic.result.content[0].text, /广州妇儿CDSS研究摘要\.docx/); const sync = syncPublicDocxDownloads({ publishDir: root }); assert.deepEqual(sync.missing, []); assert.deepEqual(sync.synced, []); for (const item of cases) { const abs = path.join(root, item.docx); assert.ok(fs.existsSync(abs)); const zipHeader = fs.readFileSync(abs).subarray(0, 2).toString('utf8'); assert.equal(zipHeader, 'PK', `${item.docx} should be a valid zip/docx`); } });