mirror of https://github.com/openclaw/openclaw.git
fix: land slash command metadata parsing (#58725) (thanks @Mlightsnow)
This commit is contained in:
parent
3b1f8e3461
commit
f559ea126d
|
|
@ -37,6 +37,7 @@ Docs: https://docs.openclaw.ai
|
|||
- Exec/approvals: resume the original agent session after an approved async exec finishes, so external completion followups continue the task instead of waiting for a new user turn. (#58860) Thanks @Nanako0129.
|
||||
- Gateway/node pairing: create repair pairing requests when a paired node reconnects with allowlisted commands missing from its approved node record, refresh stale pending repair metadata, and surface paired node command metadata in `nodes status`/`describe` even while the node is offline. Fixes #58824.
|
||||
- Channels/plugins: keep bundled channel plugins loadable from legacy `channels.<id>` config even under restrictive plugin allowlists, and make `openclaw doctor` warn only on real plugin blockers instead of misleading setup guidance. (#58873) Thanks @obviyus
|
||||
- Auto-reply/commands: strip inbound metadata before slash command detection so wrapped `/model`, `/new`, and `/status` commands are recognized. (#58725) Thanks @Mlightsnow.
|
||||
|
||||
## 2026.3.31
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,34 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { DEFAULT_TOP_OF_HOUR_STAGGER_MS } from "../cron/stagger.js";
|
||||
import { normalizeStoredCronJobs } from "./doctor-cron-store-migration.js";
|
||||
|
||||
function makeLegacyJob(overrides: Record<string, unknown>): Record<string, unknown> {
|
||||
return {
|
||||
id: "job-legacy",
|
||||
agentId: undefined,
|
||||
name: "Legacy job",
|
||||
description: null,
|
||||
enabled: true,
|
||||
deleteAfterRun: false,
|
||||
createdAtMs: 1_700_000_000_000,
|
||||
updatedAtMs: 1_700_000_000_000,
|
||||
sessionTarget: "main",
|
||||
wakeMode: "next-heartbeat",
|
||||
payload: {
|
||||
kind: "systemEvent",
|
||||
text: "tick",
|
||||
},
|
||||
state: {},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeOneJob(job: Record<string, unknown>) {
|
||||
const jobs = [job];
|
||||
const result = normalizeStoredCronJobs(jobs);
|
||||
return { job: jobs[0], result };
|
||||
}
|
||||
|
||||
describe("normalizeStoredCronJobs", () => {
|
||||
it("normalizes legacy cron fields and reports migration issues", () => {
|
||||
const jobs = [
|
||||
|
|
@ -132,4 +160,157 @@ describe("normalizeStoredCronJobs", () => {
|
|||
expect(jobs[0]?.payload).toMatchObject({ kind: "agentTurn", message: "ping" });
|
||||
expect(jobs[1]?.payload).toMatchObject({ kind: "systemEvent", text: "pong" });
|
||||
});
|
||||
|
||||
it("normalizes isolated legacy jobs without mutating runtime code paths", () => {
|
||||
const { job, result } = normalizeOneJob(
|
||||
makeLegacyJob({
|
||||
id: "job-1",
|
||||
sessionKey: " agent:main:discord:channel:ops ",
|
||||
schedule: { kind: "at", atMs: 1_700_000_000_000 },
|
||||
sessionTarget: "isolated",
|
||||
payload: {
|
||||
kind: "agentTurn",
|
||||
message: "hi",
|
||||
deliver: true,
|
||||
channel: "telegram",
|
||||
to: "7200373102",
|
||||
bestEffortDeliver: true,
|
||||
},
|
||||
isolation: { postToMainPrefix: "Cron" },
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.mutated).toBe(true);
|
||||
expect(job.sessionKey).toBe("agent:main:discord:channel:ops");
|
||||
expect(job.delivery).toEqual({
|
||||
mode: "announce",
|
||||
channel: "telegram",
|
||||
to: "7200373102",
|
||||
bestEffort: true,
|
||||
});
|
||||
expect("isolation" in job).toBe(false);
|
||||
|
||||
const payload = job.payload as Record<string, unknown>;
|
||||
expect(payload.deliver).toBeUndefined();
|
||||
expect(payload.channel).toBeUndefined();
|
||||
expect(payload.to).toBeUndefined();
|
||||
expect(payload.bestEffortDeliver).toBeUndefined();
|
||||
|
||||
const schedule = job.schedule as Record<string, unknown>;
|
||||
expect(schedule.kind).toBe("at");
|
||||
expect(schedule.at).toBe(new Date(1_700_000_000_000).toISOString());
|
||||
expect(schedule.atMs).toBeUndefined();
|
||||
});
|
||||
|
||||
it("preserves stored custom session targets", () => {
|
||||
const { job } = normalizeOneJob(
|
||||
makeLegacyJob({
|
||||
id: "job-custom-session",
|
||||
name: "Custom session",
|
||||
schedule: { kind: "cron", expr: "0 23 * * *", tz: "UTC" },
|
||||
sessionTarget: "session:ProjectAlpha",
|
||||
payload: {
|
||||
kind: "agentTurn",
|
||||
message: "hello",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(job.sessionTarget).toBe("session:ProjectAlpha");
|
||||
expect(job.delivery).toEqual({ mode: "announce" });
|
||||
});
|
||||
|
||||
it("adds anchorMs to legacy every schedules", () => {
|
||||
const createdAtMs = 1_700_000_000_000;
|
||||
const { job } = normalizeOneJob(
|
||||
makeLegacyJob({
|
||||
id: "job-every-legacy",
|
||||
name: "Legacy every",
|
||||
createdAtMs,
|
||||
updatedAtMs: createdAtMs,
|
||||
schedule: { kind: "every", everyMs: 120_000 },
|
||||
}),
|
||||
);
|
||||
|
||||
const schedule = job.schedule as Record<string, unknown>;
|
||||
expect(schedule.kind).toBe("every");
|
||||
expect(schedule.anchorMs).toBe(createdAtMs);
|
||||
});
|
||||
|
||||
it("adds default staggerMs to legacy recurring top-of-hour cron schedules", () => {
|
||||
const { job } = normalizeOneJob(
|
||||
makeLegacyJob({
|
||||
id: "job-cron-legacy",
|
||||
name: "Legacy cron",
|
||||
schedule: { kind: "cron", expr: "0 */2 * * *", tz: "UTC" },
|
||||
}),
|
||||
);
|
||||
|
||||
const schedule = job.schedule as Record<string, unknown>;
|
||||
expect(schedule.kind).toBe("cron");
|
||||
expect(schedule.staggerMs).toBe(DEFAULT_TOP_OF_HOUR_STAGGER_MS);
|
||||
});
|
||||
|
||||
it("adds default staggerMs to legacy 6-field top-of-hour cron schedules", () => {
|
||||
const { job } = normalizeOneJob(
|
||||
makeLegacyJob({
|
||||
id: "job-cron-seconds-legacy",
|
||||
name: "Legacy cron seconds",
|
||||
schedule: { kind: "cron", expr: "0 0 */3 * * *", tz: "UTC" },
|
||||
}),
|
||||
);
|
||||
|
||||
const schedule = job.schedule as Record<string, unknown>;
|
||||
expect(schedule.kind).toBe("cron");
|
||||
expect(schedule.staggerMs).toBe(DEFAULT_TOP_OF_HOUR_STAGGER_MS);
|
||||
});
|
||||
|
||||
it("removes invalid legacy staggerMs from non top-of-hour cron schedules", () => {
|
||||
const { job } = normalizeOneJob(
|
||||
makeLegacyJob({
|
||||
id: "job-cron-minute-legacy",
|
||||
name: "Legacy minute cron",
|
||||
schedule: {
|
||||
kind: "cron",
|
||||
expr: "17 * * * *",
|
||||
tz: "UTC",
|
||||
staggerMs: "bogus",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const schedule = job.schedule as Record<string, unknown>;
|
||||
expect(schedule.kind).toBe("cron");
|
||||
expect(schedule.staggerMs).toBeUndefined();
|
||||
});
|
||||
|
||||
it("migrates legacy string schedules and command-only payloads (#18445)", () => {
|
||||
const { job, result } = normalizeOneJob({
|
||||
id: "imessage-refresh",
|
||||
name: "iMessage Refresh",
|
||||
enabled: true,
|
||||
createdAtMs: 1_700_000_000_000,
|
||||
updatedAtMs: 1_700_000_000_000,
|
||||
schedule: "0 */2 * * *",
|
||||
command: "bash /tmp/imessage-refresh.sh",
|
||||
timeout: 120,
|
||||
state: {},
|
||||
});
|
||||
|
||||
expect(result.mutated).toBe(true);
|
||||
expect(job.schedule).toEqual(
|
||||
expect.objectContaining({
|
||||
kind: "cron",
|
||||
expr: "0 */2 * * *",
|
||||
}),
|
||||
);
|
||||
expect(job.sessionTarget).toBe("main");
|
||||
expect(job.wakeMode).toBe("now");
|
||||
expect(job.payload).toEqual({
|
||||
kind: "systemEvent",
|
||||
text: "bash /tmp/imessage-refresh.sh",
|
||||
});
|
||||
expect("command" in job).toBe(false);
|
||||
expect("timeout" in job).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -182,9 +182,7 @@ describe("CronService interval/cron jobs fire on time", () => {
|
|||
const sfJob = jobs.find((job) => job.id === "legacy-every");
|
||||
expect(sfJob?.state.lastStatus).toBe("ok");
|
||||
expect(sfJob?.schedule.kind).toBe("every");
|
||||
if (sfJob?.schedule.kind === "every") {
|
||||
expect(sfJob.schedule.anchorMs).toBe(nowMs);
|
||||
}
|
||||
expect(sfJob?.state.nextRunAtMs).toBe(nowMs + 8 * 60_000);
|
||||
|
||||
cron.stop();
|
||||
await store.cleanup();
|
||||
|
|
|
|||
|
|
@ -32,9 +32,17 @@ export async function ensureLoaded(
|
|||
|
||||
const fileMtimeMs = await getFileMtimeMs(state.deps.storePath);
|
||||
const loaded = await loadCronStore(state.deps.storePath);
|
||||
const jobs = (loaded.jobs ?? []) as unknown as CronJob[];
|
||||
for (const job of jobs) {
|
||||
// Persisted legacy jobs may predate the required `enabled` field.
|
||||
// Keep runtime behavior backward-compatible without rewriting the store.
|
||||
if (typeof job.enabled !== "boolean") {
|
||||
job.enabled = true;
|
||||
}
|
||||
}
|
||||
state.store = {
|
||||
version: 1,
|
||||
jobs: (loaded.jobs ?? []) as unknown as CronJob[],
|
||||
jobs,
|
||||
};
|
||||
state.storeLoadedAtMs = state.deps.nowMs();
|
||||
state.storeFileMtimeMs = fileMtimeMs;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ const TASK_ROOT = path.resolve(import.meta.dirname);
|
|||
const SRC_ROOT = path.resolve(TASK_ROOT, "..");
|
||||
|
||||
const ALLOWED_IMPORTERS = new Set([
|
||||
"auto-reply/reply/commands-status.ts",
|
||||
"auto-reply/reply/commands-tasks.ts",
|
||||
"tasks/runtime-internal.ts",
|
||||
"tasks/task-owner-access.ts",
|
||||
"tasks/task-status-access.ts",
|
||||
|
|
|
|||
Loading…
Reference in New Issue