mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-01 14:50:21 +00:00
154 lines
4.1 KiB
TypeScript
154 lines
4.1 KiB
TypeScript
/**
|
|
* Colored text support for Feishu documents.
|
|
*
|
|
* Parses a simple color markup syntax and updates a text block
|
|
* with native Feishu text_run color styles.
|
|
*
|
|
* Syntax: [color]text[/color]
|
|
* Supported colors: red, orange, yellow, green, blue, purple, grey
|
|
*
|
|
* Example:
|
|
* "Revenue [green]+15%[/green] YoY, Costs [red]-3%[/red]"
|
|
*/
|
|
|
|
import type * as Lark from "@larksuiteoapi/node-sdk";
|
|
|
|
// Feishu text_color values (1-7)
|
|
const TEXT_COLOR: Record<string, number> = {
|
|
red: 1, // Pink (closest to red in Feishu)
|
|
orange: 2,
|
|
yellow: 3,
|
|
green: 4,
|
|
blue: 5,
|
|
purple: 6,
|
|
grey: 7,
|
|
gray: 7,
|
|
};
|
|
|
|
// Feishu background_color values (1-15)
|
|
const BACKGROUND_COLOR: Record<string, number> = {
|
|
red: 1,
|
|
orange: 2,
|
|
yellow: 3,
|
|
green: 4,
|
|
blue: 5,
|
|
purple: 6,
|
|
grey: 7,
|
|
gray: 7,
|
|
};
|
|
|
|
interface Segment {
|
|
text: string;
|
|
textColor?: number;
|
|
bgColor?: number;
|
|
bold?: boolean;
|
|
}
|
|
|
|
type DocxPatchPayload = NonNullable<Parameters<Lark.Client["docx"]["documentBlock"]["patch"]>[0]>;
|
|
type DocxTextElement = NonNullable<
|
|
NonNullable<NonNullable<DocxPatchPayload["data"]>["update_text_elements"]>["elements"]
|
|
>[number];
|
|
|
|
/**
|
|
* Parse color markup into segments.
|
|
*
|
|
* Supports:
|
|
* [red]text[/red] → red text
|
|
* [bg:yellow]text[/bg] → yellow background
|
|
* [bold]text[/bold] → bold
|
|
* [green bold]text[/green] → green + bold
|
|
*/
|
|
export function parseColorMarkup(content: string): Segment[] {
|
|
const segments: Segment[] = [];
|
|
// Only [known_tag]...[/...] pairs are treated as markup. Using an open
|
|
// pattern like \[([^\]]+)\] would match any bracket token — e.g. [Q1] —
|
|
// and cause it to consume a later real closing tag ([/red]), silently
|
|
// corrupting the surrounding styled spans. Restricting the opening tag to
|
|
// the set of recognised colour/style names prevents that: [Q1] does not
|
|
// match the tag alternative and each of its characters falls through to the
|
|
// plain-text alternatives instead.
|
|
//
|
|
// Closing tag name is still not validated against the opening tag:
|
|
// [red]text[/green] is treated as [red]text[/red] — opening style applies
|
|
// and the closing tag is consumed regardless of its name.
|
|
const KNOWN = "(?:bg:[a-z]+|bold|red|orange|yellow|green|blue|purple|gr[ae]y)";
|
|
const tagPattern = new RegExp(
|
|
`\\[(${KNOWN}(?:\\s+${KNOWN})*)\\](.*?)\\[\\/(?:[^\\]]+)\\]|([^[]+|\\[)`,
|
|
"gis",
|
|
);
|
|
let match;
|
|
|
|
while ((match = tagPattern.exec(content)) !== null) {
|
|
if (match[3] !== undefined) {
|
|
// Plain text segment
|
|
if (match[3]) {
|
|
segments.push({ text: match[3] });
|
|
}
|
|
} else {
|
|
// Tagged segment
|
|
const tagStr = match[1].toLowerCase().trim();
|
|
const text = match[2];
|
|
const tags = tagStr.split(/\s+/);
|
|
|
|
const segment: Segment = { text };
|
|
|
|
for (const tag of tags) {
|
|
if (tag.startsWith("bg:")) {
|
|
const color = tag.slice(3);
|
|
if (BACKGROUND_COLOR[color]) {
|
|
segment.bgColor = BACKGROUND_COLOR[color];
|
|
}
|
|
} else if (tag === "bold") {
|
|
segment.bold = true;
|
|
} else if (TEXT_COLOR[tag]) {
|
|
segment.textColor = TEXT_COLOR[tag];
|
|
}
|
|
}
|
|
|
|
if (text) {
|
|
segments.push(segment);
|
|
}
|
|
}
|
|
}
|
|
|
|
return segments;
|
|
}
|
|
|
|
/**
|
|
* Update a text block with colored segments.
|
|
*/
|
|
export async function updateColorText(
|
|
client: Lark.Client,
|
|
docToken: string,
|
|
blockId: string,
|
|
content: string,
|
|
) {
|
|
const segments = parseColorMarkup(content);
|
|
|
|
const elements: DocxTextElement[] = segments.map((seg) => ({
|
|
text_run: {
|
|
content: seg.text,
|
|
text_element_style: {
|
|
...(seg.textColor && { text_color: seg.textColor }),
|
|
...(seg.bgColor && { background_color: seg.bgColor }),
|
|
...(seg.bold && { bold: true }),
|
|
},
|
|
},
|
|
}));
|
|
|
|
const res = await client.docx.documentBlock.patch({
|
|
path: { document_id: docToken, block_id: blockId },
|
|
data: { update_text_elements: { elements } },
|
|
});
|
|
|
|
if (res.code !== 0) {
|
|
throw new Error(res.msg);
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
segments: segments.length,
|
|
block: res.data?.block,
|
|
};
|
|
}
|