mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-09 05:20:46 +08:00
Merge main into agent/issue-88 and resolve workspace aliases
This commit is contained in:
commit
3240632bef
90 changed files with 6394 additions and 619 deletions
|
|
@ -11,8 +11,11 @@ import {
|
|||
hasInboundImages,
|
||||
imagePromptDiagnostic,
|
||||
imagePromptUserMessage,
|
||||
promptContentForMessage,
|
||||
} from '../shared/image-prompt.mjs';
|
||||
import {
|
||||
hasReplyReference,
|
||||
promptContentForInboundMessage,
|
||||
} from '../shared/semantic/reply-reference.mjs';
|
||||
import {
|
||||
hasInboundFiles,
|
||||
inboundFileUserMessage,
|
||||
|
|
@ -44,7 +47,12 @@ import {
|
|||
isPresetCommand,
|
||||
runPresetCommand,
|
||||
} from '../shared/preset-command.mjs';
|
||||
import { runWorkspaceCommand, resolveSessionListWorkspace, workspacePathSnapshot } from '../shared/workspace-command.mjs';
|
||||
import {
|
||||
parseSessionListArgument,
|
||||
resolveSessionListWorkspace,
|
||||
runWorkspaceCommand,
|
||||
workspacePathSnapshot,
|
||||
} from '../shared/workspace-command.mjs';
|
||||
import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
|
||||
import { captureContextEnhancement, enhanceContextContent } from '../shared/context-enhancement.mjs';
|
||||
import { deliverOutboundArtifacts } from '../shared/semantic/artifact-delivery.mjs';
|
||||
|
|
@ -156,8 +164,8 @@ const WORKSPACE_HELP_LINES = [
|
|||
'/workspace 工作区序号或绝对路径 切换工作区',
|
||||
'/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话',
|
||||
'/workspacelist 列出工作区绝对路径',
|
||||
'/ws、/wsl、/workspaces 工作区命令别名',
|
||||
'/sessionlist 或 /sessions [工作区序号或绝对路径] 列出会话 ID 和标题',
|
||||
'/sessionlist --limit N 仅列出当前工作区前 N 个会话',
|
||||
];
|
||||
|
||||
/** Safe user-facing text for bind/workspace failures (no raw messages). */
|
||||
|
|
@ -627,7 +635,9 @@ export class FeishuHarnessBridge {
|
|||
&& (this.#queues.has(key) || pending || this.#approvals.hasPending(key))
|
||||
? { handled: true, kind: 'busy', message: batchInputBusyMessage() }
|
||||
: this.#batchInputs.handle(key, batchText, {
|
||||
plainText: event.message.message_type === 'text' && Boolean(batchText),
|
||||
plainText: event.message.message_type === 'text'
|
||||
&& Boolean(batchText)
|
||||
&& !hasReplyReference(commandMessage),
|
||||
});
|
||||
if (result.handled) {
|
||||
if (result.kind === 'submit') {
|
||||
|
|
@ -1033,11 +1043,12 @@ export class FeishuHarnessBridge {
|
|||
const text = message.content;
|
||||
const hasImages = hasInboundImages(message);
|
||||
const hasFiles = hasInboundFiles(message);
|
||||
const hasReply = hasReplyReference(message);
|
||||
// 命令识别对 text 与纯文本 post 一视同仁:post 富文本若仅含单个
|
||||
// 文本段落(如复制粘贴的 /new),同样按命令处理;带图片/文件不认。
|
||||
// accept() 侧已用 nonEmptyString(content) 判定,两侧保持一致。
|
||||
const commandText = !hasImages && !hasFiles && text ? text.trim() : null;
|
||||
if (!text && !hasImages && !hasFiles) {
|
||||
if (!text && !hasImages && !hasFiles && !hasReply) {
|
||||
await this.#send(event.message.chat_id, t('目前支持文字、图片和文件消息。'), { replyTo: event.message.message_id });
|
||||
return;
|
||||
}
|
||||
|
|
@ -1080,8 +1091,18 @@ export class FeishuHarnessBridge {
|
|||
return;
|
||||
}
|
||||
if (SESSION_LIST_PREFIX.test(commandText)) {
|
||||
const selector = commandText.replace(/^\/(?:sessionlist|sessions)/i, '').trim() || null;
|
||||
await this.#showSessions({ chatId: event.message.chat_id, key, replyTo: event.message.message_id }, selector, 0);
|
||||
const argument = commandText.replace(/^\/(?:sessionlist|sessions)/i, '').trim();
|
||||
const request = parseSessionListArgument(argument);
|
||||
if (request.error) {
|
||||
await this.#send(event.message.chat_id, request.error, { replyTo: event.message.message_id });
|
||||
return;
|
||||
}
|
||||
await this.#showSessions(
|
||||
{ chatId: event.message.chat_id, key, replyTo: event.message.message_id },
|
||||
request.selector || null,
|
||||
0,
|
||||
{ limit: request.limit },
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (WORKSPACE_LIST_COMMAND.test(commandText)) {
|
||||
|
|
@ -1845,6 +1866,7 @@ export class FeishuHarnessBridge {
|
|||
messageId = null,
|
||||
sessionWorkspace = null,
|
||||
sessionPage = 0,
|
||||
sessionLimit = null,
|
||||
selections = [],
|
||||
}) {
|
||||
// Confirmations triggered by a card interaction stay anchored to the
|
||||
|
|
@ -1856,7 +1878,7 @@ export class FeishuHarnessBridge {
|
|||
{ chatId, key, replyTo: messageId },
|
||||
sessionWorkspace,
|
||||
page,
|
||||
{ updateMessageId: messageId },
|
||||
{ updateMessageId: messageId, limit: sessionLimit },
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -2035,7 +2057,7 @@ export class FeishuHarnessBridge {
|
|||
{ chatId, key, replyTo: messageId },
|
||||
sessionWorkspace,
|
||||
sessionPage,
|
||||
{ updateMessageId: messageId },
|
||||
{ updateMessageId: messageId, limit: sessionLimit },
|
||||
);
|
||||
}
|
||||
return;
|
||||
|
|
@ -2051,7 +2073,7 @@ export class FeishuHarnessBridge {
|
|||
{ chatId, key, replyTo: messageId },
|
||||
sessionWorkspace,
|
||||
sessionPage,
|
||||
{ updateMessageId: messageId },
|
||||
{ updateMessageId: messageId, limit: sessionLimit },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -2136,7 +2158,7 @@ export class FeishuHarnessBridge {
|
|||
{ chatId, key, replyTo = null },
|
||||
selector,
|
||||
page = 0,
|
||||
{ updateMessageId = null } = {},
|
||||
{ updateMessageId = null, limit = null } = {},
|
||||
) {
|
||||
try {
|
||||
const signal = this.#cardDataSignal();
|
||||
|
|
@ -2146,7 +2168,11 @@ export class FeishuHarnessBridge {
|
|||
return;
|
||||
}
|
||||
const listed = await this.#harness.listWorkspaceSessions(resolved.workspace, { signal });
|
||||
const sessions = this.#visibleSessions(Array.isArray(listed?.sessions) ? listed.sessions : []);
|
||||
const visibleSessions = this.#visibleSessions(Array.isArray(listed?.sessions) ? listed.sessions : []);
|
||||
const sessionLimit = Number.isSafeInteger(limit) && limit > 0 ? limit : null;
|
||||
const sessions = sessionLimit === null
|
||||
? visibleSessions
|
||||
: visibleSessions.slice(0, sessionLimit);
|
||||
const workspace = listed?.workspace ?? resolved.workspace;
|
||||
if (sessions.length === 0) {
|
||||
await this.#send(chatId, t('工作区:{workspace}\n该工作区暂无会话。', { workspace }), { replyTo });
|
||||
|
|
@ -2173,6 +2199,7 @@ export class FeishuHarnessBridge {
|
|||
// list response's workspace is display data and is not authoritative.
|
||||
sessionWorkspace: resolved.workspace,
|
||||
sessionPage: safePage,
|
||||
sessionLimit,
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
|
|
@ -2241,6 +2268,9 @@ export class FeishuHarnessBridge {
|
|||
sessionPage: Number.isSafeInteger(options.sessionPage) && options.sessionPage >= 0
|
||||
? options.sessionPage
|
||||
: 0,
|
||||
sessionLimit: Number.isSafeInteger(options.sessionLimit) && options.sessionLimit > 0
|
||||
? options.sessionLimit
|
||||
: null,
|
||||
});
|
||||
if (this.#cardKeys.size > 200) {
|
||||
const oldest = this.#cardKeys.keys().next().value;
|
||||
|
|
@ -3290,15 +3320,20 @@ export class FeishuHarnessBridge {
|
|||
askCompleted = true;
|
||||
onAskComplete?.();
|
||||
};
|
||||
let content = hasInboundImages(message)
|
||||
? await promptContentForMessage(message, { signal: this.#signal })
|
||||
let content = hasInboundImages(message) || hasReplyReference(message)
|
||||
? await promptContentForInboundMessage(message, { signal: this.#signal })
|
||||
: undefined;
|
||||
const snapshot = this.#acceptedMessageIds.get(messageId);
|
||||
let contextEnhanced = false;
|
||||
if (snapshot) {
|
||||
content = enhanceContextContent(content ?? text, snapshot, () => ({
|
||||
const originalContent = content ?? text;
|
||||
content = enhanceContextContent(originalContent, snapshot, () => ({
|
||||
channel: 'feishu',
|
||||
senderId: senderOpenId(event),
|
||||
chatId: event.message.chat_id,
|
||||
threadId: event.message.thread_id,
|
||||
}));
|
||||
contextEnhanced = content !== originalContent;
|
||||
}
|
||||
if (!this.#channel?.stream) {
|
||||
const { answer, artifacts = [] } = await askInWorkspaceSession({
|
||||
|
|
@ -3307,6 +3342,7 @@ export class FeishuHarnessBridge {
|
|||
key,
|
||||
text,
|
||||
content,
|
||||
contextEnhanced,
|
||||
createOptions: { signal: this.#signal },
|
||||
existsOptions: { signal: this.#signal },
|
||||
askOptions: this.#interactionAskOptions(event, key, message.files),
|
||||
|
|
@ -3353,8 +3389,18 @@ export class FeishuHarnessBridge {
|
|||
stream = await this.#channel.stream(chatId, {
|
||||
markdown: async (controller) => {
|
||||
promptStarted = true;
|
||||
const baseAskOptions = this.#interactionAskOptions(event, key, message.files);
|
||||
const askOptions = {
|
||||
...this.#interactionAskOptions(event, key, message.files),
|
||||
...baseAskOptions,
|
||||
// issue #86:独立交互消息(提问/审批)会落在占位卡下方,呈现前
|
||||
// 先换卡,让最终答案落在交互消息之后的新流式卡上。
|
||||
onInteraction: async (interaction) => {
|
||||
if ((interaction?.kind === 'question' || interaction?.kind === 'approval')
|
||||
&& typeof controller?.rotate === 'function') {
|
||||
await controller.rotate();
|
||||
}
|
||||
await baseAskOptions.onInteraction(interaction);
|
||||
},
|
||||
onUpdate: async (update) => {
|
||||
await controller.setContent(this.#progressText(update));
|
||||
this.#status.streamUpdates = (this.#status.streamUpdates ?? 0) + 1;
|
||||
|
|
@ -3366,6 +3412,7 @@ export class FeishuHarnessBridge {
|
|||
key,
|
||||
text,
|
||||
content,
|
||||
contextEnhanced,
|
||||
createOptions: { signal: this.#signal },
|
||||
existsOptions: { signal: this.#signal },
|
||||
askOptions,
|
||||
|
|
@ -3429,6 +3476,7 @@ export class FeishuHarnessBridge {
|
|||
key,
|
||||
text,
|
||||
content,
|
||||
contextEnhanced,
|
||||
createOptions: { signal: this.#signal },
|
||||
existsOptions: { signal: this.#signal },
|
||||
askOptions: this.#interactionAskOptions(event, key, message.files),
|
||||
|
|
|
|||
|
|
@ -488,9 +488,11 @@ export function menuHelpText() {
|
|||
'',
|
||||
'📋 会话 / 工作区',
|
||||
'/sessionlist 或 /sessions 列出工作区会话',
|
||||
'/sessionlist --limit N 仅列出当前工作区前 N 个会话',
|
||||
'/session ID 绑定已有会话',
|
||||
'/workspacelist 列出工作区',
|
||||
'/workspace 工作区序号或绝对路径 切换工作区',
|
||||
'/ws、/wsl、/workspaces 工作区命令别名',
|
||||
'/new 开启全新会话',
|
||||
'',
|
||||
'📊 状态 / 压缩',
|
||||
|
|
@ -554,6 +556,7 @@ const HELP_TEXT_COMMANDS = [
|
|||
'`/new` — 开启全新会话',
|
||||
'`/session ID` — 绑定已有会话',
|
||||
'`/sessionlist [工作区]` 或 `/sessions [工作区]` — 列出会话',
|
||||
'`/sessionlist --limit N` 或 `/sessions --limit N` — 仅列出当前工作区前 N 个会话',
|
||||
'`/workspace 工作区序号或绝对路径` — 切换工作区',
|
||||
'`/workspacelist` — 列出工作区',
|
||||
'`/status` — 查看连接状态',
|
||||
|
|
@ -593,6 +596,7 @@ export function helpCard(extraTextLines = []) {
|
|||
{ tag: 'hr' },
|
||||
{ tag: 'div', text: markdown([
|
||||
t(HELP_TEXT_COMMANDS),
|
||||
t('/ws、/wsl、/workspaces 工作区命令别名'),
|
||||
t('`/version` — 查看插件版本'),
|
||||
t('/history [数量] 查看最近历史消息(默认 3 条,最多 5 条)'),
|
||||
].join('\n') + extraText) },
|
||||
|
|
|
|||
|
|
@ -205,15 +205,45 @@ export class VerifiedFeishuChannel {
|
|||
}
|
||||
|
||||
const cards = [];
|
||||
let activeCard = null;
|
||||
let rotating = false;
|
||||
try {
|
||||
const firstCard = await this.#createStreamCard(chatId, options.replyTo);
|
||||
cards.push(firstCard);
|
||||
activeCard = await this.#createStreamCard(chatId, options.replyTo);
|
||||
cards.push(activeCard);
|
||||
let lastContent = this.#initialText;
|
||||
// issue #86:独立交互消息(提问/审批)落在占位卡下方后,最终答案不得
|
||||
// 回写旧卡。rotate() 把旧卡定格为「过程记录 + 指引行」并标记换卡态;
|
||||
// 下一次 setContent(过程更新或最终答案)才创建新卡——新卡必然创建于
|
||||
// 交互消息之后。旧卡纳入 cards,参与 recall 与 providerMessageIds。
|
||||
const ensureActiveCard = async () => {
|
||||
if (!rotating) return activeCard;
|
||||
activeCard = await this.#createStreamCard(chatId, options.replyTo);
|
||||
cards.push(activeCard);
|
||||
rotating = false;
|
||||
return activeCard;
|
||||
};
|
||||
const controller = {
|
||||
messageId: firstCard.messageId,
|
||||
get messageId() {
|
||||
return activeCard.messageId;
|
||||
},
|
||||
rotate: async () => {
|
||||
if (rotating) return;
|
||||
rotating = true;
|
||||
try {
|
||||
await this.#updateStreamCard(
|
||||
activeCard,
|
||||
`${streamPreview(lastContent)}\n\n${t('⤵️ 最终结果见下方')}`,
|
||||
);
|
||||
await this.#finishStreamCard(activeCard);
|
||||
} catch (error) {
|
||||
// 明确降级:定格失败不阻塞交互呈现,旧卡保留原内容。
|
||||
console.warn('[dsh-feishu] unable to finalize the superseded stream card:', error.message);
|
||||
}
|
||||
},
|
||||
setContent: async (content) => {
|
||||
const next = String(content ?? '') || '…';
|
||||
await this.#updateStreamCard(firstCard, streamPreview(next));
|
||||
const card = await ensureActiveCard();
|
||||
await this.#updateStreamCard(card, streamPreview(next));
|
||||
// Updates are replaceable snapshots, including progress/tool text.
|
||||
// Retain the full latest snapshot even when its preview is unchanged.
|
||||
lastContent = next;
|
||||
|
|
@ -224,14 +254,14 @@ export class VerifiedFeishuChannel {
|
|||
const chunks = splitStreamContent(lastContent);
|
||||
for (const [index, chunk] of chunks.entries()) {
|
||||
const card = index === 0
|
||||
? firstCard
|
||||
? await ensureActiveCard()
|
||||
: await this.#createStreamCard(chatId, options.replyTo);
|
||||
if (index > 0) cards.push(card);
|
||||
await this.#updateStreamCard(card, chunk);
|
||||
await this.#finishStreamCard(card);
|
||||
}
|
||||
return {
|
||||
messageId: firstCard.messageId,
|
||||
messageId: cards[0].messageId,
|
||||
providerMessageIds: cards.map((card) => card.messageId),
|
||||
};
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -2,8 +2,14 @@ import { ImagePromptError } from '../shared/image-prompt.mjs';
|
|||
import { t } from '../shared/i18n.mjs';
|
||||
|
||||
const FEISHU_MISSING_MESSAGE_SCOPE_CODE = 99991672;
|
||||
const FEISHU_CARD_MESSAGE_CONTENT_TYPE = 'raw_card_content';
|
||||
const FEISHU_ERROR_BODY_LIMIT = 64 * 1024;
|
||||
const FEISHU_ERROR_BODY_TIMEOUT_MS = 1_000;
|
||||
const FEISHU_CARD_TEXT_MAX_DEPTH = 12;
|
||||
const FEISHU_CARD_TEXT_MAX_NODES = 1_000;
|
||||
const FEISHU_CARD_UNAVAILABLE_TEXTS = new Set([
|
||||
'请升级至最新版本客户端,以查看内容',
|
||||
]);
|
||||
const FEISHU_IMAGE_PERMISSION_MESSAGE =
|
||||
'飞书机器人缺少图片读取权限 im:message:readonly(飞书显示为“获取单聊、群组消息”)。请私聊机器人执行 /repair 命令,或者在「IM机器人」设置页点击“补全权限”按钮并扫码。按飞书提示发布新版本、完成必要审批后,再重新发送图片。';
|
||||
|
||||
|
|
@ -56,6 +62,124 @@ function nonEmptyString(value) {
|
|||
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
||||
}
|
||||
|
||||
function objectRecord(value) {
|
||||
return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
||||
}
|
||||
|
||||
function jsonRecord(value) {
|
||||
if (objectRecord(value)) return value;
|
||||
if (typeof value !== 'string') return null;
|
||||
try {
|
||||
return objectRecord(JSON.parse(value));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function interactiveCardRoot(parsed) {
|
||||
const root = objectRecord(parsed);
|
||||
if (!root) return null;
|
||||
// raw_card_content wraps CardKit entities in json_card. Keep direct Card
|
||||
// 1.0/2.0 payloads readable as well for historical messages and fixtures.
|
||||
return jsonRecord(root.json_card) ?? jsonRecord(root.card) ?? root;
|
||||
}
|
||||
|
||||
function cardProperty(value) {
|
||||
const record = objectRecord(value);
|
||||
return objectRecord(record?.property) ?? record;
|
||||
}
|
||||
|
||||
function cardTextContent(property) {
|
||||
const i18n = objectRecord(property?.i18nContent);
|
||||
const content = nonEmptyString(i18n?.zh_cn)
|
||||
?? nonEmptyString(i18n?.en_us)
|
||||
?? nonEmptyString(i18n?.ja_jp)
|
||||
?? nonEmptyString(property?.content)
|
||||
?? nonEmptyString(property?.text);
|
||||
return content && !FEISHU_CARD_UNAVAILABLE_TEXTS.has(content) ? content : null;
|
||||
}
|
||||
|
||||
function cardElementText(
|
||||
element,
|
||||
{ depth = 0, inline = false, budget = { remaining: FEISHU_CARD_TEXT_MAX_NODES } } = {},
|
||||
) {
|
||||
if (depth > FEISHU_CARD_TEXT_MAX_DEPTH || budget.remaining <= 0) return '';
|
||||
budget.remaining -= 1;
|
||||
if (Array.isArray(element)) {
|
||||
return element
|
||||
.map((part) => cardElementText(part, {
|
||||
depth: depth + 1,
|
||||
inline: Array.isArray(part),
|
||||
budget,
|
||||
}))
|
||||
.filter(Boolean)
|
||||
.join(inline ? ' ' : '\n');
|
||||
}
|
||||
const value = objectRecord(element);
|
||||
if (!value) return '';
|
||||
const property = cardProperty(value);
|
||||
if (!property) return '';
|
||||
const tag = String(value.tag ?? '').toLowerCase();
|
||||
|
||||
if (
|
||||
tag === 'markdown'
|
||||
|| tag === 'markdown_v1'
|
||||
|| tag === 'lark_md'
|
||||
|| tag === 'plain_text'
|
||||
|| tag === 'text'
|
||||
) {
|
||||
const content = cardTextContent(property);
|
||||
if (content) return content;
|
||||
const nested = cardElementText(property.elements, {
|
||||
depth: depth + 1,
|
||||
inline: true,
|
||||
budget,
|
||||
});
|
||||
if (nested || tag !== 'markdown_v1') return nested;
|
||||
return cardElementText(value.fallback ?? property.fallback, {
|
||||
depth: depth + 1,
|
||||
inline: true,
|
||||
budget,
|
||||
});
|
||||
}
|
||||
if (tag === 'a' || tag === 'link' || tag === 'button') {
|
||||
if (typeof property.text === 'string') return nonEmptyString(property.text) ?? '';
|
||||
return cardElementText(property.text, { depth: depth + 1, inline: true, budget });
|
||||
}
|
||||
if (tag === 'div') {
|
||||
return [
|
||||
cardElementText(property.text, { depth: depth + 1, budget }),
|
||||
cardElementText(property.fields, { depth: depth + 1, budget }),
|
||||
].filter(Boolean).join('\n');
|
||||
}
|
||||
|
||||
// Traverse visible layout containers only. Deliberately ignore callback
|
||||
// values, form state, URLs, ids, template variables and other hidden data.
|
||||
return ['elements', 'columns', 'fields', 'children', 'actions']
|
||||
.map((key) => cardElementText(property[key], { depth: depth + 1, budget }))
|
||||
.filter(Boolean)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
function interactiveCardText(parsed) {
|
||||
const card = interactiveCardRoot(parsed);
|
||||
if (!card) return '';
|
||||
const budget = { remaining: FEISHU_CARD_TEXT_MAX_NODES };
|
||||
const header = cardProperty(card.header);
|
||||
const title = [
|
||||
cardElementText(header?.title, { inline: true, budget }),
|
||||
cardElementText(header?.subtitle, { inline: true, budget }),
|
||||
].filter(Boolean).join('\n') || nonEmptyString(card.title) || '';
|
||||
const body = cardProperty(card.body);
|
||||
const elements = Array.isArray(body?.elements)
|
||||
? body.elements
|
||||
: Array.isArray(card.elements)
|
||||
? card.elements
|
||||
: null;
|
||||
const content = cardElementText(elements, { budget });
|
||||
return [title, content].filter(Boolean).join('\n');
|
||||
}
|
||||
|
||||
function postContent(event, parsed = parsedMessageContent(event)) {
|
||||
if (event?.message?.message_type !== 'post') return null;
|
||||
if (!parsed) return null;
|
||||
|
|
@ -283,6 +407,109 @@ function feishuFileSource(event, client, file) {
|
|||
};
|
||||
}
|
||||
|
||||
function feishuReplyTargetId(event) {
|
||||
const parentId = nonEmptyString(event?.message?.parent_id);
|
||||
if (parentId) return parentId;
|
||||
const rootId = nonEmptyString(event?.message?.root_id);
|
||||
const messageId = nonEmptyString(event?.message?.message_id);
|
||||
return rootId && rootId !== messageId ? rootId : null;
|
||||
}
|
||||
|
||||
function feishuReplyAttachments(messageType, parsed, post) {
|
||||
if (messageType === 'post') {
|
||||
return (post?.imageKeys ?? []).map(() => ({ kind: 'image' }));
|
||||
}
|
||||
if (messageType === 'image') return [{ kind: 'image' }];
|
||||
if (messageType === 'file') {
|
||||
return [{
|
||||
kind: 'file',
|
||||
...(nonEmptyString(parsed?.file_name) ? { name: nonEmptyString(parsed.file_name) } : {}),
|
||||
}];
|
||||
}
|
||||
if (messageType === 'audio') return [{ kind: 'audio' }];
|
||||
if (messageType === 'media') {
|
||||
return [{
|
||||
kind: 'video',
|
||||
...(nonEmptyString(parsed?.file_name) ? { name: nonEmptyString(parsed.file_name) } : {}),
|
||||
}];
|
||||
}
|
||||
if (messageType === 'sticker') return [{ kind: 'other' }];
|
||||
return [];
|
||||
}
|
||||
|
||||
function feishuReplyReference(event, client) {
|
||||
const messageId = feishuReplyTargetId(event);
|
||||
if (!messageId) return null;
|
||||
const chatId = nonEmptyString(event?.message?.chat_id);
|
||||
return {
|
||||
messageId,
|
||||
async load({ signal } = {}) {
|
||||
signal?.throwIfAborted();
|
||||
let response;
|
||||
try {
|
||||
response = await client?.im?.v1?.message?.get?.({
|
||||
path: { message_id: messageId },
|
||||
params: {
|
||||
with_sender_name: true,
|
||||
card_msg_content_type: FEISHU_CARD_MESSAGE_CONTENT_TYPE,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
signal?.throwIfAborted();
|
||||
if (await feishuProviderCode(error, signal) === FEISHU_MISSING_MESSAGE_SCOPE_CODE) {
|
||||
return { messageId, unavailableReason: 'permission-denied' };
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
signal?.throwIfAborted();
|
||||
if (providerCode(response) === FEISHU_MISSING_MESSAGE_SCOPE_CODE) {
|
||||
return { messageId, unavailableReason: 'permission-denied' };
|
||||
}
|
||||
if (providerCode(response) !== null && providerCode(response) !== 0) {
|
||||
return { messageId, unavailableReason: 'not-delivered' };
|
||||
}
|
||||
const item = response?.data?.items?.find?.(
|
||||
(candidate) => nonEmptyString(candidate?.message_id) === messageId,
|
||||
);
|
||||
if (!item) return { messageId, unavailableReason: 'not-found' };
|
||||
if (item.deleted) return { messageId, unavailableReason: 'deleted' };
|
||||
const itemChatId = nonEmptyString(item.chat_id);
|
||||
if (!chatId || !itemChatId || itemChatId !== chatId) {
|
||||
return { messageId, unavailableReason: 'not-found' };
|
||||
}
|
||||
|
||||
const messageType = nonEmptyString(item.msg_type) ?? '';
|
||||
const quotedEvent = {
|
||||
message: {
|
||||
message_id: messageId,
|
||||
message_type: messageType,
|
||||
content: item.body?.content,
|
||||
mentions: item.mentions ?? [],
|
||||
},
|
||||
};
|
||||
const parsed = parsedMessageContent(quotedEvent);
|
||||
const post = postContent(quotedEvent, parsed);
|
||||
const quoted = extractInboundMessage(quotedEvent, client);
|
||||
const content = messageType === 'interactive'
|
||||
? interactiveCardText(parsed)
|
||||
: quoted.content;
|
||||
const attachments = feishuReplyAttachments(messageType, parsed, post);
|
||||
return {
|
||||
messageId,
|
||||
...(nonEmptyString(item.sender?.id) ? { authorId: nonEmptyString(item.sender.id) } : {}),
|
||||
...(nonEmptyString(item.sender?.sender_name)
|
||||
? { authorName: nonEmptyString(item.sender.sender_name) }
|
||||
: {}),
|
||||
...(content ? { content } : {}),
|
||||
attachments,
|
||||
...(messageType === 'interactive' && !content && attachments.length === 0
|
||||
? { unavailableReason: 'unsupported' }
|
||||
: {}),
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function extractInboundMessage(event, client) {
|
||||
const messageType = event?.message?.message_type;
|
||||
const parsed = parsedMessageContent(event);
|
||||
|
|
@ -292,10 +519,12 @@ export function extractInboundMessage(event, client) {
|
|||
: null;
|
||||
const imageKeys = standaloneImageKey ? [standaloneImageKey] : post?.imageKeys ?? [];
|
||||
const file = messageType === 'file' ? feishuFileSource(event, client, parsed) : null;
|
||||
const replyTo = feishuReplyReference(event, client);
|
||||
return {
|
||||
content: messageType === 'text' ? extractText(event) ?? '' : post?.text ?? '',
|
||||
images: imageKeys.map((key) => feishuImageSource(event, client, key)),
|
||||
files: file ? [file] : [],
|
||||
...(replyTo ? { replyTo } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue