From e03f59c2a4b98ac9a03029f9cdec7c414e7d3d51 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Fri, 5 Jun 2026 12:46:33 +0800 Subject: [PATCH] feat(artifacts): auto-prepare on session.start and support expectedArtifactDirs - Register session.start hook to call prepareXWorkmateArtifacts best-effort so subsequent export calls have a ready scope. - Use scope directory birthtime/mtime as a floor for sinceUnixMs when the scope is already prepared, so files written before export are still picked up. - Fall back to scanning params.expectedArtifactDirs (under workspaceRoot) when no scoped candidates are found, so callers can point at ad-hoc output directories. --- dist/index.js | 15 +++++++++++++++ dist/src/exportArtifacts.js | 31 ++++++++++++++++++++++++++++++- index.ts | 15 +++++++++++++++ src/exportArtifacts.ts | 31 ++++++++++++++++++++++++++++++- 4 files changed, 90 insertions(+), 2 deletions(-) diff --git a/dist/index.js b/dist/index.js index 93f4ab9..d853951 100644 --- a/dist/index.js +++ b/dist/index.js @@ -37,6 +37,21 @@ const plugin = { }; export default plugin; function register(api) { + api.registerHook("session.start", async (event) => { + try { + const params = scopedGatewayParams(event?.context ?? event); + if (params.sessionKey && params.runId) { + await prepareXWorkmateArtifacts({ + params, + config: api.config, + pluginConfig: api.pluginConfig, + }); + } + } + catch (e) { + // Ignored: best-effort preparation + } + }); api.registerGatewayMethod("xworkmate.artifacts.prepare", async (opts) => { try { const payload = await prepareXWorkmateArtifacts({ diff --git a/dist/src/exportArtifacts.js b/dist/src/exportArtifacts.js index 240fce3..82d740d 100644 --- a/dist/src/exportArtifacts.js +++ b/dist/src/exportArtifacts.js @@ -146,17 +146,46 @@ export async function exportXWorkmateArtifacts(input) { if (!scopePrepared && sinceUnixMs > 0) { await fs.mkdir(scopeRoot, { recursive: true }); } + let effectiveSince = sinceUnixMs; + if (scopePrepared && sinceUnixMs > 0) { + try { + const scopeStat = await fs.stat(scopeRoot); + effectiveSince = Math.min(sinceUnixMs, scopeStat.birthtimeMs || scopeStat.mtimeMs); + } + catch { } + } const scopedCandidates = (await directoryExists(scopeRoot)) ? await collectCandidates({ scanRoot: scopeRoot, relativeRoot: scopeRoot, - sinceUnixMs, + sinceUnixMs: effectiveSince, warnSkippedSymlinks: true, warnings, ignoreRules: await loadArtifactIgnoreRules(scopeRoot, warnings), }) : []; const candidates = scopedCandidates; + const expectedDirs = Array.isArray(params.expectedArtifactDirs) + ? params.expectedArtifactDirs.map((d) => String(d).trim()).filter(Boolean) + : []; + if (candidates.length === 0 && expectedDirs.length > 0) { + for (const dir of expectedDirs) { + const dirPath = path.join(workspaceRoot, safeInputRelativePath(dir, "expectedArtifactDir")); + if (await directoryExists(dirPath)) { + const dirCandidates = await collectCandidates({ + scanRoot: dirPath, + relativeRoot: workspaceRoot, + sinceUnixMs: effectiveSince, + warnSkippedSymlinks: true, + warnings, + ignoreRules: await loadArtifactIgnoreRules(dirPath, warnings), + }); + for (const c of dirCandidates) { + candidates.push(c); + } + } + } + } if (!scopePrepared && candidates.length === 0) { warnings.push("artifact scope is not prepared for this task run"); } diff --git a/index.ts b/index.ts index 436b8a8..769005c 100644 --- a/index.ts +++ b/index.ts @@ -84,6 +84,21 @@ const plugin = { export default plugin; function register(api: OpenClawPluginApi) { + api.registerHook("session.start", async (event: any) => { + try { + const params = scopedGatewayParams(event?.context ?? event); + if (params.sessionKey && params.runId) { + await prepareXWorkmateArtifacts({ + params, + config: api.config, + pluginConfig: api.pluginConfig, + }); + } + } catch (e) { + // Ignored: best-effort preparation + } + }); + api.registerGatewayMethod("xworkmate.artifacts.prepare", async (opts: GatewayRequestHandlerOptions) => { try { const payload = await prepareXWorkmateArtifacts({ diff --git a/src/exportArtifacts.ts b/src/exportArtifacts.ts index 7aa302e..40e90b5 100644 --- a/src/exportArtifacts.ts +++ b/src/exportArtifacts.ts @@ -243,17 +243,46 @@ export async function exportXWorkmateArtifacts(input: ExportInput): Promise 0) { await fs.mkdir(scopeRoot, { recursive: true }); } + let effectiveSince = sinceUnixMs; + if (scopePrepared && sinceUnixMs > 0) { + try { + const scopeStat = await fs.stat(scopeRoot); + effectiveSince = Math.min(sinceUnixMs, scopeStat.birthtimeMs || scopeStat.mtimeMs); + } catch {} + } const scopedCandidates = (await directoryExists(scopeRoot)) ? await collectCandidates({ scanRoot: scopeRoot, relativeRoot: scopeRoot, - sinceUnixMs, + sinceUnixMs: effectiveSince, warnSkippedSymlinks: true, warnings, ignoreRules: await loadArtifactIgnoreRules(scopeRoot, warnings), }) : []; const candidates = scopedCandidates; + const expectedDirs = Array.isArray(params.expectedArtifactDirs) + ? params.expectedArtifactDirs.map((d: any) => String(d).trim()).filter(Boolean) + : []; + if (candidates.length === 0 && expectedDirs.length > 0) { + for (const dir of expectedDirs) { + const dirPath = path.join(workspaceRoot, safeInputRelativePath(dir, "expectedArtifactDir")); + if (await directoryExists(dirPath)) { + const dirCandidates = await collectCandidates({ + scanRoot: dirPath, + relativeRoot: workspaceRoot, + sinceUnixMs: effectiveSince, + warnSkippedSymlinks: true, + warnings, + ignoreRules: await loadArtifactIgnoreRules(dirPath, warnings), + }); + for (const c of dirCandidates) { + candidates.push(c); + } + } + } + } + if (!scopePrepared && candidates.length === 0) { warnings.push("artifact scope is not prepared for this task run"); }