auto: sync OpenClaw config 2026-09-09 16:13

This commit is contained in:
2026-09-09 16:13:31 +08:00
parent c79d73c0fe
commit 0aa13d3cf0
440 changed files with 51636 additions and 13 deletions
@@ -0,0 +1,8 @@
/**
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*
* Converter for "audio" message type.
*/
import type { ContentConverterFn } from './types';
export declare const convertAudio: ContentConverterFn;
@@ -0,0 +1,24 @@
"use strict";
/**
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*
* Converter for "audio" message type.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.convertAudio = void 0;
const utils_1 = require("./utils.js");
const convertAudio = (raw) => {
const parsed = (0, utils_1.safeParse)(raw);
const fileKey = parsed?.file_key;
if (!fileKey) {
return { content: '[audio]', resources: [] };
}
const duration = parsed?.duration;
const durationAttr = duration != null ? ` duration="${(0, utils_1.formatDuration)(duration)}"` : '';
return {
content: `<audio key="${fileKey}"${durationAttr}/>`,
resources: [{ type: 'audio', fileKey, duration: duration ?? undefined }],
};
};
exports.convertAudio = convertAudio;
@@ -0,0 +1,13 @@
/**
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*
* Converters for calendar-related message types:
* - share_calendar_event
* - calendar
* - general_calendar
*/
import type { ContentConverterFn } from './types';
export declare const convertShareCalendarEvent: ContentConverterFn;
export declare const convertCalendar: ContentConverterFn;
export declare const convertGeneralCalendar: ContentConverterFn;
@@ -0,0 +1,56 @@
"use strict";
/**
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*
* Converters for calendar-related message types:
* - share_calendar_event
* - calendar
* - general_calendar
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.convertGeneralCalendar = exports.convertCalendar = exports.convertShareCalendarEvent = void 0;
const utils_1 = require("./utils.js");
function formatCalendarContent(parsed) {
const summary = parsed?.summary ?? '';
const parts = [];
if (summary) {
parts.push(`📅 ${summary}`);
}
const start = parsed?.start_time ? (0, utils_1.millisToDatetime)(parsed.start_time) : '';
const end = parsed?.end_time ? (0, utils_1.millisToDatetime)(parsed.end_time) : '';
if (start && end) {
parts.push(`🕙 ${start} ~ ${end}`);
}
else if (start) {
parts.push(`🕙 ${start}`);
}
return parts.join('\n') || '[calendar event]';
}
const convertShareCalendarEvent = (raw) => {
const parsed = (0, utils_1.safeParse)(raw);
const inner = formatCalendarContent(parsed);
return {
content: `<calendar_share>${inner}</calendar_share>`,
resources: [],
};
};
exports.convertShareCalendarEvent = convertShareCalendarEvent;
const convertCalendar = (raw) => {
const parsed = (0, utils_1.safeParse)(raw);
const inner = formatCalendarContent(parsed);
return {
content: `<calendar_invite>${inner}</calendar_invite>`,
resources: [],
};
};
exports.convertCalendar = convertCalendar;
const convertGeneralCalendar = (raw) => {
const parsed = (0, utils_1.safeParse)(raw);
const inner = formatCalendarContent(parsed);
return {
content: `<calendar>${inner}</calendar>`,
resources: [],
};
};
exports.convertGeneralCalendar = convertGeneralCalendar;
@@ -0,0 +1,30 @@
/**
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*
* Shared helper functions for Feishu content converters.
*/
import type { ApiMessageItem, ConvertContext } from './types';
/** 从 mention 的 id 字段提取 open_id(兼容事件推送的对象格式和 API 响应的字符串格式) */
export declare function extractMentionOpenId(id: unknown): string;
/**
* Build a {@link ConvertContext} from a raw Feishu API message item.
*
* Extracts the `mentions` array that the IM API returns on each message
* item and maps it into the key→MentionInfo / openId→MentionInfo
* structures the converter system expects.
*/
export declare function buildConvertContextFromItem(item: ApiMessageItem, fallbackMessageId: string, accountId?: string): ConvertContext;
/**
* Resolve mention placeholders in text.
*
* - Bot self-mention + stripBotMentions: leading-only strip. When the
* self-mention sits at the very start of the message, drop it (the
* `WasMentioned` envelope field already tells the agent "this message
* was addressed to you", so the anchor is redundant). When it appears
* mid-text, render it as plain `@Name` so the surrounding context still
* reads naturally without leaving an inline anchor the LLM might echo
* back into its reply.
* - Non-bot mentions: replace the placeholder key with readable `@name`.
*/
export declare function resolveMentions(text: string, ctx: ConvertContext): string;
@@ -0,0 +1,82 @@
"use strict";
/**
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*
* Shared helper functions for Feishu content converters.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.extractMentionOpenId = extractMentionOpenId;
exports.buildConvertContextFromItem = buildConvertContextFromItem;
exports.resolveMentions = resolveMentions;
const user_name_cache_1 = require("../inbound/user-name-cache.js");
const utils_1 = require("./utils.js");
/** 从 mention 的 id 字段提取 open_id(兼容事件推送的对象格式和 API 响应的字符串格式) */
function extractMentionOpenId(id) {
if (typeof id === 'string')
return id;
if (id != null && typeof id === 'object' && 'open_id' in id) {
const openId = id.open_id;
return typeof openId === 'string' ? openId : '';
}
return '';
}
/**
* Build a {@link ConvertContext} from a raw Feishu API message item.
*
* Extracts the `mentions` array that the IM API returns on each message
* item and maps it into the key→MentionInfo / openId→MentionInfo
* structures the converter system expects.
*/
function buildConvertContextFromItem(item, fallbackMessageId, accountId) {
const mentions = new Map();
const mentionsByOpenId = new Map();
for (const m of item.mentions ?? []) {
const openId = extractMentionOpenId(m.id);
if (!openId)
continue;
const info = {
key: m.key,
openId,
name: m.name ?? '',
isBot: false,
};
mentions.set(m.key, info);
mentionsByOpenId.set(openId, info);
}
return {
mentions,
mentionsByOpenId,
messageId: item.message_id ?? fallbackMessageId,
accountId,
resolveUserName: accountId ? (openId) => (0, user_name_cache_1.getUserNameCache)(accountId).get(openId) : undefined,
};
}
/**
* Resolve mention placeholders in text.
*
* - Bot self-mention + stripBotMentions: leading-only strip. When the
* self-mention sits at the very start of the message, drop it (the
* `WasMentioned` envelope field already tells the agent "this message
* was addressed to you", so the anchor is redundant). When it appears
* mid-text, render it as plain `@Name` so the surrounding context still
* reads naturally without leaving an inline anchor the LLM might echo
* back into its reply.
* - Non-bot mentions: replace the placeholder key with readable `@name`.
*/
function resolveMentions(text, ctx) {
if (ctx.mentions.size === 0)
return text;
let result = text;
for (const [key, info] of ctx.mentions) {
if (info.isBot && ctx.stripBotMentions) {
result = result.replace(new RegExp((0, utils_1.escapeRegExp)(key), 'g'), `@${info.name}`);
const leadingPattern = new RegExp(`^\\s*@${(0, utils_1.escapeRegExp)(info.name)}[\\s,:]*`);
result = result.replace(leadingPattern, '').trimStart();
}
else {
result = result.replace(new RegExp((0, utils_1.escapeRegExp)(key), 'g'), `@${info.name}`);
}
}
return result;
}
@@ -0,0 +1,24 @@
/**
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*
* Content converter for Feishu messages.
*
* Each message type (text, post, image, etc.) has a dedicated converter
* function that parses raw JSON content into an AI-friendly text
* representation plus a list of resource descriptors.
*
* This module is a general-purpose message parsing utility — usable
* from inbound handling, outbound formatting, and skills.
*/
import type { ConvertContext, ConvertResult } from './types';
export type { ApiMessageItem, ConvertContext, ConvertResult, ContentConverterFn } from './types';
export { buildConvertContextFromItem, extractMentionOpenId, resolveMentions } from './content-converter-helpers';
/**
* Convert raw message content using the converter for the given message
* type. Falls back to the "unknown" converter for unrecognised types.
*
* Returns a Promise because some converters (e.g. merge_forward) perform
* async operations. Synchronous converters are awaited transparently.
*/
export declare function convertMessageContent(raw: string, messageType: string, ctx: ConvertContext): Promise<ConvertResult>;
@@ -0,0 +1,40 @@
"use strict";
/**
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*
* Content converter for Feishu messages.
*
* Each message type (text, post, image, etc.) has a dedicated converter
* function that parses raw JSON content into an AI-friendly text
* representation plus a list of resource descriptors.
*
* This module is a general-purpose message parsing utility — usable
* from inbound handling, outbound formatting, and skills.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.resolveMentions = exports.extractMentionOpenId = exports.buildConvertContextFromItem = void 0;
exports.convertMessageContent = convertMessageContent;
const index_1 = require("./index.js");
var content_converter_helpers_1 = require("./content-converter-helpers.js");
Object.defineProperty(exports, "buildConvertContextFromItem", { enumerable: true, get: function () { return content_converter_helpers_1.buildConvertContextFromItem; } });
Object.defineProperty(exports, "extractMentionOpenId", { enumerable: true, get: function () { return content_converter_helpers_1.extractMentionOpenId; } });
Object.defineProperty(exports, "resolveMentions", { enumerable: true, get: function () { return content_converter_helpers_1.resolveMentions; } });
// ---------------------------------------------------------------------------
// Convert
// ---------------------------------------------------------------------------
/**
* Convert raw message content using the converter for the given message
* type. Falls back to the "unknown" converter for unrecognised types.
*
* Returns a Promise because some converters (e.g. merge_forward) perform
* async operations. Synchronous converters are awaited transparently.
*/
async function convertMessageContent(raw, messageType, ctx) {
const fn = index_1.converters.get(messageType) ?? index_1.converters.get('unknown');
if (!fn) {
return { content: raw, resources: [] };
}
const nextCtx = ctx.convertMessageContent ? ctx : { ...ctx, convertMessageContent };
return fn(raw, nextCtx);
}
@@ -0,0 +1,8 @@
/**
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*
* Converter for "file" message type.
*/
import type { ContentConverterFn } from './types';
export declare const convertFile: ContentConverterFn;
@@ -0,0 +1,24 @@
"use strict";
/**
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*
* Converter for "file" message type.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.convertFile = void 0;
const utils_1 = require("./utils.js");
const convertFile = (raw) => {
const parsed = (0, utils_1.safeParse)(raw);
const fileKey = parsed?.file_key;
if (!fileKey) {
return { content: '[file]', resources: [] };
}
const fileName = parsed?.file_name ?? '';
const nameAttr = fileName ? ` name="${fileName}"` : '';
return {
content: `<file key="${fileKey}"${nameAttr}/>`,
resources: [{ type: 'file', fileKey, fileName: fileName || undefined }],
};
};
exports.convertFile = convertFile;
@@ -0,0 +1,8 @@
/**
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*
* Converter for "folder" message type.
*/
import type { ContentConverterFn } from './types';
export declare const convertFolder: ContentConverterFn;
@@ -0,0 +1,24 @@
"use strict";
/**
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*
* Converter for "folder" message type.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.convertFolder = void 0;
const utils_1 = require("./utils.js");
const convertFolder = (raw) => {
const parsed = (0, utils_1.safeParse)(raw);
const fileKey = parsed?.file_key;
if (!fileKey) {
return { content: '[folder]', resources: [] };
}
const fileName = parsed?.file_name ?? '';
const nameAttr = fileName ? ` name="${fileName}"` : '';
return {
content: `<folder key="${fileKey}"${nameAttr}/>`,
resources: [],
};
};
exports.convertFolder = convertFolder;
@@ -0,0 +1,8 @@
/**
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*
* Converter for "hongbao" (red packet) message type.
*/
import type { ContentConverterFn } from './types';
export declare const convertHongbao: ContentConverterFn;
@@ -0,0 +1,20 @@
"use strict";
/**
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*
* Converter for "hongbao" (red packet) message type.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.convertHongbao = void 0;
const utils_1 = require("./utils.js");
const convertHongbao = (raw) => {
const parsed = (0, utils_1.safeParse)(raw);
const text = parsed?.text;
const textAttr = text ? ` text="${text}"` : '';
return {
content: `<hongbao${textAttr}/>`,
resources: [],
};
};
exports.convertHongbao = convertHongbao;
@@ -0,0 +1,8 @@
/**
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*
* Converter for "image" message type.
*/
import type { ContentConverterFn } from './types';
export declare const convertImage: ContentConverterFn;
@@ -0,0 +1,22 @@
"use strict";
/**
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*
* Converter for "image" message type.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.convertImage = void 0;
const utils_1 = require("./utils.js");
const convertImage = (raw) => {
const parsed = (0, utils_1.safeParse)(raw);
const imageKey = parsed?.image_key;
if (!imageKey) {
return { content: '[image]', resources: [] };
}
return {
content: `![image](${imageKey})`,
resources: [{ type: 'image', fileKey: imageKey }],
};
};
exports.convertImage = convertImage;
@@ -0,0 +1,8 @@
/**
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*
* Content converter mapping for all Feishu message types.
*/
import type { ContentConverterFn } from './types';
export declare const converters: ReadonlyMap<string, ContentConverterFn>;
@@ -0,0 +1,53 @@
"use strict";
/**
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*
* Content converter mapping for all Feishu message types.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.converters = void 0;
const text_1 = require("./text.js");
const post_1 = require("./post.js");
const image_1 = require("./image.js");
const file_1 = require("./file.js");
const audio_1 = require("./audio.js");
const video_1 = require("./video.js");
const sticker_1 = require("./sticker.js");
const index_1 = require("./interactive/index.js");
const share_1 = require("./share.js");
const location_1 = require("./location.js");
const merge_forward_1 = require("./merge-forward.js");
const folder_1 = require("./folder.js");
const system_1 = require("./system.js");
const hongbao_1 = require("./hongbao.js");
const calendar_1 = require("./calendar.js");
const video_chat_1 = require("./video-chat.js");
const todo_1 = require("./todo.js");
const vote_1 = require("./vote.js");
const unknown_1 = require("./unknown.js");
exports.converters = new Map([
['text', text_1.convertText],
['post', post_1.convertPost],
['image', image_1.convertImage],
['file', file_1.convertFile],
['audio', audio_1.convertAudio],
['video', video_1.convertVideo],
['media', video_1.convertVideo],
['sticker', sticker_1.convertSticker],
['interactive', index_1.convertInteractive],
['share_chat', share_1.convertShareChat],
['share_user', share_1.convertShareUser],
['location', location_1.convertLocation],
['merge_forward', merge_forward_1.convertMergeForward],
['folder', folder_1.convertFolder],
['system', system_1.convertSystem],
['hongbao', hongbao_1.convertHongbao],
['share_calendar_event', calendar_1.convertShareCalendarEvent],
['calendar', calendar_1.convertCalendar],
['general_calendar', calendar_1.convertGeneralCalendar],
['video_chat', video_chat_1.convertVideoChat],
['todo', todo_1.convertTodo],
['vote', vote_1.convertVote],
['unknown', unknown_1.convertUnknown],
]);
@@ -0,0 +1,76 @@
/**
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import type { ConvertCardResult, Obj, RawCardContent } from './types';
export declare const MODE: {
readonly Concise: 0;
readonly Detailed: 1;
};
type Mode = (typeof MODE)[keyof typeof MODE];
export declare class CardConverter {
private mode;
private attachment;
constructor(mode: Mode);
convert(input: RawCardContent): ConvertCardResult;
private extractBody;
private extractHeaderTitle;
private convertBody;
convertElements(elements: unknown[], depth: number): string;
convertElement(elem: Obj, depth: number): string;
extractProperty(elem: Obj): Obj;
extractTextContent(textElem: unknown): string;
private extractTextFromProperty;
convertPlainText(prop: Obj): string;
convertMarkdown(prop: Obj): string;
convertMarkdownV1(elem: Obj, prop: Obj): string;
convertMarkdownElements(elements: unknown[]): string;
convertDiv(prop: Obj, _id: string): string;
convertNote(prop: Obj): string;
convertLink(prop: Obj): string;
convertEmoji(prop: Obj): string;
convertLocalDatetime(prop: Obj): string;
convertList(prop: Obj): string;
convertBlockquote(prop: Obj): string;
convertCodeBlock(prop: Obj): string;
convertCodeSpan(prop: Obj): string;
convertHeading(prop: Obj): string;
convertFallbackText(prop: Obj): string;
convertTextTag(prop: Obj): string;
convertNumberTag(prop: Obj): string;
convertUnknown(prop: Obj | undefined, tag: string): string;
convertColumnSet(prop: Obj, depth: number): string;
convertColumn(prop: Obj, depth: number): string;
convertForm(prop: Obj, _id: string): string;
convertCollapsiblePanel(prop: Obj, _id: string): string;
convertInteractiveContainer(prop: Obj, _id: string): string;
convertRepeat(prop: Obj): string;
convertButton(prop: Obj, _id: string): string;
convertActions(prop: Obj): string;
convertSelect(prop: Obj, _id: string, isMulti: boolean): string;
convertSelectImg(prop: Obj, _id: string): string;
convertInput(prop: Obj, _id: string): string;
convertDatePicker(prop: Obj, _id: string, pickerType: string): string;
convertChecker(prop: Obj, _id: string): string;
convertOverflow(prop: Obj): string;
convertPerson(prop: Obj, _id: string): string;
convertPersonV1(prop: Obj, _id: string): string;
convertPersonList(prop: Obj): string;
convertAvatar(prop: Obj, _id: string): string;
convertAt(prop: Obj): string;
convertImage(prop: Obj, _id: string): string;
convertImgCombination(prop: Obj): string;
convertChart(prop: Obj, _id: string): string;
private extractChartSummary;
private extractLineBarSummary;
private extractPieSummary;
private extractGenericSummary;
convertAudio(prop: Obj, _id: string): string;
convertVideo(prop: Obj, _id: string): string;
convertTable(prop: Obj): string;
private extractTableCellValue;
private extractTextStyle;
private applyTextStyle;
private getImageToken;
}
export {};
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,9 @@
/**
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*
* Utility functions for card content conversion.
*/
export declare function escapeAttr(s: string): string;
export declare function formatMillisecondsToISO8601(milliseconds: string): string;
export declare function normalizeTimeFormat(input: string): string;
@@ -0,0 +1,47 @@
"use strict";
/**
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*
* Utility functions for card content conversion.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.escapeAttr = escapeAttr;
exports.formatMillisecondsToISO8601 = formatMillisecondsToISO8601;
exports.normalizeTimeFormat = normalizeTimeFormat;
function escapeAttr(s) {
return s.replace(/"/g, '\\"').replace(/\n/g, '\\n');
}
function formatMillisecondsToISO8601(milliseconds) {
const ms = parseInt(milliseconds, 10);
if (isNaN(ms))
return '';
return new Date(ms).toISOString();
}
function normalizeTimeFormat(input) {
if (!input)
return '';
const num = parseInt(input, 10);
if (!isNaN(num) && String(num) === input.trim()) {
if (input.length >= 13) {
return new Date(num).toISOString();
}
else if (input.length >= 10) {
return new Date(num * 1000).toISOString();
}
}
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(input)) {
return input;
}
const dtMatch = /^(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2}(?::\d{2})?)$/.exec(input);
if (dtMatch) {
const d = new Date(`${dtMatch[1]}T${dtMatch[2]}`);
if (!isNaN(d.getTime()))
return d.toISOString();
}
if (/^\d{4}-\d{2}-\d{2}$/.test(input))
return input;
if (/^\d{2}:\d{2}(:\d{2})?$/.test(input))
return input;
return input;
}
@@ -0,0 +1,8 @@
/**
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*
* Entry point for the interactive (card) message converter.
*/
import type { ContentConverterFn } from '../types';
export declare const convertInteractive: ContentConverterFn;
@@ -0,0 +1,25 @@
"use strict";
/**
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*
* Entry point for the interactive (card) message converter.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.convertInteractive = void 0;
const utils_1 = require("../utils.js");
const card_converter_1 = require("./card-converter.js");
const legacy_1 = require("./legacy.js");
const convertInteractive = (raw) => {
const parsed = (0, utils_1.safeParse)(raw);
if (!parsed) {
return { content: '[interactive card]', resources: [] };
}
if (typeof parsed.json_card === 'string') {
const converter = new card_converter_1.CardConverter(card_converter_1.MODE.Concise);
const result = converter.convert(parsed);
return { content: result.content || '[interactive card]', resources: [] };
}
return (0, legacy_1.convertLegacyCard)(parsed);
};
exports.convertInteractive = convertInteractive;
@@ -0,0 +1,11 @@
/**
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*
* Legacy card converter for non-raw_card_content format.
*/
import type { Obj } from './types';
export declare function convertLegacyCard(parsed: Obj): {
content: string;
resources: never[];
};
@@ -0,0 +1,60 @@
"use strict";
/**
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*
* Legacy card converter for non-raw_card_content format.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.convertLegacyCard = convertLegacyCard;
function convertLegacyCard(parsed) {
const texts = [];
const header = parsed.header;
if (header) {
const title = header.title;
if (title && typeof title.content === 'string') {
texts.push(`**${title.content}**`);
}
}
const body = parsed.body;
const elements = (parsed.elements ?? body?.elements ?? []);
extractTexts(elements, texts);
const content = texts.length > 0 ? texts.join('\n') : '[interactive card]';
return { content, resources: [] };
}
function extractTexts(elements, out) {
if (!Array.isArray(elements))
return;
for (const el of elements) {
if (typeof el !== 'object' || el == null)
continue;
const elem = el;
if (elem.tag === 'markdown' && typeof elem.content === 'string') {
out.push(elem.content);
continue;
}
if (elem.tag === 'div' || elem.tag === 'plain_text' || elem.tag === 'lark_md') {
const text = elem.text;
if (text?.content && typeof text.content === 'string') {
out.push(text.content);
}
if (typeof elem.content === 'string') {
out.push(elem.content);
}
}
if (elem.tag === 'column_set') {
const columns = elem.columns;
if (columns) {
for (const col of columns) {
const colObj = col;
if (colObj.elements) {
extractTexts(colObj.elements, out);
}
}
}
}
if (elem.elements) {
extractTexts(elem.elements, out);
}
}
}
@@ -0,0 +1,23 @@
/**
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*
* Types and constants for the interactive (card) converter.
*/
export type Obj = Record<string, unknown>;
export interface RawCardContent {
json_card: string;
json_attachment?: string;
card_schema?: number;
}
export interface ConvertCardResult {
content: string;
schema: number;
}
export interface TextStyle {
bold: boolean;
italic: boolean;
strikethrough: boolean;
}
export declare const EMOJI_MAP: Record<string, string>;
export declare const CHART_TYPE_NAMES: Record<string, string>;
@@ -0,0 +1,27 @@
"use strict";
/**
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*
* Types and constants for the interactive (card) converter.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.CHART_TYPE_NAMES = exports.EMOJI_MAP = void 0;
exports.EMOJI_MAP = {
OK: '👌',
THUMBSUP: '👍',
SMILE: '😊',
HEART: '❤️',
CLAP: '👏',
FIRE: '🔥',
PARTY: '🎉',
THINK: '🤔',
};
exports.CHART_TYPE_NAMES = {
bar: '柱状图',
line: '折线图',
pie: '饼图',
area: '面积图',
radar: '雷达图',
scatter: '散点图',
};
@@ -0,0 +1,8 @@
/**
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*
* Converter for "location" message type.
*/
import type { ContentConverterFn } from './types';
export declare const convertLocation: ContentConverterFn;
@@ -0,0 +1,23 @@
"use strict";
/**
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*
* Converter for "location" message type.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.convertLocation = void 0;
const utils_1 = require("./utils.js");
const convertLocation = (raw) => {
const parsed = (0, utils_1.safeParse)(raw);
const name = parsed?.name ?? '';
const lat = parsed?.latitude ?? '';
const lng = parsed?.longitude ?? '';
const nameAttr = name ? ` name="${name}"` : '';
const coordsAttr = lat && lng ? ` coords="lat:${lat},lng:${lng}"` : '';
return {
content: `<location${nameAttr}${coordsAttr}/>`,
resources: [],
};
};
exports.convertLocation = convertLocation;
@@ -0,0 +1,32 @@
/**
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*
* Converter for "merge_forward" message type.
*
* Unlike other converters this is async it fetches sub-messages via
* the Feishu IM API and recursively expands nested merge_forward messages.
*
* The API returns ALL nested sub-messages in a single flat `items`
* array with `upper_message_id` pointing to the parent container.
* We build a tree from this flat list and recursively format it
* only one API call is needed regardless of nesting depth.
*
* This module is a pure "data → format" converter: all API capabilities
* (`fetchSubMessages`, `batchResolveNames`, `resolveUserName`) are
* injected via callbacks in `ConvertContext`. Callers are responsible
* for creating the appropriate callbacks (UAT / TAT / event push).
*/
import type { ContentConverterFn } from './types';
/**
* Recursively expand a merge_forward message.
*
* Output format aligns with the Go reference implementation:
* ```
* <forwarded_messages>
* [RFC3339] sender_id:
* message content
* </forwarded_messages>
* ```
*/
export declare const convertMergeForward: ContentConverterFn;
@@ -0,0 +1,235 @@
"use strict";
/**
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*
* Converter for "merge_forward" message type.
*
* Unlike other converters this is async it fetches sub-messages via
* the Feishu IM API and recursively expands nested merge_forward messages.
*
* The API returns ALL nested sub-messages in a single flat `items`
* array with `upper_message_id` pointing to the parent container.
* We build a tree from this flat list and recursively format it
* only one API call is needed regardless of nesting depth.
*
* This module is a pure "data → format" converter: all API capabilities
* (`fetchSubMessages`, `batchResolveNames`, `resolveUserName`) are
* injected via callbacks in `ConvertContext`. Callers are responsible
* for creating the appropriate callbacks (UAT / TAT / event push).
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.convertMergeForward = void 0;
const lark_logger_1 = require("../../core/lark-logger.js");
const content_converter_helpers_1 = require("./content-converter-helpers.js");
const log = (0, lark_logger_1.larkLogger)('converters/merge-forward');
/**
* Recursively expand a merge_forward message.
*
* Output format aligns with the Go reference implementation:
* ```
* <forwarded_messages>
* [RFC3339] sender_id:
* message content
* </forwarded_messages>
* ```
*/
const convertMergeForward = async (_raw, ctx) => {
const { accountId, messageId, resolveUserName, batchResolveNames, fetchSubMessages, convertMessageContent } = ctx;
if (!fetchSubMessages) {
return { content: '<forwarded_messages/>', resources: [] };
}
const content = await expand(accountId, messageId, resolveUserName, batchResolveNames, fetchSubMessages, convertMessageContent);
return { content, resources: [] };
};
exports.convertMergeForward = convertMergeForward;
// ---------------------------------------------------------------------------
// Single-API-call expansion with tree building
// ---------------------------------------------------------------------------
async function expand(accountId, messageId, resolveUserName, batchResolveNames, fetchSubMessages, convertContent) {
// --- Phase 1: Fetch (single API call via callback) ---
let items;
try {
items = await fetchSubMessages(messageId);
}
catch (error) {
log.error('fetch sub-messages failed', {
messageId,
error: error instanceof Error ? error.message : String(error),
});
return '<forwarded_messages/>';
}
if (items.length === 0) {
return '<forwarded_messages/>';
}
// --- Phase 2: Build children map ---
const childrenMap = buildChildrenMap(items, messageId);
// --- Phase 2.5: Batch resolve sender names (via callback) ---
const senderIds = collectSenderIds(items, messageId);
if (senderIds.length > 0 && batchResolveNames) {
try {
await batchResolveNames(senderIds);
}
catch (err) {
log.debug('batchResolveNames failed (best-effort)', {
error: err instanceof Error ? err.message : String(err),
});
}
}
// --- Phase 3: Format tree recursively ---
return formatSubTree(messageId, childrenMap, accountId, resolveUserName, convertContent);
}
// ---------------------------------------------------------------------------
// Tree building
// ---------------------------------------------------------------------------
/**
* Build a map from parent message ID ordered child items.
*
* The API returns a flat `items` array where each item may carry an
* `upper_message_id` pointing to its parent container. Items without
* `upper_message_id` are direct children of the root container.
*
* The root container message itself (matching `rootMessageId`) is skipped.
*/
function buildChildrenMap(items, rootMessageId) {
const map = new Map();
for (const item of items) {
// Skip the root container message itself
if (item.message_id === rootMessageId && !item.upper_message_id) {
continue;
}
const parentId = item.upper_message_id ?? rootMessageId;
let children = map.get(parentId);
if (!children) {
children = [];
map.set(parentId, children);
}
children.push(item);
}
// Sort each group by create_time ascending
for (const children of map.values()) {
children.sort((a, b) => {
const ta = parseInt(String(a.create_time ?? '0'), 10);
const tb = parseInt(String(b.create_time ?? '0'), 10);
return ta - tb;
});
}
return map;
}
// ---------------------------------------------------------------------------
// Sender ID collection
// ---------------------------------------------------------------------------
/**
* Collect all unique sender IDs from non-root items for batch name resolution.
*/
function collectSenderIds(items, rootMessageId) {
const ids = new Set();
for (const item of items) {
// Skip the root container
if (item.message_id === rootMessageId && !item.upper_message_id) {
continue;
}
if (item.sender?.sender_type === 'user') {
const senderId = item.sender.id;
if (senderId) {
ids.add(senderId);
}
}
}
return [...ids];
}
// ---------------------------------------------------------------------------
// Recursive tree formatting
// ---------------------------------------------------------------------------
/**
* Recursively format a sub-tree of messages rooted at `parentId`.
*
* For `merge_forward` children this recurses into `formatSubTree`
* directly (no additional API calls). For other message types it
* delegates to `convertMessageContent`.
*/
async function formatSubTree(parentId, childrenMap, accountId, resolveUserName, convertContent) {
const children = childrenMap.get(parentId);
if (!children || children.length === 0) {
return '<forwarded_messages/>';
}
const parts = [];
for (const item of children) {
try {
const msgType = item.msg_type ?? 'text';
const senderId = item.sender?.id ?? 'unknown';
const createTime = item.create_time ? parseInt(String(item.create_time), 10) : undefined;
const timestamp = createTime ? formatTimestamp(createTime) : 'unknown';
const rawContent = item.body?.content ?? '{}';
let content;
if (msgType === 'merge_forward') {
// Recurse into nested merge_forward via the tree — no API call
const nestedId = item.message_id;
if (nestedId) {
content = await formatSubTree(nestedId, childrenMap, accountId, resolveUserName, convertContent);
}
else {
content = '<forwarded_messages/>';
}
}
else {
// Delegate to the unified converter system.
// Do NOT pass cfg/account here — sub-converters for non-merge_forward
// types don't need it, and passing it would cause nested
// merge_forward to re-enter expand() via convertMessageContent.
const subCtx = {
...(0, content_converter_helpers_1.buildConvertContextFromItem)(item, parentId, accountId),
accountId,
resolveUserName,
convertMessageContent: convertContent,
};
if (!convertContent) {
content = rawContent;
}
else {
content = (await convertContent(rawContent, msgType, subCtx)).content;
}
}
const displayName = resolveUserName?.(senderId) ?? senderId;
const indented = indentLines(content, ' ');
parts.push(`[${timestamp}] ${displayName}:\n${indented}`);
}
catch (err) {
log.warn('failed to convert sub-message', {
messageId: item.message_id,
msgType: item.msg_type ?? 'unknown',
error: err instanceof Error ? err.message : String(err),
});
}
}
if (parts.length === 0) {
return '<forwarded_messages/>';
}
return `<forwarded_messages>\n${parts.join('\n')}\n</forwarded_messages>`;
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/**
* Convert a millisecond timestamp to RFC 3339 format with +08:00 offset
* (Beijing time).
*/
function formatTimestamp(ms) {
const date = new Date(ms);
const utcMs = date.getTime() + date.getTimezoneOffset() * 60_000;
const bjDate = new Date(utcMs + 8 * 3600_000);
const y = bjDate.getFullYear();
const mo = String(bjDate.getMonth() + 1).padStart(2, '0');
const d = String(bjDate.getDate()).padStart(2, '0');
const h = String(bjDate.getHours()).padStart(2, '0');
const mi = String(bjDate.getMinutes()).padStart(2, '0');
const s = String(bjDate.getSeconds()).padStart(2, '0');
return `${y}-${mo}-${d}T${h}:${mi}:${s}+08:00`;
}
/** Add a prefix indent to every line of text. */
function indentLines(text, indent) {
return text
.split('\n')
.map((line) => `${indent}${line}`)
.join('\n');
}
@@ -0,0 +1,11 @@
/**
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*
* Converter for "post" (rich text) message type.
*
* Preserves structure as Markdown: links as `[text](href)`,
* images as `![image](key)`, code blocks, and mention resolution.
*/
import type { ContentConverterFn } from './types';
export declare const convertPost: ContentConverterFn;
@@ -0,0 +1,139 @@
"use strict";
/**
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*
* Converter for "post" (rich text) message type.
*
* Preserves structure as Markdown: links as `[text](href)`,
* images as `![image](key)`, code blocks, and mention resolution.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.convertPost = void 0;
const content_converter_helpers_1 = require("./content-converter-helpers.js");
const utils_1 = require("./utils.js");
/** Preferred locale order for multi-language post unwrapping. */
const LOCALE_PRIORITY = ['zh_cn', 'en_us', 'ja_jp'];
/**
* Unwrap a parsed post object that may be locale-wrapped.
*
* Feishu post messages come in two shapes:
* - Flat: `{ title, content }`
* - Locale: `{ zh_cn: { title, content }, en_us: { title, content } }`
*/
function unwrapLocale(parsed) {
if ('title' in parsed || 'content' in parsed) {
return parsed;
}
for (const locale of LOCALE_PRIORITY) {
const localeData = parsed[locale];
if (localeData != null && typeof localeData === 'object') {
return localeData;
}
}
const firstKey = Object.keys(parsed)[0];
if (firstKey) {
const firstValue = parsed[firstKey];
if (firstValue != null && typeof firstValue === 'object') {
return firstValue;
}
}
return undefined;
}
const convertPost = (raw, ctx) => {
const rawParsed = (0, utils_1.safeParse)(raw);
if (rawParsed == null || typeof rawParsed !== 'object') {
return { content: '[rich text message]', resources: [] };
}
const parsed = unwrapLocale(rawParsed);
if (!parsed) {
return { content: '[rich text message]', resources: [] };
}
const resources = [];
const lines = [];
// Title
if (parsed.title) {
lines.push(`**${parsed.title}**`, '');
}
const contentBlocks = parsed.content ?? [];
for (const paragraph of contentBlocks) {
if (!Array.isArray(paragraph))
continue;
let line = '';
for (const el of paragraph) {
line += renderElement(el, ctx, resources);
}
lines.push(line);
}
let content = lines.join('\n').trim() || '[rich text message]';
content = (0, content_converter_helpers_1.resolveMentions)(content, ctx);
return { content, resources };
};
exports.convertPost = convertPost;
function renderElement(el, ctx, resources) {
switch (el.tag) {
case 'text': {
let text = el.text ?? '';
text = applyStyle(text, el.style);
return text;
}
case 'a': {
const text = el.text ?? el.href ?? '';
return el.href ? `[${text}](${el.href})` : text;
}
case 'at': {
// At-mention in post — use placeholder key if available via context,
// otherwise fall back to @user_name.
const userId = el.user_id ?? '';
if (userId === 'all')
return '@all';
const name = el.user_name ?? userId;
// O(1) lookup via reverse map
const info = ctx.mentionsByOpenId.get(userId);
if (info) {
// Let resolveMentions handle it — return the placeholder key
return info.key;
}
return `@${name}`;
}
case 'img': {
if (el.image_key) {
resources.push({ type: 'image', fileKey: el.image_key });
return `![image](${el.image_key})`;
}
return '';
}
case 'media': {
if (el.file_key) {
resources.push({ type: 'file', fileKey: el.file_key });
return `<file key="${el.file_key}"/>`;
}
return '';
}
case 'code_block': {
const lang = el.language ?? '';
const code = el.text ?? '';
return `\n\`\`\`${lang}\n${code}\n\`\`\`\n`;
}
case 'hr':
return '\n---\n';
default:
return el.text ?? '';
}
}
function applyStyle(text, style) {
if (!style || style.length === 0)
return text;
let result = text;
if (style.includes('bold'))
result = `**${result}**`;
if (style.includes('italic'))
result = `*${result}*`;
if (style.includes('underline'))
result = `<u>${result}</u>`;
if (style.includes('lineThrough'))
result = `~~${result}~~`;
if (style.includes('codeInline'))
result = `\`${result}\``;
return result;
}
@@ -0,0 +1,9 @@
/**
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*
* Converter for "share_chat" and "share_user" message types.
*/
import type { ContentConverterFn } from './types';
export declare const convertShareChat: ContentConverterFn;
export declare const convertShareUser: ContentConverterFn;
@@ -0,0 +1,28 @@
"use strict";
/**
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*
* Converter for "share_chat" and "share_user" message types.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.convertShareUser = exports.convertShareChat = void 0;
const utils_1 = require("./utils.js");
const convertShareChat = (raw) => {
const parsed = (0, utils_1.safeParse)(raw);
const chatId = parsed?.chat_id ?? '';
return {
content: `<group_card id="${chatId}"/>`,
resources: [],
};
};
exports.convertShareChat = convertShareChat;
const convertShareUser = (raw) => {
const parsed = (0, utils_1.safeParse)(raw);
const userId = parsed?.user_id ?? '';
return {
content: `<contact_card id="${userId}"/>`,
resources: [],
};
};
exports.convertShareUser = convertShareUser;
@@ -0,0 +1,8 @@
/**
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*
* Converter for "sticker" message type.
*/
import type { ContentConverterFn } from './types';
export declare const convertSticker: ContentConverterFn;
@@ -0,0 +1,22 @@
"use strict";
/**
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*
* Converter for "sticker" message type.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.convertSticker = void 0;
const utils_1 = require("./utils.js");
const convertSticker = (raw) => {
const parsed = (0, utils_1.safeParse)(raw);
const fileKey = parsed?.file_key;
if (!fileKey) {
return { content: '[sticker]', resources: [] };
}
return {
content: `<sticker key="${fileKey}"/>`,
resources: [{ type: 'sticker', fileKey }],
};
};
exports.convertSticker = convertSticker;
@@ -0,0 +1,12 @@
/**
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*
* Converter for "system" message type.
*
* System messages use a template string with placeholders like
* `{from_user}`, `{to_chatters}`, `{divider_text}` that are replaced
* with actual values from the message body.
*/
import type { ContentConverterFn } from './types';
export declare const convertSystem: ContentConverterFn;
@@ -0,0 +1,36 @@
"use strict";
/**
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*
* Converter for "system" message type.
*
* System messages use a template string with placeholders like
* `{from_user}`, `{to_chatters}`, `{divider_text}` that are replaced
* with actual values from the message body.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.convertSystem = void 0;
const utils_1 = require("./utils.js");
const convertSystem = (raw) => {
const parsed = (0, utils_1.safeParse)(raw);
if (!parsed?.template) {
return { content: '[system message]', resources: [] };
}
let content = parsed.template;
const replacements = {
'{from_user}': parsed.from_user?.length ? parsed.from_user.filter(Boolean).join(', ') : undefined,
'{to_chatters}': parsed.to_chatters?.length ? parsed.to_chatters.filter(Boolean).join(', ') : undefined,
'{divider_text}': parsed.divider_text?.text,
};
for (const [placeholder, value] of Object.entries(replacements)) {
if (value != null) {
content = content.replaceAll(placeholder, value);
}
else {
content = content.replaceAll(placeholder, '');
}
}
return { content: content.trim(), resources: [] };
};
exports.convertSystem = convertSystem;
@@ -0,0 +1,8 @@
/**
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*
* Converter for "text" message type.
*/
import type { ContentConverterFn } from './types';
export declare const convertText: ContentConverterFn;
@@ -0,0 +1,18 @@
"use strict";
/**
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*
* Converter for "text" message type.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.convertText = void 0;
const content_converter_helpers_1 = require("./content-converter-helpers.js");
const utils_1 = require("./utils.js");
const convertText = (raw, ctx) => {
const parsed = (0, utils_1.safeParse)(raw);
const text = parsed?.text ?? raw;
const content = (0, content_converter_helpers_1.resolveMentions)(text, ctx);
return { content, resources: [] };
};
exports.convertText = convertText;
@@ -0,0 +1,8 @@
/**
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*
* Converter for "todo" message type.
*/
import type { ContentConverterFn } from './types';
export declare const convertTodo: ContentConverterFn;
@@ -0,0 +1,45 @@
"use strict";
/**
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*
* Converter for "todo" message type.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.convertTodo = void 0;
const utils_1 = require("./utils.js");
/** Extract plain text from post-style content blocks. */
function extractPlainText(content) {
const lines = [];
for (const paragraph of content) {
if (!Array.isArray(paragraph))
continue;
let line = '';
for (const el of paragraph) {
if (el.text)
line += el.text;
}
lines.push(line);
}
return lines.join('\n').trim();
}
const convertTodo = (raw) => {
const parsed = (0, utils_1.safeParse)(raw);
const parts = [];
// Build title from summary.title and summary.content
const title = parsed?.summary?.title ?? '';
const body = parsed?.summary?.content ? extractPlainText(parsed.summary.content) : '';
const fullTitle = [title, body].filter(Boolean).join('\n');
if (fullTitle) {
parts.push(fullTitle);
}
if (parsed?.due_time) {
parts.push(`Due: ${(0, utils_1.millisToDatetime)(parsed.due_time)}`);
}
const inner = parts.join('\n') || '[todo]';
return {
content: `<todo>\n${inner}\n</todo>`,
resources: [],
};
};
exports.convertTodo = convertTodo;
@@ -0,0 +1,114 @@
/**
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*
* Shared types for the content converter system.
*/
import type { ClawdbotConfig } from 'openclaw/plugin-sdk';
import type { LarkAccount } from '../../core/types';
import type { MentionInfo, ResourceDescriptor } from '../types';
/**
* Shape of a message item returned by the Feishu IM API.
*
* Used by `buildConvertContextFromItem` (content-converter) and the
* `merge_forward` converter to process sub-messages and history items
* without resorting to `any`.
*/
export interface ApiMessageItem {
message_id?: string;
msg_type?: string;
create_time?: string;
upper_message_id?: string;
body?: {
content?: string;
};
sender?: {
id?: string;
sender_type?: string;
};
mentions?: Array<{
key: string;
id: unknown;
name?: string;
}>;
parent_id?: string;
thread_id?: string;
deleted?: boolean;
updated?: boolean;
}
/** Context passed to every converter function. */
export interface ConvertContext {
/** Map from placeholder key ("@_user_X") to structured mention info. */
mentions: Map<string, MentionInfo>;
/** Reverse map from openId to MentionInfo for O(1) lookup. */
mentionsByOpenId: Map<string, MentionInfo>;
messageId: string;
botOpenId?: string;
/** Plugin config — retained for non-converter downstream consumers. */
cfg?: ClawdbotConfig;
/**
* Pre-resolved account retained for non-converter downstream consumers.
* merge_forward no longer reads this; it uses injected callbacks instead.
*/
account?: LarkAccount;
/** Account identifier for multi-account setups. */
accountId?: string;
/** Synchronous lookup of cached user display name by openId. */
resolveUserName?: (openId: string) => string | undefined;
/**
* Async batch name resolution callback.
*
* Called by merge_forward to resolve sub-message sender names.
* The callback should populate whatever cache `resolveUserName` reads from.
* All callers must inject this; merge_forward has no internal fallback.
*/
batchResolveNames?: (openIds: string[]) => Promise<void>;
/**
* Async callback to fetch sub-messages of a merge_forward container.
*
* Returns the flat items array from the IM API response.
* All callers must inject this; merge_forward has no internal fallback
* and returns `<forwarded_messages/>` when not provided.
*/
fetchSubMessages?: (messageId: string) => Promise<ApiMessageItem[]>;
/**
* Recursive dispatcher used by converters such as merge_forward.
*
* Injected by `convertMessageContent()` so converter modules do not need
* to import the main dispatcher directly.
*/
convertMessageContent?: (raw: string, messageType: string, ctx: ConvertContext) => Promise<ConvertResult>;
/** 是否删除机器人 mention(事件推送场景=true,历史消息读取=false */
stripBotMentions?: boolean;
}
/** Result produced by a converter function. */
export interface ConvertResult {
/** AI-friendly formatted text. */
content: string;
/** Resource descriptors (images, files, audio, video, stickers). */
resources: ResourceDescriptor[];
}
/**
* Converter function for a single message type.
*
* May return a ConvertResult synchronously or a Promise for types that
* require async operations (e.g. merge_forward expansion via API).
*/
export type ContentConverterFn = (raw: string, ctx: ConvertContext) => ConvertResult | Promise<ConvertResult>;
/**
* Element within a Feishu "post" (rich text) content block.
*
* Shared by the `post` and `todo` converters.
*/
export interface PostElement {
tag: string;
text?: string;
href?: string;
image_key?: string;
file_key?: string;
user_id?: string;
user_name?: string;
style?: string[];
language?: string;
un_escape?: boolean;
}
@@ -0,0 +1,8 @@
"use strict";
/**
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*
* Shared types for the content converter system.
*/
Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,8 @@
/**
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*
* Fallback converter for unsupported message types.
*/
import type { ContentConverterFn } from './types';
export declare const convertUnknown: ContentConverterFn;
@@ -0,0 +1,20 @@
"use strict";
/**
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*
* Fallback converter for unsupported message types.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.convertUnknown = void 0;
const utils_1 = require("./utils.js");
const convertUnknown = (raw) => {
const parsed = (0, utils_1.safeParse)(raw);
if (parsed != null && typeof parsed === 'object' && 'text' in parsed) {
const text = parsed.text;
if (typeof text === 'string')
return { content: text, resources: [] };
}
return { content: '[unsupported message]', resources: [] };
};
exports.convertUnknown = convertUnknown;
@@ -0,0 +1,22 @@
/**
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*
* Shared utilities for content converters.
*/
/** Escape a string for safe use inside a RegExp. */
export declare function escapeRegExp(str: string): string;
/**
* Safely parse a JSON string, returning undefined on failure.
*/
export declare function safeParse(raw: string): unknown | undefined;
/**
* Format a duration in milliseconds to a human-readable string.
*
* Examples: 1500 "1.5s", 65000 "65s"
*/
export declare function formatDuration(ms: number): string;
/**
* Convert a millisecond timestamp to "YYYY-MM-DD HH:mm" in UTC+8 (Beijing time).
*/
export declare function millisToDatetime(ms: string | number): string;
@@ -0,0 +1,57 @@
"use strict";
/**
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*
* Shared utilities for content converters.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.escapeRegExp = escapeRegExp;
exports.safeParse = safeParse;
exports.formatDuration = formatDuration;
exports.millisToDatetime = millisToDatetime;
/** Escape a string for safe use inside a RegExp. */
function escapeRegExp(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
/**
* Safely parse a JSON string, returning undefined on failure.
*/
function safeParse(raw) {
try {
return JSON.parse(raw);
}
catch {
return undefined;
}
}
/**
* Format a duration in milliseconds to a human-readable string.
*
* Examples: 1500 "1.5s", 65000 "65s"
*/
function formatDuration(ms) {
const seconds = ms / 1000;
if (seconds < 1)
return `${ms}ms`;
if (Number.isInteger(seconds))
return `${seconds}s`;
return `${seconds.toFixed(1)}s`;
}
/**
* Convert a millisecond timestamp to "YYYY-MM-DD HH:mm" in UTC+8 (Beijing time).
*/
function millisToDatetime(ms) {
const num = Number(ms);
if (!Number.isFinite(num))
return String(ms);
// UTC+8 offset in milliseconds
const utc8Offset = 8 * 60 * 60 * 1000;
const d = new Date(num + utc8Offset);
const year = d.getUTCFullYear();
const month = String(d.getUTCMonth() + 1).padStart(2, '0');
const day = String(d.getUTCDate()).padStart(2, '0');
const hour = String(d.getUTCHours()).padStart(2, '0');
const minute = String(d.getUTCMinutes()).padStart(2, '0');
return `${year}-${month}-${day} ${hour}:${minute}`;
}
@@ -0,0 +1,8 @@
/**
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*
* Converter for "video_chat" message type.
*/
import type { ContentConverterFn } from './types';
export declare const convertVideoChat: ContentConverterFn;
@@ -0,0 +1,31 @@
"use strict";
/**
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*
* Converter for "video_chat" message type.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.convertVideoChat = void 0;
const utils_1 = require("./utils.js");
const convertVideoChat = (raw) => {
const parsed = (0, utils_1.safeParse)(raw);
const topic = parsed?.topic ?? '';
const meetingNo = parsed?.meet_number?.trim() ?? '';
const parts = [];
if (topic) {
parts.push(`Topic: ${topic}`);
}
if (parsed?.start_time) {
parts.push(`Start time: ${(0, utils_1.millisToDatetime)(parsed.start_time)}`);
}
if (meetingNo) {
parts.push(`Meeting number: ${meetingNo}`);
}
const inner = parts.join('\n') || '[video chat]';
return {
content: `<meeting>${inner}</meeting>`,
resources: [],
};
};
exports.convertVideoChat = convertVideoChat;
@@ -0,0 +1,8 @@
/**
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*
* Converter for "video" and "media" message types.
*/
import type { ContentConverterFn } from './types';
export declare const convertVideo: ContentConverterFn;
@@ -0,0 +1,35 @@
"use strict";
/**
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*
* Converter for "video" and "media" message types.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.convertVideo = void 0;
const utils_1 = require("./utils.js");
const convertVideo = (raw) => {
const parsed = (0, utils_1.safeParse)(raw);
const fileKey = parsed?.file_key;
if (!fileKey) {
return { content: '[video]', resources: [] };
}
const fileName = parsed?.file_name ?? '';
const duration = parsed?.duration;
const coverKey = parsed?.image_key;
const nameAttr = fileName ? ` name="${fileName}"` : '';
const durationAttr = duration != null ? ` duration="${(0, utils_1.formatDuration)(duration)}"` : '';
return {
content: `<video key="${fileKey}"${nameAttr}${durationAttr}/>`,
resources: [
{
type: 'video',
fileKey,
fileName: fileName || undefined,
duration: duration ?? undefined,
coverImageKey: coverKey ?? undefined,
},
],
};
};
exports.convertVideo = convertVideo;
@@ -0,0 +1,8 @@
/**
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*
* Converter for "vote" message type.
*/
import type { ContentConverterFn } from './types';
export declare const convertVote: ContentConverterFn;
@@ -0,0 +1,28 @@
"use strict";
/**
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*
* Converter for "vote" message type.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.convertVote = void 0;
const utils_1 = require("./utils.js");
const convertVote = (raw) => {
const parsed = (0, utils_1.safeParse)(raw);
const topic = parsed?.topic ?? '';
const options = parsed?.options ?? [];
const parts = [];
if (topic) {
parts.push(topic);
}
for (const opt of options) {
parts.push(`${opt}`);
}
const inner = parts.join('\n') || '[vote]';
return {
content: `<vote>\n${inner}\n</vote>`,
resources: [],
};
};
exports.convertVote = convertVote;