mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-09 11:43:19 +08:00
feat: support inbound files across all channels
This commit is contained in:
parent
29bcb67a64
commit
0a26d7df92
45 changed files with 2651 additions and 343 deletions
|
|
@ -12,6 +12,10 @@ import {
|
|||
imagePromptUserMessage,
|
||||
promptContentForMessage,
|
||||
} from '../shared/image-prompt.mjs';
|
||||
import {
|
||||
hasInboundFiles,
|
||||
inboundFileUserMessage,
|
||||
} from '../shared/inbound-file.mjs';
|
||||
import {
|
||||
harnessAnswerForQuestion,
|
||||
harnessQuestionText,
|
||||
|
|
@ -91,7 +95,7 @@ const REPAIR_URL_HOSTS = new Set([
|
|||
const HELP_TEXT = [
|
||||
'北汇星河 AIOS 已连接 DeepSeek Harness。',
|
||||
'',
|
||||
'直接发送文字或图片即可继续当前会话。',
|
||||
'直接发送文字、图片或文件即可继续当前会话。',
|
||||
'/new 开启一个全新会话',
|
||||
'/compact 压缩当前会话的较早上下文',
|
||||
'/workspace 工作区绝对路径 切换工作区',
|
||||
|
|
@ -431,7 +435,7 @@ export class FeishuHarnessBridge {
|
|||
const processingReaction = this.#addReaction(messageId, 'OnIt');
|
||||
const commandMessage = extractInboundMessage(event, this.#client);
|
||||
const commandText = nonEmptyString(commandMessage.content) ?? '';
|
||||
const commandRunner = isControlCommand(commandText)
|
||||
const commandRunner = hasInboundFiles(commandMessage) ? null : isControlCommand(commandText)
|
||||
? runControlCommand
|
||||
: (isModelCommand(commandText)
|
||||
? runModelCommand
|
||||
|
|
@ -610,7 +614,8 @@ export class FeishuHarnessBridge {
|
|||
await this.#finishReaction(messageId, processingReaction, 'ERROR');
|
||||
await this.#send(
|
||||
event.message.chat_id,
|
||||
imagePromptUserMessage(error)
|
||||
inboundFileUserMessage(error)
|
||||
?? imagePromptUserMessage(error)
|
||||
?? '处理失败,请稍后重试。如果问题持续,请在 DeepSeek Harness 的飞书插件页面检查连接状态。',
|
||||
).catch(() => undefined);
|
||||
}
|
||||
|
|
@ -641,6 +646,7 @@ export class FeishuHarnessBridge {
|
|||
{
|
||||
signal: this.#signal,
|
||||
hasImages: hasInboundImages(message),
|
||||
hasFiles: hasInboundFiles(message),
|
||||
pendingInteraction: this.#pendingInteractions.has(key)
|
||||
|| this.#approvals.hasPending(key),
|
||||
control: { owner: this, key },
|
||||
|
|
@ -671,9 +677,10 @@ export class FeishuHarnessBridge {
|
|||
const message = extractInboundMessage(event, this.#client);
|
||||
const text = message.content;
|
||||
const hasImages = hasInboundImages(message);
|
||||
const commandText = event.message.message_type === 'text' && !hasImages ? text : null;
|
||||
if (!text && !hasImages) {
|
||||
await this.#send(event.message.chat_id, '目前支持文字和图片消息。');
|
||||
const hasFiles = hasInboundFiles(message);
|
||||
const commandText = event.message.message_type === 'text' && !hasImages && !hasFiles ? text : null;
|
||||
if (!text && !hasImages && !hasFiles) {
|
||||
await this.#send(event.message.chat_id, '目前支持文字、图片和文件消息。');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1603,7 +1610,7 @@ export class FeishuHarnessBridge {
|
|||
}
|
||||
}
|
||||
|
||||
#interactionAskOptions(event, key) {
|
||||
#interactionAskOptions(event, key, files) {
|
||||
return {
|
||||
timeoutMs: this.#replyTimeoutMs,
|
||||
signal: this.#signal,
|
||||
|
|
@ -1615,6 +1622,7 @@ export class FeishuHarnessBridge {
|
|||
requiresMention: event.message.chat_type !== 'p2p',
|
||||
}),
|
||||
onInteractionResolved: (resolution) => this.#handleInteractionResolved(resolution),
|
||||
files,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -1709,7 +1717,7 @@ export class FeishuHarnessBridge {
|
|||
content,
|
||||
createOptions: { signal: this.#signal },
|
||||
existsOptions: { signal: this.#signal },
|
||||
askOptions: this.#interactionAskOptions(event, key),
|
||||
askOptions: this.#interactionAskOptions(event, key, message.files),
|
||||
});
|
||||
let textReceipt;
|
||||
let textSendError = null;
|
||||
|
|
@ -1749,7 +1757,7 @@ export class FeishuHarnessBridge {
|
|||
markdown: async (controller) => {
|
||||
promptStarted = true;
|
||||
const askOptions = {
|
||||
...this.#interactionAskOptions(event, key),
|
||||
...this.#interactionAskOptions(event, key, message.files),
|
||||
onUpdate: async (update) => {
|
||||
await controller.setContent(this.#progressText(update));
|
||||
this.#status.streamUpdates = (this.#status.streamUpdates ?? 0) + 1;
|
||||
|
|
@ -1821,7 +1829,7 @@ export class FeishuHarnessBridge {
|
|||
content,
|
||||
createOptions: { signal: this.#signal },
|
||||
existsOptions: { signal: this.#signal },
|
||||
askOptions: this.#interactionAskOptions(event, key),
|
||||
askOptions: this.#interactionAskOptions(event, key, message.files),
|
||||
});
|
||||
let textReceipt;
|
||||
let textSendError = null;
|
||||
|
|
|
|||
|
|
@ -125,6 +125,31 @@ async function readBoundedStream(stream, { signal, maxBytes }) {
|
|||
}
|
||||
}
|
||||
|
||||
async function readStream(stream, { signal }) {
|
||||
if (!stream || typeof stream[Symbol.asyncIterator] !== 'function') {
|
||||
throw new Error('Feishu file download returned no readable stream');
|
||||
}
|
||||
signal?.throwIfAborted();
|
||||
const abort = () => stream.destroy?.(
|
||||
signal.reason ?? new DOMException('Feishu file download aborted', 'AbortError'),
|
||||
);
|
||||
signal?.addEventListener('abort', abort, { once: true });
|
||||
const chunks = [];
|
||||
let size = 0;
|
||||
try {
|
||||
for await (const chunk of stream) {
|
||||
signal?.throwIfAborted();
|
||||
const data = Buffer.from(chunk);
|
||||
size += data.length;
|
||||
chunks.push(data);
|
||||
}
|
||||
signal?.throwIfAborted();
|
||||
return Buffer.concat(chunks, size);
|
||||
} finally {
|
||||
signal?.removeEventListener('abort', abort);
|
||||
}
|
||||
}
|
||||
|
||||
function providerCode(value) {
|
||||
if (!value || typeof value !== 'object') return null;
|
||||
const code = value.code ?? value.error?.code;
|
||||
|
|
@ -232,6 +257,26 @@ function feishuImageSource(event, client, key) {
|
|||
};
|
||||
}
|
||||
|
||||
function feishuFileSource(event, client, file) {
|
||||
const key = nonEmptyString(file?.file_key);
|
||||
if (!key) return null;
|
||||
return {
|
||||
name: nonEmptyString(file?.file_name) ?? 'file',
|
||||
async load({ signal } = {}) {
|
||||
signal?.throwIfAborted();
|
||||
const resource = await client?.im?.v1?.messageResource?.get?.({
|
||||
path: {
|
||||
message_id: event.message.message_id,
|
||||
file_key: key,
|
||||
},
|
||||
params: { type: 'file' },
|
||||
});
|
||||
signal?.throwIfAborted();
|
||||
return readStream(resource?.getReadableStream?.(), { signal });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function extractInboundMessage(event, client) {
|
||||
const messageType = event?.message?.message_type;
|
||||
const parsed = parsedMessageContent(event);
|
||||
|
|
@ -240,9 +285,11 @@ export function extractInboundMessage(event, client) {
|
|||
? nonEmptyString(parsed?.image_key)
|
||||
: null;
|
||||
const imageKeys = standaloneImageKey ? [standaloneImageKey] : post?.imageKeys ?? [];
|
||||
const file = messageType === 'file' ? feishuFileSource(event, client, parsed) : null;
|
||||
return {
|
||||
content: messageType === 'text' ? extractText(event) ?? '' : post?.text ?? '',
|
||||
images: imageKeys.map((key) => feishuImageSource(event, client, key)),
|
||||
files: file ? [file] : [],
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue