mirror of
https://github.com/agentscope-ai/ReMe.git
synced 2026-10-13 04:01:35 +00:00
fix(dsh): preserve exact host validation with serializable forms
This commit is contained in:
parent
75c9e23f72
commit
c723544ba1
2 changed files with 82 additions and 44 deletions
|
|
@ -3,31 +3,19 @@ import z from "@deepseek-ai/schemastery";
|
|||
import { validTimezone, validateDailyCron } from "./scheduling.js";
|
||||
import type { ReMeConfig, ReMeConfigInput } from "./types.js";
|
||||
|
||||
// DSH sends volatile schemas to the browser as JSON, so these checks must serialize without callbacks.
|
||||
const HTTP_ENDPOINT =
|
||||
/^https?:\/\/(?:\[[0-9a-fA-F:.]+\]|[^\s/?#:@]+)(?::\d{1,5})?(?:[/?#][^\s]*)?$/;
|
||||
const DAILY_CRON = /^(?:[0-5]?\d)\s+(?:[01]?\d|2[0-3])\s+\*\s+\*\s+\*$/;
|
||||
const TIMEZONES = [
|
||||
...Intl.supportedValuesOf("timeZone"),
|
||||
"UTC",
|
||||
"Etc/UTC",
|
||||
"GMT",
|
||||
"Etc/GMT",
|
||||
];
|
||||
const TIMEZONE = new RegExp(`^(?:${TIMEZONES.map(escapeRegExp).join("|")})$`);
|
||||
const DEFAULT_ENDPOINT =
|
||||
process.env.REME_URL ||
|
||||
`http://${process.env.REME_HOST || "127.0.0.1"}:${
|
||||
process.env.REME_PORT || "2333"
|
||||
}`;
|
||||
const DEFAULT_DREAM_CRON = process.env.REME_DSH_DREAM_CRON || "0 23 * * *";
|
||||
|
||||
export const Config = z.object({
|
||||
endpoint: z
|
||||
.string()
|
||||
.pattern(HTTP_ENDPOINT)
|
||||
.description("ReMe HTTP service URL")
|
||||
.default(
|
||||
process.env.REME_URL ||
|
||||
`http://${process.env.REME_HOST || "127.0.0.1"}:${
|
||||
process.env.REME_PORT || "2333"
|
||||
}`,
|
||||
)
|
||||
.volatile(),
|
||||
endpoint: checkedString(
|
||||
DEFAULT_ENDPOINT,
|
||||
"ReMe HTTP service URL",
|
||||
assertEndpoint,
|
||||
),
|
||||
requestTimeoutMs: z.natural().min(1000).max(120000).default(10000).volatile(),
|
||||
backgroundTimeoutMs: z
|
||||
.natural()
|
||||
|
|
@ -39,27 +27,47 @@ export const Config = z.object({
|
|||
autoMemoryEnabled: z.boolean().default(true).volatile(),
|
||||
autoMemoryInterval: z.natural().min(1).max(1000).default(5).volatile(),
|
||||
autoDreamEnabled: z.boolean().default(true).volatile(),
|
||||
dreamCron: z
|
||||
.string()
|
||||
.pattern(DAILY_CRON)
|
||||
.description("Daily cron in the workspace timezone")
|
||||
.default(process.env.REME_DSH_DREAM_CRON || "0 23 * * *")
|
||||
.volatile(),
|
||||
dreamCron: checkedString(
|
||||
DEFAULT_DREAM_CRON,
|
||||
"Daily cron in the workspace timezone",
|
||||
validateDailyCron,
|
||||
),
|
||||
dreamHint: z.string().default("").volatile(),
|
||||
dreamIntervalMs: z.natural().max(2147483647).default(0),
|
||||
rootAgentsOnly: z.boolean().default(true).volatile(),
|
||||
language: z.union(["en", "zh"]).default("en").volatile(),
|
||||
searchLimit: z.natural().min(1).max(50).default(5).volatile(),
|
||||
timezone: z
|
||||
.string()
|
||||
.pattern(TIMEZONE)
|
||||
.default("Asia/Shanghai")
|
||||
.description("IANA timezone matching the ReMe workspace")
|
||||
.volatile(),
|
||||
timezone: checkedString(
|
||||
"Asia/Shanghai",
|
||||
"IANA timezone matching the ReMe workspace",
|
||||
(value) => {
|
||||
if (!validTimezone(value))
|
||||
throw new TypeError(`Invalid ReMe timezone: ${value}`);
|
||||
},
|
||||
),
|
||||
});
|
||||
|
||||
function escapeRegExp(value: string): string {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
function checkedString(
|
||||
defaultValue: string,
|
||||
description: string,
|
||||
check: (value: string) => void,
|
||||
) {
|
||||
const form = z
|
||||
.string()
|
||||
.description(description)
|
||||
.default(defaultValue)
|
||||
.volatile();
|
||||
const host = z
|
||||
.transform(z.string(), (value) => {
|
||||
check(value);
|
||||
return value;
|
||||
})
|
||||
.description(description)
|
||||
.default(defaultValue)
|
||||
.volatile();
|
||||
// DSH sends Config.toJSON() to the browser; keep Host validation while serializing a plain form field.
|
||||
host.toJSON = () => form.toJSON();
|
||||
return host;
|
||||
}
|
||||
|
||||
const DEFAULT_CONFIG: Readonly<ReMeConfig> = Object.freeze({
|
||||
|
|
|
|||
|
|
@ -30,10 +30,24 @@ test("exports a Cordis schema that rejects invalid configuration", async () => {
|
|||
for (const input of [
|
||||
{ endpoint: "not-a-url" },
|
||||
{ endpoint: "http://localhost:bad-port" },
|
||||
{ endpoint: "http://localhost:65536" },
|
||||
{ endpoint: "http://[:::]" },
|
||||
{ endpoint: "http://999.0.0.1" },
|
||||
{ dreamCron: "every night" },
|
||||
{ timezone: "Mars/Olympus" },
|
||||
]) {
|
||||
assert.ok(Config["~standard"].validate(input).issues?.length);
|
||||
assert.throws(() => Config["~standard"].validate(input));
|
||||
}
|
||||
|
||||
for (const timezone of [
|
||||
"US/Pacific",
|
||||
"US/Eastern",
|
||||
"Etc/GMT+8",
|
||||
"Etc/GMT-8",
|
||||
]) {
|
||||
const result = Config["~standard"].validate({ timezone });
|
||||
assert.equal(result.issues, undefined);
|
||||
assert.equal(result.value.timezone.get(), timezone);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -51,9 +65,10 @@ test("volatile settings survive Host projection and browser schema rehydration",
|
|||
fields.map(([name]) => [name, defaults[name].get()]),
|
||||
);
|
||||
assert.equal(browserForm["~standard"].validate(values).issues, undefined);
|
||||
assert.ok(
|
||||
browserForm["~standard"].validate({ ...values, dreamCron: "bad" }).issues
|
||||
?.length,
|
||||
assert.equal(
|
||||
browserForm["~standard"].validate({ ...values, timezone: "US/Pacific" })
|
||||
.issues,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -116,28 +131,43 @@ test("Loader keeps the previous live values when an update fails schema validati
|
|||
const id = await ctx.loader.create({
|
||||
id: "reme-memory",
|
||||
name: "cordis:remeTest",
|
||||
config: { endpoint: "http://valid.test", autoDreamEnabled: false },
|
||||
config: {
|
||||
endpoint: "http://valid.test",
|
||||
autoMemoryEnabled: false,
|
||||
autoDreamEnabled: false,
|
||||
timezone: "US/Pacific",
|
||||
},
|
||||
});
|
||||
const entry = ctx.loader.resolve(id);
|
||||
await entry.fiber.await();
|
||||
for (const patch of [
|
||||
{ endpoint: "not-a-url" },
|
||||
{ endpoint: "http://localhost:65536" },
|
||||
{ endpoint: "http://999.0.0.1" },
|
||||
{ dreamCron: "every night" },
|
||||
{ timezone: "Mars/Olympus" },
|
||||
]) {
|
||||
await entry.update({
|
||||
config: {
|
||||
endpoint: "http://valid.test",
|
||||
autoMemoryEnabled: true,
|
||||
autoDreamEnabled: false,
|
||||
timezone: "US/Pacific",
|
||||
...patch,
|
||||
},
|
||||
});
|
||||
assert.equal(live.endpoint.get(), "http://valid.test");
|
||||
assert.equal(live.autoMemoryEnabled.get(), false);
|
||||
assert.equal(live.dreamCron.get(), "0 23 * * *");
|
||||
assert.equal(live.timezone.get(), "Asia/Shanghai");
|
||||
assert.equal(live.timezone.get(), "US/Pacific");
|
||||
}
|
||||
await entry.update({
|
||||
config: { endpoint: "http://updated.test", autoDreamEnabled: false },
|
||||
config: {
|
||||
endpoint: "http://updated.test",
|
||||
autoMemoryEnabled: false,
|
||||
autoDreamEnabled: false,
|
||||
timezone: "US/Pacific",
|
||||
},
|
||||
});
|
||||
assert.equal(live.endpoint.get(), "http://updated.test");
|
||||
} finally {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue