mindspace: close authority boundaries
This commit is contained in:
+283
-5
@@ -10,6 +10,7 @@
|
||||
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import readline from 'node:readline';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
@@ -46,6 +47,36 @@ const PRIVATE_DATA_MAX_ROWS = Number(process.env.PRIVATE_DATA_MAX_ROWS ?? 200);
|
||||
const PRIVATE_DATA_USER_ID = process.env.PRIVATE_DATA_USER_ID?.trim();
|
||||
const AGENT_API_BASE_URL = process.env.MINDSPACE_AGENT_API_BASE_URL?.trim();
|
||||
const INTERNAL_AGENT_SECRET = process.env.MINDSPACE_INTERNAL_AGENT_SECRET?.trim();
|
||||
const MINDSPACE_MCP_BASE_URL =
|
||||
process.env.MINDSPACE_MCP_BASE_URL
|
||||
?.trim()
|
||||
.replace(/\/+$/, '');
|
||||
const MINDSPACE_MCP_SCOPED_TOKEN =
|
||||
process.env.MINDSPACE_MCP_SCOPED_TOKEN
|
||||
?.trim();
|
||||
const MINDSPACE_WORKSPACE_REF =
|
||||
process.env.MINDSPACE_WORKSPACE_REF
|
||||
?.trim();
|
||||
const MINDSPACE_SESSION_ID =
|
||||
process.env.MINDSPACE_SESSION_ID?.trim();
|
||||
const MINDSPACE_PACKAGE_ID =
|
||||
process.env.MINDSPACE_PACKAGE_ID?.trim();
|
||||
const LOGICAL_WORKSPACE_TOOLS = new Set([
|
||||
'create_dir',
|
||||
'edit_file',
|
||||
'generate_long_image',
|
||||
'list_dir',
|
||||
'publish_page',
|
||||
'read_file',
|
||||
'write_file',
|
||||
]);
|
||||
const LOGICAL_WORKSPACE_ENABLED = Boolean(
|
||||
MINDSPACE_MCP_BASE_URL &&
|
||||
MINDSPACE_MCP_SCOPED_TOKEN &&
|
||||
MINDSPACE_WORKSPACE_REF &&
|
||||
MINDSPACE_SESSION_ID &&
|
||||
MINDSPACE_PACKAGE_ID,
|
||||
);
|
||||
|
||||
const allowedToolsEnv = process.env.ALLOWED_TOOLS?.trim();
|
||||
const ALLOWED_TOOLS = allowedToolsEnv ? new Set(allowedToolsEnv.split(',').map((s) => s.trim())) : null;
|
||||
@@ -99,16 +130,29 @@ function normalizeDocxSections(sections) {
|
||||
});
|
||||
}
|
||||
|
||||
function runGenerateDocxScript({ outputPath, title, sections }) {
|
||||
function runGenerateDocxScript({
|
||||
outputPath,
|
||||
physicalOutputPath = null,
|
||||
title,
|
||||
sections,
|
||||
}) {
|
||||
const script = resolveDocxGenerateScript();
|
||||
const python = process.env.PYTHON_BIN?.trim() || 'python3';
|
||||
const targetOutputPath =
|
||||
physicalOutputPath ?? outputPath;
|
||||
const payload = JSON.stringify({
|
||||
title: String(title ?? ''),
|
||||
sections: normalizeDocxSections(sections),
|
||||
});
|
||||
const stdout = execFileSync(
|
||||
python,
|
||||
[script, '--json', '-', '--output', outputPath],
|
||||
[
|
||||
script,
|
||||
'--json',
|
||||
'-',
|
||||
'--output',
|
||||
targetOutputPath,
|
||||
],
|
||||
{
|
||||
cwd: SANDBOX,
|
||||
input: payload,
|
||||
@@ -116,7 +160,9 @@ function runGenerateDocxScript({ outputPath, title, sections }) {
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
},
|
||||
);
|
||||
const abs = resolveSandboxed(outputPath);
|
||||
const abs = physicalOutputPath
|
||||
? path.resolve(physicalOutputPath)
|
||||
: resolveSandboxed(outputPath);
|
||||
if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) {
|
||||
throw new Error(`generate_docx: 脚本执行后未找到 ${outputPath}`);
|
||||
}
|
||||
@@ -124,7 +170,11 @@ function runGenerateDocxScript({ outputPath, title, sections }) {
|
||||
if (size < 64) {
|
||||
throw new Error(`generate_docx: ${outputPath} 体积异常(${size} 字节)`);
|
||||
}
|
||||
return { bytes: size, stdout: String(stdout ?? '').trim() };
|
||||
return {
|
||||
body: fs.readFileSync(abs),
|
||||
bytes: size,
|
||||
stdout: String(stdout ?? '').trim(),
|
||||
};
|
||||
}
|
||||
|
||||
async function generateMindSpaceImage({ purpose, prompt, negativePrompt, idempotencyKey }) {
|
||||
@@ -221,6 +271,22 @@ const ALL_TOOLS = [
|
||||
required: ['path'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'publish_page',
|
||||
description:
|
||||
'通过 MindSpace Service 登记并交付已写入的 public/*.html 页面,返回 canonical workspace URL。仅在 scoped workspace RPC 启用时可用。',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
path: {
|
||||
type: 'string',
|
||||
description:
|
||||
'页面工作区相对路径,例如 public/report.html',
|
||||
},
|
||||
},
|
||||
required: ['path'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'generate_long_image',
|
||||
description:
|
||||
@@ -551,10 +617,181 @@ function resolveH5RootFromSandbox() {
|
||||
return SANDBOX_MODULE_DIR;
|
||||
}
|
||||
|
||||
async function callLogicalWorkspaceTool(
|
||||
name,
|
||||
args,
|
||||
) {
|
||||
const endpoint = new URL(
|
||||
`mindspace/v1/mcp/${encodeURIComponent(
|
||||
name,
|
||||
)}`,
|
||||
`${MINDSPACE_MCP_BASE_URL}/`,
|
||||
);
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
accept: 'application/json',
|
||||
authorization:
|
||||
`Bearer ${MINDSPACE_MCP_SCOPED_TOKEN}`,
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
arguments: args ?? {},
|
||||
}),
|
||||
signal: AbortSignal.timeout(
|
||||
Number(
|
||||
process.env
|
||||
.MINDSPACE_MCP_REQUEST_TIMEOUT_MS ??
|
||||
(name === 'generate_long_image'
|
||||
? 90_000
|
||||
: 30_000),
|
||||
),
|
||||
),
|
||||
});
|
||||
let payload = null;
|
||||
try {
|
||||
payload = await response.json();
|
||||
} catch {
|
||||
// The bounded error below intentionally avoids echoing an upstream body.
|
||||
}
|
||||
if (!response.ok || !payload?.ok) {
|
||||
const error = new Error(
|
||||
`MindSpace workspace tool ${name} failed (${response.status}): ${
|
||||
String(
|
||||
payload?.message ??
|
||||
payload?.code ??
|
||||
'unknown_error',
|
||||
).slice(0, 240)
|
||||
}`,
|
||||
);
|
||||
error.code =
|
||||
payload?.code ??
|
||||
'mindspace_workspace_tool_failed';
|
||||
throw error;
|
||||
}
|
||||
const result = payload.result ?? {};
|
||||
switch (name) {
|
||||
case 'read_file':
|
||||
return [
|
||||
{
|
||||
type: 'text',
|
||||
text: String(result.content ?? ''),
|
||||
},
|
||||
];
|
||||
case 'list_dir': {
|
||||
const lines = (
|
||||
Array.isArray(result.entries)
|
||||
? result.entries
|
||||
: []
|
||||
).map(
|
||||
(entry) =>
|
||||
`${entry?.type === 'directory' ? '[目录]' : '[文件]'} ${entry?.name ?? ''}`,
|
||||
);
|
||||
return [
|
||||
{
|
||||
type: 'text',
|
||||
text: lines.join('\n') || '(空目录)',
|
||||
},
|
||||
];
|
||||
}
|
||||
case 'write_file':
|
||||
return [
|
||||
{
|
||||
type: 'text',
|
||||
text:
|
||||
`已通过 MindSpace 写入 ${result.relativePath ?? args?.path}` +
|
||||
`(${Number(result.sizeBytes ?? 0)} 字节,package ${result.packageId ?? MINDSPACE_PACKAGE_ID})`,
|
||||
},
|
||||
];
|
||||
case 'edit_file':
|
||||
return [
|
||||
{
|
||||
type: 'text',
|
||||
text:
|
||||
`已通过 MindSpace 编辑 ${result.relativePath ?? args?.path}` +
|
||||
`(package ${result.packageId ?? MINDSPACE_PACKAGE_ID})`,
|
||||
},
|
||||
];
|
||||
case 'generate_long_image':
|
||||
return [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(
|
||||
{
|
||||
relativePath:
|
||||
result.relativePath,
|
||||
canonicalUrl:
|
||||
result.canonicalUrl,
|
||||
packageId:
|
||||
result.packageId,
|
||||
sizeBytes:
|
||||
result.sizeBytes,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
},
|
||||
];
|
||||
case 'write_binary_file':
|
||||
return [
|
||||
{
|
||||
type: 'text',
|
||||
text:
|
||||
`已通过 MindSpace 写入二进制文件 ${result.relativePath ?? args?.path}` +
|
||||
`(${Number(result.sizeBytes ?? 0)} 字节,package ${result.packageId ?? MINDSPACE_PACKAGE_ID})`,
|
||||
},
|
||||
];
|
||||
case 'create_dir':
|
||||
return [
|
||||
{
|
||||
type: 'text',
|
||||
text:
|
||||
`已通过 MindSpace 创建目录 ${result.relativePath ?? args?.path}`,
|
||||
},
|
||||
];
|
||||
case 'publish_page':
|
||||
return [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(
|
||||
{
|
||||
relativePath:
|
||||
result.relativePath,
|
||||
canonicalUrl:
|
||||
result.canonicalUrl,
|
||||
packageId:
|
||||
result.packageId,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
},
|
||||
];
|
||||
default:
|
||||
throw new Error(
|
||||
`Unsupported logical workspace tool: ${name}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function callTool(name, args) {
|
||||
if (ALLOWED_TOOLS && !ALLOWED_TOOLS.has(name)) {
|
||||
throw new Error(`工具 ${name} 未授权`);
|
||||
}
|
||||
if (
|
||||
LOGICAL_WORKSPACE_ENABLED &&
|
||||
LOGICAL_WORKSPACE_TOOLS.has(name)
|
||||
) {
|
||||
return callLogicalWorkspaceTool(
|
||||
name,
|
||||
args,
|
||||
);
|
||||
}
|
||||
if (name === 'publish_page') {
|
||||
throw new Error(
|
||||
'publish_page: MindSpace scoped workspace RPC 未配置',
|
||||
);
|
||||
}
|
||||
switch (name) {
|
||||
case 'read_file': {
|
||||
const abs = resolveSandboxed(args.path);
|
||||
@@ -634,7 +871,48 @@ async function callTool(name, args) {
|
||||
throw new Error('generate_docx: title 不能为空');
|
||||
}
|
||||
const sections = args.sections ?? args.payload?.sections;
|
||||
const result = runGenerateDocxScript({ outputPath, title, sections });
|
||||
let result;
|
||||
if (LOGICAL_WORKSPACE_ENABLED) {
|
||||
const tempDir = fs.mkdtempSync(
|
||||
path.join(
|
||||
os.tmpdir(),
|
||||
'mindspace-docx-',
|
||||
),
|
||||
);
|
||||
try {
|
||||
result = runGenerateDocxScript({
|
||||
outputPath,
|
||||
physicalOutputPath:
|
||||
path.join(
|
||||
tempDir,
|
||||
'generated.docx',
|
||||
),
|
||||
title,
|
||||
sections,
|
||||
});
|
||||
await callLogicalWorkspaceTool(
|
||||
'write_binary_file',
|
||||
{
|
||||
path: outputPath,
|
||||
bodyBase64:
|
||||
result.body.toString(
|
||||
'base64',
|
||||
),
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(tempDir, {
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
result = runGenerateDocxScript({
|
||||
outputPath,
|
||||
title,
|
||||
sections,
|
||||
});
|
||||
}
|
||||
return [
|
||||
{
|
||||
type: 'text',
|
||||
|
||||
Reference in New Issue
Block a user