mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 12:26:38 -06:00
fix(line): drop blank entries from trailing commas in agenda/device directives (#105381)
* fix(line): drop blank entries from trailing commas in agenda/device directives * fix(line): drop blank device action labels Co-authored-by: Eden <146086744+edenfunf@users.noreply.github.com> --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -381,6 +381,54 @@ describe("parseLineDirectives", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("blank comma-separated entries", () => {
|
||||
// A trailing or doubled comma in agent output must not emit empty Flex text
|
||||
// components (agenda) or empty action labels (device); LINE rejects those with
|
||||
// HTTP 400 "May not be empty".
|
||||
const collectRenderedStrings = (node: unknown, out: string[]): string[] => {
|
||||
if (Array.isArray(node)) {
|
||||
for (const item of node) {
|
||||
collectRenderedStrings(item, out);
|
||||
}
|
||||
} else if (node && typeof node === "object") {
|
||||
const record = node as Record<string, unknown>;
|
||||
if (record.type === "text" && typeof record.text === "string") {
|
||||
out.push(record.text);
|
||||
}
|
||||
if (typeof record.label === "string") {
|
||||
out.push(record.label);
|
||||
}
|
||||
for (const value of Object.values(record)) {
|
||||
collectRenderedStrings(value, out);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
it("drops trailing commas in agenda events", () => {
|
||||
const result = parseLineDirectives({ text: "[[agenda: Today | Standup:9am, Lunch:12pm,]]" });
|
||||
const flexMessage = requireFlexMessage(
|
||||
getLineData(result).flexMessage,
|
||||
"agenda trailing comma",
|
||||
);
|
||||
expect(flexMessage.altText).toBe("📋 Today (2 events)");
|
||||
expect(collectRenderedStrings(flexMessage.contents, [])).not.toContain("");
|
||||
});
|
||||
|
||||
it("drops doubled commas and blank labels in device controls", () => {
|
||||
const result = parseLineDirectives({
|
||||
text: "[[device: TV | Box | Playing | Play:toggle,, :ignored, Menu:menu]]",
|
||||
});
|
||||
const flexMessage = requireFlexMessage(
|
||||
getLineData(result).flexMessage,
|
||||
"device blank controls",
|
||||
);
|
||||
const renderedStrings = collectRenderedStrings(flexMessage.contents, []);
|
||||
expect(renderedStrings).toEqual(expect.arrayContaining(["Play", "Menu"]));
|
||||
expect(renderedStrings).not.toContain("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("device", () => {
|
||||
it("parses device variants", () => {
|
||||
const cases = [
|
||||
|
||||
@@ -287,7 +287,9 @@ export function parseLineDirectives(payload: ReplyPayload): ReplyPayload {
|
||||
if (parts.length >= 2) {
|
||||
const title = expectDefined(parts[0], "agenda title field");
|
||||
const eventsStr = expectDefined(parts[1], "agenda events field");
|
||||
const events = eventsStr.split(",").map((eventStr) => {
|
||||
// normalizeStringEntries drops blank entries from trailing/double commas; an empty
|
||||
// event title becomes an empty Flex text component, which LINE rejects with HTTP 400.
|
||||
const events = normalizeStringEntries(eventsStr.split(",")).map((eventStr) => {
|
||||
const trimmed = eventStr.trim();
|
||||
const colonIdx = trimmed.lastIndexOf(":");
|
||||
if (colonIdx > 0) {
|
||||
@@ -321,12 +323,16 @@ export function parseLineDirectives(payload: ReplyPayload): ReplyPayload {
|
||||
const [, deviceType, status, controlsStr] = parts;
|
||||
const deviceKey = toSlug(deviceName || "device");
|
||||
const controls = controlsStr
|
||||
? controlsStr.split(",").map((ctrlStr) => {
|
||||
? normalizeStringEntries(controlsStr.split(",")).flatMap((ctrlStr) => {
|
||||
const controlParts = ctrlStr.split(":").map((s) => s.trim());
|
||||
const label = expectDefined(controlParts[0], "device control label");
|
||||
// A nonempty raw entry can still parse to `:data`; LINE rejects a blank action label.
|
||||
if (!label) {
|
||||
return [];
|
||||
}
|
||||
const data = controlParts[1];
|
||||
const action = data || normalizeLowercaseStringOrEmpty(label).replace(/\s+/g, "_");
|
||||
return { label, data: lineActionData(action, { "line.device": deviceKey }) };
|
||||
return [{ label, data: lineActionData(action, { "line.device": deviceKey }) }];
|
||||
})
|
||||
: [];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user