auto: sync OpenClaw config 2026-09-09 16:13
This commit is contained in:
@@ -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;
|
||||
+30
@@ -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: ``,
|
||||
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],
|
||||
]);
|
||||
+76
@@ -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 ``, 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 ``, 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 ``;
|
||||
}
|
||||
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;
|
||||
Reference in New Issue
Block a user