4e21ca937a
Deploy Documentation / deploy (push) Has been cancelled
Canary / Prepare Version (push) Has been cancelled
Canary / build-cli (push) Has been cancelled
Canary / Upload Install Script (push) Has been cancelled
Canary / bundle-desktop (push) Has been cancelled
Canary / bundle-desktop-intel (push) Has been cancelled
Canary / bundle-desktop-linux (push) Has been cancelled
Canary / bundle-desktop-windows (push) Has been cancelled
Canary / bundle-desktop-windows-cuda (push) Has been cancelled
Canary / Release (push) Has been cancelled
Unused Dependencies / machete (push) Has been cancelled
CI / changes (push) Has been cancelled
CI / Check Rust Code Format (push) Has been cancelled
CI / Build and Test Rust Project (push) Has been cancelled
CI / Build Rust Project on Windows (push) Has been cancelled
CI / Check MSRV (push) Has been cancelled
CI / Lint Rust Code (push) Has been cancelled
CI / Check Generated Schemas are Up-to-Date (push) Has been cancelled
CI / Test and Lint Electron Desktop App (push) Has been cancelled
CI / H5 Plaza Tests and Build (push) Has been cancelled
Live Provider Tests / check-fork (push) Has been cancelled
Live Provider Tests / changes (push) Has been cancelled
Live Provider Tests / Build Binary (push) Has been cancelled
Live Provider Tests / Smoke Tests (push) Has been cancelled
Live Provider Tests / Smoke Tests (Code Execution) (push) Has been cancelled
Live Provider Tests / Compaction Tests (push) Has been cancelled
Live Provider Tests / goose server HTTP integration tests (push) Has been cancelled
Publish Ask AI Bot Docker Image / docker (push) Has been cancelled
Publish Docker Image / docker (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
Fork goose with custom MCP widgets, platform extensions (aider, git, web, search), MindSpace H5 backend/frontend, Plaza/Ops UIs, and deploy scripts for tkmind.cn. Co-authored-by: Cursor <cursoragent@cursor.com>
185 lines
7.1 KiB
JavaScript
185 lines
7.1 KiB
JavaScript
import fs from 'node:fs';
|
||
import path from 'node:path';
|
||
import { SYSTEM_CATEGORIES } from './mindspace.mjs';
|
||
import { PUBLISH_ROOT_DIR, resolvePublishDir } from './user-publish.mjs';
|
||
|
||
/** 用户 MindSpace 下的分区子目录(与上传分类一致) */
|
||
export const UPLOAD_ZONE_CODES = ['oa', 'private', 'public'];
|
||
|
||
export function resolveMindspaceStorageRoot(h5Root, env = process.env) {
|
||
return path.resolve(env.MINDSPACE_STORAGE_ROOT ?? path.join(h5Root, 'data', 'mindspace'));
|
||
}
|
||
|
||
/** Agent 工作区 = MindSpace/<username>/ */
|
||
export function resolveUserWorkspaceRoot(h5Root, user) {
|
||
return resolvePublishDir(h5Root, user);
|
||
}
|
||
|
||
export const WORKSPACE_HINTS_FILENAME = '.tkmindhints';
|
||
export const LEGACY_WORKSPACE_HINTS_FILENAME = '.goosehints';
|
||
|
||
function renderBrandingBlock() {
|
||
return `## 品牌与称呼(硬性)
|
||
|
||
- 对用户统一称 **TKMind**,不要称 goose、Goose、goosed
|
||
- 不要描述本工作区为「Rust goose 项目」或「goose AI 框架」
|
||
- 本工作区是 TKMind **MindSpace 用户空间**,用于 OA/私人/公开文件管理与静态页面生成
|
||
`;
|
||
}
|
||
|
||
export function resolveZoneDir(workspaceRoot, categoryCode) {
|
||
return path.join(workspaceRoot, categoryCode);
|
||
}
|
||
|
||
export function resolveZoneFilePath(workspaceRoot, categoryCode, filename) {
|
||
return path.join(resolveZoneDir(workspaceRoot, categoryCode), filename);
|
||
}
|
||
|
||
export function zoneLabel(categoryCode) {
|
||
return SYSTEM_CATEGORIES.find((item) => item.code === categoryCode)?.name ?? categoryCode;
|
||
}
|
||
|
||
export function ensureUserZoneDirs(workspaceRoot) {
|
||
for (const code of UPLOAD_ZONE_CODES) {
|
||
fs.mkdirSync(resolveZoneDir(workspaceRoot, code), { recursive: true });
|
||
}
|
||
}
|
||
|
||
/** 上传完成后镜像到 MindSpace/<user>/<category>/文件名 */
|
||
export function mirrorAssetToZone({ workspaceRoot, categoryCode, filename, sourcePath }) {
|
||
if (!UPLOAD_ZONE_CODES.includes(categoryCode)) return null;
|
||
if (!sourcePath || !fs.existsSync(sourcePath)) return null;
|
||
ensureUserZoneDirs(workspaceRoot);
|
||
const dest = resolveZoneFilePath(workspaceRoot, categoryCode, filename);
|
||
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
||
fs.copyFileSync(sourcePath, dest);
|
||
return dest;
|
||
}
|
||
|
||
export function removeZoneMirror({ workspaceRoot, categoryCode, filename }) {
|
||
if (!UPLOAD_ZONE_CODES.includes(categoryCode)) return;
|
||
const target = resolveZoneFilePath(workspaceRoot, categoryCode, filename);
|
||
if (fs.existsSync(target)) fs.unlinkSync(target);
|
||
}
|
||
|
||
export function isPathInsideUserWorkspace(workspaceRoot, requestedPath) {
|
||
const base = path.resolve(workspaceRoot);
|
||
const resolved = path.resolve(requestedPath);
|
||
return resolved === base || resolved.startsWith(`${base}${path.sep}`);
|
||
}
|
||
|
||
export function renderUserSpaceHints({ username, workspaceRoot }) {
|
||
const zoneLines = UPLOAD_ZONE_CODES.map(
|
||
(code) => `- \`${code}/\` — ${zoneLabel(code)}(用户上传落在此分区)`,
|
||
);
|
||
return `# TKMind 用户空间分区
|
||
|
||
你是用户 \`${username}\` 的专属助手。当前会话工作区(唯一文件根目录):
|
||
\`${workspaceRoot}\`
|
||
|
||
${renderBrandingBlock()}
|
||
## 分区规则(硬性)
|
||
|
||
用户上传的文件**只会**出现在下列子目录之一:
|
||
|
||
${zoneLines.join('\n')}
|
||
|
||
## 查找 / 读取文件
|
||
|
||
1. 在工作区内搜索,例如 \`tree oa/\`、\`find oa -name '*.csv'\`、\`cat oa/文件名.csv\`
|
||
2. 用户说「OA 区」→ \`oa/\`;「私人区」→ \`private/\`;「公开区」→ \`public/\`
|
||
3. **禁止**访问其它用户目录、\`${PUBLISH_ROOT_DIR}/\` 根目录(非本用户)、\`data/mindspace/\`、主机绝对路径
|
||
4. **禁止**用公网 URL 列目录或读 CSV;公网链接仅用于分享已发布 HTML
|
||
5. 找不到时说明该分区内没有该文件,请用户确认上传到了哪个分区
|
||
|
||
## 生成页面
|
||
|
||
- 静态 HTML 可写入工作区根目录或 \`public/\`,完成后给出公网链接
|
||
|
||
## 工作区文件与 OA 界面
|
||
|
||
- 写入 \`oa/\`、\`private/\`、\`public/\` 根目录的支持类型文件(docx、csv、pdf、图片等)会**自动同步**到 MindSpace 资产库
|
||
- 打开对应分区或保存文件后会出现在界面中,可直接预览或下载
|
||
- 用户上传的文件仍以界面入库为准,并镜像到上述分区
|
||
`;
|
||
}
|
||
|
||
export function buildUserSpaceConstraints({ username, workspaceRoot, publicBaseUrl, slug }) {
|
||
const zoneList = UPLOAD_ZONE_CODES.map((code) => `\`${code}/\`(${zoneLabel(code)})`).join('、');
|
||
return [
|
||
'## TKMind 用户空间分区(硬性约束)',
|
||
'',
|
||
'- 对用户统一称 **TKMind**;禁止称 goose / Goose / goosed 或「Rust goose 项目」',
|
||
`- 用户 \`${username}\` 的 Agent 工作区:\`${workspaceRoot}\``,
|
||
`- 用户上传落在分区子目录:${zoneList}(例如 OA 文件在 \`oa/\`)`,
|
||
'- **查找文件**:只在上述工作区内搜索(如 `oa/2025-12-06T13-34_export.csv`)',
|
||
'- **禁止**:访问其它用户目录、MindSpace 根目录、data/mindspace 内部路径、主机绝对路径',
|
||
'- **禁止**用公网 URL 列目录或读 CSV;生成 HTML 写入工作区并给出公网链接',
|
||
publicBaseUrl && slug
|
||
? `- 公网 HTML 前缀(仅分享用):\`${publicBaseUrl}/${PUBLISH_ROOT_DIR}/${slug}/\``
|
||
: null,
|
||
]
|
||
.filter(Boolean)
|
||
.join('\n');
|
||
}
|
||
|
||
export function ensureUserSpaceHints(workspaceRoot, context) {
|
||
const hintsPath = path.join(workspaceRoot, WORKSPACE_HINTS_FILENAME);
|
||
const legacyPath = path.join(workspaceRoot, LEGACY_WORKSPACE_HINTS_FILENAME);
|
||
const content = renderUserSpaceHints({ ...context, workspaceRoot });
|
||
const existing = fs.existsSync(hintsPath) ? fs.readFileSync(hintsPath, 'utf8') : '';
|
||
if (existing !== content) {
|
||
fs.writeFileSync(hintsPath, content, 'utf8');
|
||
}
|
||
if (fs.existsSync(legacyPath)) {
|
||
fs.unlinkSync(legacyPath);
|
||
}
|
||
return hintsPath;
|
||
}
|
||
|
||
export async function syncUserZonesFromAssets(pool, storageRoot, userId, workspaceRoot) {
|
||
ensureUserZoneDirs(workspaceRoot);
|
||
const placeholders = UPLOAD_ZONE_CODES.map(() => '?').join(', ');
|
||
const [rows] = await pool.query(
|
||
`SELECT a.original_filename, c.category_code, v.storage_key
|
||
FROM h5_assets a
|
||
JOIN h5_space_categories c ON c.id = a.category_id AND c.user_id = a.user_id
|
||
JOIN h5_asset_versions v ON v.id = a.current_version_id
|
||
WHERE a.user_id = ? AND a.status <> 'deleted' AND c.category_code IN (${placeholders})`,
|
||
[userId, ...UPLOAD_ZONE_CODES],
|
||
);
|
||
for (const row of rows) {
|
||
const sourcePath = path.join(storageRoot, row.storage_key);
|
||
mirrorAssetToZone({
|
||
workspaceRoot,
|
||
categoryCode: row.category_code,
|
||
filename: row.original_filename,
|
||
sourcePath,
|
||
});
|
||
}
|
||
return { workspaceRoot, mirrored: rows.length };
|
||
}
|
||
|
||
export async function ensureUserSpaceLayout({
|
||
pool,
|
||
storageRoot,
|
||
userId,
|
||
username,
|
||
publicBaseUrl,
|
||
slug,
|
||
workspaceRoot,
|
||
}) {
|
||
ensureUserZoneDirs(workspaceRoot);
|
||
if (pool && userId) {
|
||
await syncUserZonesFromAssets(pool, storageRoot, userId, workspaceRoot);
|
||
}
|
||
const context = { username: username ?? slug, workspaceRoot, slug, publicBaseUrl };
|
||
ensureUserSpaceHints(workspaceRoot, context);
|
||
return {
|
||
workspaceRoot,
|
||
zonesRoot: workspaceRoot,
|
||
zonesPublicDir: resolveZoneDir(workspaceRoot, 'public'),
|
||
constraints: buildUserSpaceConstraints(context),
|
||
...context,
|
||
};
|
||
} |