Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
286
extensions/feishu/src/post.ts
Normal file
286
extensions/feishu/src/post.ts
Normal file
@@ -0,0 +1,286 @@
|
||||
// Feishu plugin module implements post behavior.
|
||||
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { isRecord } from "./comment-shared.js";
|
||||
import { normalizeFeishuExternalKey } from "./external-keys.js";
|
||||
|
||||
const FALLBACK_POST_TEXT = "[Rich text message]";
|
||||
const MARKDOWN_SPECIAL_CHARS = /([\\`*_{}[\]()#+\-!|>~])/g;
|
||||
|
||||
type PostParseResult = {
|
||||
textContent: string;
|
||||
imageKeys: string[];
|
||||
mediaKeys: Array<{ fileKey: string; fileName?: string }>;
|
||||
mentionedOpenIds: string[];
|
||||
};
|
||||
|
||||
type PostPayload = {
|
||||
title: string;
|
||||
content: unknown[];
|
||||
};
|
||||
|
||||
function toStringOrEmpty(value: unknown): string {
|
||||
return typeof value === "string" ? value : "";
|
||||
}
|
||||
|
||||
function escapeMarkdownText(text: string): string {
|
||||
return text.replace(MARKDOWN_SPECIAL_CHARS, "\\$1");
|
||||
}
|
||||
|
||||
function toBoolean(value: unknown): boolean {
|
||||
return value === true || value === 1 || value === "true";
|
||||
}
|
||||
|
||||
function isStyleEnabled(style: Record<string, unknown> | undefined, key: string): boolean {
|
||||
if (!style) {
|
||||
return false;
|
||||
}
|
||||
return toBoolean(style[key]);
|
||||
}
|
||||
|
||||
function wrapInlineCode(text: string): string {
|
||||
const maxRun = Math.max(0, ...(text.match(/`+/g) ?? []).map((run) => run.length));
|
||||
const fence = "`".repeat(maxRun + 1);
|
||||
const needsPadding = text.startsWith("`") || text.endsWith("`");
|
||||
const body = needsPadding ? ` ${text} ` : text;
|
||||
return `${fence}${body}${fence}`;
|
||||
}
|
||||
|
||||
function sanitizeFenceLanguage(language: string): string {
|
||||
return language.trim().replace(/[^A-Za-z0-9_+#.-]/g, "");
|
||||
}
|
||||
|
||||
function renderTextElement(element: Record<string, unknown>): string {
|
||||
const text = toStringOrEmpty(element.text);
|
||||
const style = isRecord(element.style) ? element.style : undefined;
|
||||
|
||||
if (isStyleEnabled(style, "code")) {
|
||||
return wrapInlineCode(text);
|
||||
}
|
||||
|
||||
let rendered = escapeMarkdownText(text);
|
||||
if (!rendered) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (isStyleEnabled(style, "bold")) {
|
||||
rendered = `**${rendered}**`;
|
||||
}
|
||||
if (isStyleEnabled(style, "italic")) {
|
||||
rendered = `*${rendered}*`;
|
||||
}
|
||||
if (isStyleEnabled(style, "underline")) {
|
||||
rendered = `<u>${rendered}</u>`;
|
||||
}
|
||||
if (
|
||||
isStyleEnabled(style, "strikethrough") ||
|
||||
isStyleEnabled(style, "line_through") ||
|
||||
isStyleEnabled(style, "lineThrough")
|
||||
) {
|
||||
rendered = `~~${rendered}~~`;
|
||||
}
|
||||
return rendered;
|
||||
}
|
||||
|
||||
function renderLinkElement(element: Record<string, unknown>): string {
|
||||
const href = toStringOrEmpty(element.href).trim();
|
||||
const rawText = toStringOrEmpty(element.text);
|
||||
const text = rawText || href;
|
||||
if (!text) {
|
||||
return "";
|
||||
}
|
||||
if (!href) {
|
||||
return escapeMarkdownText(text);
|
||||
}
|
||||
return `[${escapeMarkdownText(text)}](${href})`;
|
||||
}
|
||||
|
||||
function renderMentionElement(element: Record<string, unknown>): string {
|
||||
const mention =
|
||||
toStringOrEmpty(element.user_name) ||
|
||||
toStringOrEmpty(element.user_id) ||
|
||||
toStringOrEmpty(element.open_id);
|
||||
if (!mention) {
|
||||
return "";
|
||||
}
|
||||
return `@${escapeMarkdownText(mention)}`;
|
||||
}
|
||||
|
||||
function renderEmotionElement(element: Record<string, unknown>): string {
|
||||
const text =
|
||||
toStringOrEmpty(element.emoji) ||
|
||||
toStringOrEmpty(element.text) ||
|
||||
toStringOrEmpty(element.emoji_type);
|
||||
return escapeMarkdownText(text);
|
||||
}
|
||||
|
||||
function renderCodeBlockElement(element: Record<string, unknown>): string {
|
||||
const language = sanitizeFenceLanguage(
|
||||
toStringOrEmpty(element.language) || toStringOrEmpty(element.lang),
|
||||
);
|
||||
const code = (toStringOrEmpty(element.text) || toStringOrEmpty(element.content)).replace(
|
||||
/\r\n/g,
|
||||
"\n",
|
||||
);
|
||||
const trailingNewline = code.endsWith("\n") ? "" : "\n";
|
||||
return `\`\`\`${language}\n${code}${trailingNewline}\`\`\``;
|
||||
}
|
||||
|
||||
function renderElement(
|
||||
element: unknown,
|
||||
imageKeys: string[],
|
||||
mediaKeys: Array<{ fileKey: string; fileName?: string }>,
|
||||
mentionedOpenIds: string[],
|
||||
renderMediaPlaceholders: boolean,
|
||||
): string {
|
||||
if (!isRecord(element)) {
|
||||
return escapeMarkdownText(toStringOrEmpty(element));
|
||||
}
|
||||
|
||||
const tag = normalizeLowercaseStringOrEmpty(toStringOrEmpty(element.tag));
|
||||
switch (tag) {
|
||||
case "text":
|
||||
return renderTextElement(element);
|
||||
case "a":
|
||||
return renderLinkElement(element);
|
||||
case "at":
|
||||
{
|
||||
const mentioned = toStringOrEmpty(element.open_id) || toStringOrEmpty(element.user_id);
|
||||
const normalizedMention = normalizeFeishuExternalKey(mentioned);
|
||||
if (normalizedMention) {
|
||||
mentionedOpenIds.push(normalizedMention);
|
||||
}
|
||||
}
|
||||
return renderMentionElement(element);
|
||||
case "img": {
|
||||
const imageKey = normalizeFeishuExternalKey(toStringOrEmpty(element.image_key));
|
||||
if (imageKey) {
|
||||
imageKeys.push(imageKey);
|
||||
}
|
||||
return renderMediaPlaceholders ? "![image]" : "";
|
||||
}
|
||||
case "media": {
|
||||
const fileKey = normalizeFeishuExternalKey(toStringOrEmpty(element.file_key));
|
||||
if (fileKey) {
|
||||
const fileName = toStringOrEmpty(element.file_name) || undefined;
|
||||
mediaKeys.push({ fileKey, fileName });
|
||||
}
|
||||
return renderMediaPlaceholders ? "[media]" : "";
|
||||
}
|
||||
case "emotion":
|
||||
return renderEmotionElement(element);
|
||||
case "md":
|
||||
case "lark_md":
|
||||
return toStringOrEmpty(element.text) || toStringOrEmpty(element.content);
|
||||
case "br":
|
||||
return "\n";
|
||||
case "hr":
|
||||
return "\n\n---\n\n";
|
||||
case "code": {
|
||||
const code = toStringOrEmpty(element.text) || toStringOrEmpty(element.content);
|
||||
return code ? wrapInlineCode(code) : "";
|
||||
}
|
||||
case "code_block":
|
||||
case "pre":
|
||||
return renderCodeBlockElement(element);
|
||||
default:
|
||||
return escapeMarkdownText(toStringOrEmpty(element.text));
|
||||
}
|
||||
}
|
||||
|
||||
function toPostPayload(candidate: unknown): PostPayload | null {
|
||||
if (!isRecord(candidate) || !Array.isArray(candidate.content)) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
title: toStringOrEmpty(candidate.title),
|
||||
content: candidate.content,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveLocalePayload(candidate: unknown): PostPayload | null {
|
||||
const direct = toPostPayload(candidate);
|
||||
if (direct) {
|
||||
return direct;
|
||||
}
|
||||
if (!isRecord(candidate)) {
|
||||
return null;
|
||||
}
|
||||
for (const value of Object.values(candidate)) {
|
||||
const localePayload = toPostPayload(value);
|
||||
if (localePayload) {
|
||||
return localePayload;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolvePostPayload(parsed: unknown): PostPayload | null {
|
||||
const direct = toPostPayload(parsed);
|
||||
if (direct) {
|
||||
return direct;
|
||||
}
|
||||
|
||||
if (!isRecord(parsed)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const wrappedPost = resolveLocalePayload(parsed.post);
|
||||
if (wrappedPost) {
|
||||
return wrappedPost;
|
||||
}
|
||||
|
||||
return resolveLocalePayload(parsed);
|
||||
}
|
||||
|
||||
export function parsePostContent(
|
||||
content: string,
|
||||
options: { renderMediaPlaceholders?: boolean; emptyTextFallback?: string } = {},
|
||||
): PostParseResult {
|
||||
try {
|
||||
const parsed = JSON.parse(content);
|
||||
const payload = resolvePostPayload(parsed);
|
||||
if (!payload) {
|
||||
return {
|
||||
textContent: FALLBACK_POST_TEXT,
|
||||
imageKeys: [],
|
||||
mediaKeys: [],
|
||||
mentionedOpenIds: [],
|
||||
};
|
||||
}
|
||||
|
||||
const imageKeys: string[] = [];
|
||||
const mediaKeys: Array<{ fileKey: string; fileName?: string }> = [];
|
||||
const mentionedOpenIds: string[] = [];
|
||||
const paragraphs: string[] = [];
|
||||
|
||||
for (const paragraph of payload.content) {
|
||||
if (!Array.isArray(paragraph)) {
|
||||
continue;
|
||||
}
|
||||
let renderedParagraph = "";
|
||||
for (const element of paragraph) {
|
||||
renderedParagraph += renderElement(
|
||||
element,
|
||||
imageKeys,
|
||||
mediaKeys,
|
||||
mentionedOpenIds,
|
||||
options.renderMediaPlaceholders !== false,
|
||||
);
|
||||
}
|
||||
paragraphs.push(renderedParagraph);
|
||||
}
|
||||
|
||||
const title = escapeMarkdownText(payload.title.trim());
|
||||
const body = paragraphs.join("\n").trim();
|
||||
const textContent = [title, body].filter(Boolean).join("\n\n").trim();
|
||||
|
||||
return {
|
||||
textContent: textContent || (options.emptyTextFallback ?? FALLBACK_POST_TEXT),
|
||||
imageKeys,
|
||||
mediaKeys,
|
||||
mentionedOpenIds,
|
||||
};
|
||||
} catch {
|
||||
return { textContent: FALLBACK_POST_TEXT, imageKeys: [], mediaKeys: [], mentionedOpenIds: [] };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user