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.
This commit is contained in:
Haitao Pan 2026-06-05 12:46:33 +08:00
parent 2695c38612
commit e03f59c2a4
4 changed files with 90 additions and 2 deletions

15
dist/index.js vendored
View File

@ -37,6 +37,21 @@ const plugin = {
}; };
export default plugin; export default plugin;
function register(api) { 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) => { api.registerGatewayMethod("xworkmate.artifacts.prepare", async (opts) => {
try { try {
const payload = await prepareXWorkmateArtifacts({ const payload = await prepareXWorkmateArtifacts({

View File

@ -146,17 +146,46 @@ export async function exportXWorkmateArtifacts(input) {
if (!scopePrepared && sinceUnixMs > 0) { if (!scopePrepared && sinceUnixMs > 0) {
await fs.mkdir(scopeRoot, { recursive: true }); 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)) const scopedCandidates = (await directoryExists(scopeRoot))
? await collectCandidates({ ? await collectCandidates({
scanRoot: scopeRoot, scanRoot: scopeRoot,
relativeRoot: scopeRoot, relativeRoot: scopeRoot,
sinceUnixMs, sinceUnixMs: effectiveSince,
warnSkippedSymlinks: true, warnSkippedSymlinks: true,
warnings, warnings,
ignoreRules: await loadArtifactIgnoreRules(scopeRoot, warnings), ignoreRules: await loadArtifactIgnoreRules(scopeRoot, warnings),
}) })
: []; : [];
const candidates = scopedCandidates; 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) { if (!scopePrepared && candidates.length === 0) {
warnings.push("artifact scope is not prepared for this task run"); warnings.push("artifact scope is not prepared for this task run");
} }

View File

@ -84,6 +84,21 @@ const plugin = {
export default plugin; export default plugin;
function register(api: OpenClawPluginApi) { 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) => { api.registerGatewayMethod("xworkmate.artifacts.prepare", async (opts: GatewayRequestHandlerOptions) => {
try { try {
const payload = await prepareXWorkmateArtifacts({ const payload = await prepareXWorkmateArtifacts({

View File

@ -243,17 +243,46 @@ export async function exportXWorkmateArtifacts(input: ExportInput): Promise<XWor
if (!scopePrepared && sinceUnixMs > 0) { if (!scopePrepared && sinceUnixMs > 0) {
await fs.mkdir(scopeRoot, { recursive: true }); 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)) const scopedCandidates = (await directoryExists(scopeRoot))
? await collectCandidates({ ? await collectCandidates({
scanRoot: scopeRoot, scanRoot: scopeRoot,
relativeRoot: scopeRoot, relativeRoot: scopeRoot,
sinceUnixMs, sinceUnixMs: effectiveSince,
warnSkippedSymlinks: true, warnSkippedSymlinks: true,
warnings, warnings,
ignoreRules: await loadArtifactIgnoreRules(scopeRoot, warnings), ignoreRules: await loadArtifactIgnoreRules(scopeRoot, warnings),
}) })
: []; : [];
const candidates = scopedCandidates; 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) { if (!scopePrepared && candidates.length === 0) {
warnings.push("artifact scope is not prepared for this task run"); warnings.push("artifact scope is not prepared for this task run");
} }