fix(agent): recover stale runs and improve new-user OA delivery
Fix DEV logout cookie clearing, materialize selected MindSpace OA assets before agent runs, and recover zombie runs from synced workspace pages. Add client run wait timeout, harness retry limits, page-edit asset forwarding, and logout/john2 scenario tests. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+138
-6
@@ -1,3 +1,74 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { resolveAssetWorkspaceRelativePath } from './mindspace-workspace-path.mjs';
|
||||
import { ensureUserZoneDirs } from './user-space.mjs';
|
||||
|
||||
function sanitizeBasename(value) {
|
||||
const normalized = String(value ?? '').replace(/\\/g, '/').trim();
|
||||
const basename = normalized.split('/').pop() ?? '';
|
||||
return basename.replace(/[^\w.\-()\u4e00-\u9fff]+/g, '_') || 'file';
|
||||
}
|
||||
|
||||
async function materializeAssetToWorkspace({
|
||||
assetService,
|
||||
resolveWorkspaceRoot,
|
||||
userId,
|
||||
assetId,
|
||||
}) {
|
||||
const { asset, path: sourcePath } = await assetService.readAsset(userId, assetId);
|
||||
const workspaceRoot = await resolveWorkspaceRoot(userId);
|
||||
ensureUserZoneDirs(workspaceRoot);
|
||||
const filename = sanitizeBasename(asset.filename ?? asset.displayName ?? asset.id);
|
||||
const relativePath =
|
||||
resolveAssetWorkspaceRelativePath({
|
||||
categoryCode: asset.categoryCode ?? 'oa',
|
||||
originalFilename: filename,
|
||||
}) ?? `oa/${filename}`;
|
||||
const destPath = path.join(workspaceRoot, ...relativePath.split('/'));
|
||||
await fs.mkdir(path.dirname(destPath), { recursive: true });
|
||||
await fs.copyFile(sourcePath, destPath);
|
||||
const stat = await fs.stat(destPath);
|
||||
return {
|
||||
assetId,
|
||||
relativePath,
|
||||
sizeBytes: stat.size,
|
||||
displayName: asset.displayName ?? filename,
|
||||
mimeType: asset.mimeType ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildSelectedAssetReadInstructions(context) {
|
||||
const selected = context.selectedAssets ?? [];
|
||||
if (selected.length !== 1) return [];
|
||||
|
||||
const asset = selected[0];
|
||||
const h5ApiBase = String(context.h5ApiBase ?? '').replace(/\/$/, '');
|
||||
const sessionId = String(context.agentSessionId ?? '').trim();
|
||||
if (!h5ApiBase) return [];
|
||||
|
||||
const sessionToken = sessionId || '<CURRENT_AGENT_SESSION>';
|
||||
const sessionHint = sessionId
|
||||
? ''
|
||||
: '- session_id 未写入上下文时,使用当前 Agent 会话 ID 替换 `<CURRENT_AGENT_SESSION>`。\n';
|
||||
|
||||
return [
|
||||
'【勾选资料读取(硬性)】',
|
||||
`- 用户已勾选「${asset.displayName}」(id: ${asset.id})`,
|
||||
'- **禁止** curl / fetch 浏览器登录接口 `GET /api/mindspace/v1/assets/:id/download`(goosed 无 cookie,会 401)',
|
||||
'- **优先**用 `list_dir oa` / `read_file` / `cat oa/文件名` 读取工作区已落盘副本',
|
||||
sessionHint,
|
||||
'- 若工作区尚无该文件,**必须**用 shell 调用 Agent 代理下载并落盘到 `oa/`:',
|
||||
'```bash',
|
||||
`curl -sS -X POST '${h5ApiBase}/api/agent/mindspace_asset_download' \\`,
|
||||
" -H 'Content-Type: application/json' \\",
|
||||
` -d '{"session_id":"${sessionToken}","asset_id":"${asset.id}"}'`,
|
||||
'```',
|
||||
'- 成功后响应 JSON 含 `relativePath`(如 `oa/report.xlsx`),后续分析/引用只用该相对路径',
|
||||
'- **禁止**访问其它用户目录、MindSpace 根目录或 `./MindSpace/<username>/` 等猜测路径',
|
||||
'',
|
||||
].filter(Boolean);
|
||||
}
|
||||
|
||||
export function buildSelectedAssetDeleteInstructions(context) {
|
||||
const selected = context.selectedAssets ?? [];
|
||||
if (selected.length !== 1) return [];
|
||||
@@ -29,7 +100,24 @@ export function buildSelectedAssetDeleteInstructions(context) {
|
||||
].filter(Boolean);
|
||||
}
|
||||
|
||||
export function createAssetAgentService({ assetService, resolveUserIdForAgentSession }) {
|
||||
export function createAssetAgentService({
|
||||
assetService,
|
||||
resolveUserIdForAgentSession,
|
||||
resolveWorkspaceRoot,
|
||||
}) {
|
||||
if (typeof resolveWorkspaceRoot !== 'function') {
|
||||
throw new Error('resolveWorkspaceRoot is required for asset agent service');
|
||||
}
|
||||
|
||||
async function resolveOwnedUserId(sessionId, explicitUserId = null) {
|
||||
if (explicitUserId) return explicitUserId;
|
||||
const userId = await resolveUserIdForAgentSession(sessionId);
|
||||
if (!userId) {
|
||||
throw Object.assign(new Error('无效的 Agent 会话'), { code: 'forbidden' });
|
||||
}
|
||||
return userId;
|
||||
}
|
||||
|
||||
return {
|
||||
async applyAgentDelete(input = {}) {
|
||||
const sessionId = String(input?.sessionId ?? input?.session_id ?? '').trim();
|
||||
@@ -42,11 +130,7 @@ export function createAssetAgentService({ assetService, resolveUserIdForAgentSes
|
||||
throw Object.assign(new Error('删除前须已在对话中获得用户确认'), { code: 'confirmation_required' });
|
||||
}
|
||||
|
||||
const userId = await resolveUserIdForAgentSession(sessionId);
|
||||
if (!userId) {
|
||||
throw Object.assign(new Error('无效的 Agent 会话'), { code: 'forbidden' });
|
||||
}
|
||||
|
||||
const userId = await resolveOwnedUserId(sessionId);
|
||||
const result = await assetService.deleteAsset(userId, assetId);
|
||||
return {
|
||||
assetId,
|
||||
@@ -54,5 +138,53 @@ export function createAssetAgentService({ assetService, resolveUserIdForAgentSes
|
||||
...result,
|
||||
};
|
||||
},
|
||||
|
||||
async applyAgentDownload(input = {}) {
|
||||
const sessionId = String(input?.sessionId ?? input?.session_id ?? '').trim();
|
||||
const assetId = String(input?.assetId ?? input?.asset_id ?? '').trim();
|
||||
if (!sessionId || !assetId) {
|
||||
throw Object.assign(new Error('缺少 session_id 或 asset_id'), { code: 'invalid_request' });
|
||||
}
|
||||
|
||||
const userId = await resolveOwnedUserId(sessionId);
|
||||
const materialized = await materializeAssetToWorkspace({
|
||||
assetService,
|
||||
resolveWorkspaceRoot,
|
||||
userId,
|
||||
assetId,
|
||||
});
|
||||
return {
|
||||
userId,
|
||||
...materialized,
|
||||
};
|
||||
},
|
||||
|
||||
async materializeAssetsForSession({
|
||||
sessionId = null,
|
||||
userId = null,
|
||||
assetIds = [],
|
||||
} = {}) {
|
||||
const normalizedIds = [...new Set(
|
||||
(Array.isArray(assetIds) ? assetIds : [])
|
||||
.map((item) => String(item ?? '').trim())
|
||||
.filter(Boolean),
|
||||
)];
|
||||
if (normalizedIds.length === 0) {
|
||||
return { materialized: [] };
|
||||
}
|
||||
const resolvedUserId = await resolveOwnedUserId(sessionId, userId);
|
||||
const materialized = [];
|
||||
for (const assetId of normalizedIds) {
|
||||
materialized.push(
|
||||
await materializeAssetToWorkspace({
|
||||
assetService,
|
||||
resolveWorkspaceRoot,
|
||||
userId: resolvedUserId,
|
||||
assetId,
|
||||
}),
|
||||
);
|
||||
}
|
||||
return { materialized };
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user