mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-19 06:30:44 +08:00
fix: preserve quoted message context across channels
This commit is contained in:
parent
c2be2389b0
commit
94e114b9b3
46 changed files with 4242 additions and 321 deletions
|
|
@ -2,9 +2,11 @@ import {
|
|||
DEFAULT_WEIXIN_MAX_MESSAGE_CHARS,
|
||||
extractWeixinFiles,
|
||||
extractWeixinImages,
|
||||
extractWeixinReplyReference,
|
||||
extractWeixinText,
|
||||
splitWeixinText,
|
||||
weixinMessageId,
|
||||
weixinMessageTimestampMs,
|
||||
} from './weixin-api.mjs';
|
||||
import {
|
||||
harnessAnswerForQuestion,
|
||||
|
|
@ -38,7 +40,6 @@ import {
|
|||
hasInboundImages,
|
||||
imagePromptDiagnostic,
|
||||
imagePromptUserMessage,
|
||||
promptContentForMessage,
|
||||
} from '../shared/image-prompt.mjs';
|
||||
import {
|
||||
hasInboundFiles,
|
||||
|
|
@ -47,10 +48,15 @@ import {
|
|||
} from '../shared/inbound-file.mjs';
|
||||
import { rememberConnectionTestTarget } from '../shared/connection-test.mjs';
|
||||
import { deliverOutboundArtifacts } from '../shared/semantic/artifact-delivery.mjs';
|
||||
import {
|
||||
hasReplyReference,
|
||||
promptContentForInboundMessage,
|
||||
} from '../shared/semantic/reply-reference.mjs';
|
||||
import {
|
||||
createDeliveryReceipt,
|
||||
providerMessageIdsFor,
|
||||
} from '../shared/semantic/delivery.mjs';
|
||||
import { recoverAssistantTextByTimestamp } from '../shared/session-reply-recovery.mjs';
|
||||
import {
|
||||
channelDeliveryFailure,
|
||||
clearLastMessageFailure,
|
||||
|
|
@ -67,6 +73,10 @@ const INTERACTION_RESOLVED_TEXT = () => t('这个问题已在其他客户端处
|
|||
const DEFAULT_TYPING_KEEPALIVE_MS = 5_000;
|
||||
const TYPING_RETRY_DELAY_MS = 60_000;
|
||||
const WEIXIN_SEND_DIAGNOSTIC = Symbol('weixin-send-diagnostic');
|
||||
const WEIXIN_REPLY_HISTORY_PAGE_SIZE = 100;
|
||||
const WEIXIN_REPLY_HISTORY_MAX_PAGES = 3;
|
||||
const WEIXIN_REPLY_HISTORY_TIMEOUT_MS = 5_000;
|
||||
const WEIXIN_REPLY_HISTORY_MATCH_TOLERANCE_MS = 15_000;
|
||||
|
||||
const HELP_TEXT = () => [
|
||||
t('微信已连接 DeepSeek Harness。'),
|
||||
|
|
@ -182,7 +192,20 @@ function weixinSendFailureOptions(error) {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
export function weixinInboundMessage(message, api) {
|
||||
export function weixinInboundMessage(message, api, state, { loadReplyContent } = {}) {
|
||||
const toUserId = nonEmptyString(message?.from_user_id);
|
||||
const replyTo = extractWeixinReplyReference(message, {
|
||||
resolveContent: (reference) => state?.recentOutboundTextFor?.({
|
||||
toUserId,
|
||||
...reference,
|
||||
}),
|
||||
...(typeof loadReplyContent === 'function' ? {
|
||||
loadContent: (reference, options) => loadReplyContent({
|
||||
toUserId,
|
||||
...reference,
|
||||
}, options),
|
||||
} : {}),
|
||||
});
|
||||
return {
|
||||
content: extractWeixinText(message) ?? '',
|
||||
images: typeof api?.inboundImages === 'function'
|
||||
|
|
@ -191,6 +214,7 @@ export function weixinInboundMessage(message, api) {
|
|||
files: typeof api?.inboundFiles === 'function'
|
||||
? api.inboundFiles(message)
|
||||
: extractWeixinFiles(message),
|
||||
...(replyTo ? { replyTo } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -375,7 +399,8 @@ export class WeixinHarnessBridge {
|
|||
&& (this.#queues.has(key) || pending || this.#approvals.hasPending(key))
|
||||
? { handled: true, kind: 'busy', message: batchInputBusyMessage() }
|
||||
: this.#batchInputs.handle(key, commandText, {
|
||||
plainText: isNativeWeixinText(message),
|
||||
plainText: isNativeWeixinText(message)
|
||||
&& !extractWeixinReplyReference(message),
|
||||
});
|
||||
if (result.handled) {
|
||||
if (result.kind === 'submit') {
|
||||
|
|
@ -489,7 +514,7 @@ export class WeixinHarnessBridge {
|
|||
batchSubmission = null,
|
||||
} = {}) {
|
||||
const preparedMessage = prefetchInboundFiles(
|
||||
weixinInboundMessage(message, this.#api),
|
||||
this.#inboundMessage(message),
|
||||
{ signal: this.#signal },
|
||||
);
|
||||
const previous = this.#queues.get(key) ?? Promise.resolve();
|
||||
|
|
@ -508,6 +533,54 @@ export class WeixinHarnessBridge {
|
|||
return current;
|
||||
}
|
||||
|
||||
#inboundMessage(message) {
|
||||
return weixinInboundMessage(message, this.#api, this.#state, {
|
||||
loadReplyContent: (reference, options) => this.#loadReplyContent(reference, options),
|
||||
});
|
||||
}
|
||||
|
||||
async #loadReplyContent(reference, { signal: callerSignal } = {}) {
|
||||
const indexed = this.#state.recentOutboundTextFor?.(reference);
|
||||
if (indexed) return { content: indexed };
|
||||
const quotedAt = [
|
||||
weixinMessageTimestampMs(reference?.messageId),
|
||||
Number(reference?.createTimeMs),
|
||||
Number(reference?.updateTimeMs),
|
||||
].find(Number.isSafeInteger);
|
||||
if (quotedAt === undefined) return { unavailableReason: 'not-delivered' };
|
||||
const sender = nonEmptyString(reference?.toUserId);
|
||||
const sessionId = sender ? this.#state.sessionFor(conversationKey(sender)) : null;
|
||||
const session = typeof sessionId === 'string' && sessionId
|
||||
? this.#harness.workspaceSession?.(sessionId)
|
||||
: null;
|
||||
if (typeof session?.readHistory !== 'function') {
|
||||
return { unavailableReason: 'not-delivered' };
|
||||
}
|
||||
const text = await recoverAssistantTextByTimestamp({
|
||||
session,
|
||||
quotedAt,
|
||||
signal: callerSignal,
|
||||
pageSize: WEIXIN_REPLY_HISTORY_PAGE_SIZE,
|
||||
maxPages: WEIXIN_REPLY_HISTORY_MAX_PAGES,
|
||||
timeoutMs: WEIXIN_REPLY_HISTORY_TIMEOUT_MS,
|
||||
toleranceMs: WEIXIN_REPLY_HISTORY_MATCH_TOLERANCE_MS,
|
||||
});
|
||||
if (!text) return { unavailableReason: 'not-delivered' };
|
||||
const messageId = nonEmptyString(reference?.messageId);
|
||||
try {
|
||||
await this.#state.rememberOutboundMessage?.({
|
||||
toUserId: sender,
|
||||
text,
|
||||
sentAt: quotedAt,
|
||||
completedAt: quotedAt,
|
||||
providerMessageIds: messageId ? [messageId] : [],
|
||||
});
|
||||
} catch (error) {
|
||||
this.#logger.warn?.('[dsh-weixin] failed to remember a recovered quote:', error);
|
||||
}
|
||||
return { content: text };
|
||||
}
|
||||
|
||||
#finishBatchResult(message, messageId, key, sender, contextToken, runId, result) {
|
||||
let task;
|
||||
task = Promise.resolve().then(async () => {
|
||||
|
|
@ -644,11 +717,12 @@ export class WeixinHarnessBridge {
|
|||
let batchSettled = batchSubmission === null;
|
||||
let promptRecorded = false;
|
||||
try {
|
||||
const promptMessage = preparedMessage ?? weixinInboundMessage(message, this.#api);
|
||||
const promptMessage = preparedMessage ?? this.#inboundMessage(message);
|
||||
const text = promptMessage.content;
|
||||
const hasImages = hasInboundImages(promptMessage);
|
||||
const hasFiles = hasInboundFiles(promptMessage);
|
||||
if (!text && !hasImages && !hasFiles) {
|
||||
const hasReply = hasReplyReference(promptMessage);
|
||||
if (!text && !hasImages && !hasFiles && !hasReply) {
|
||||
await this.#send(sender, t('目前支持文字、图片、文件,以及微信已转成文字的语音消息。'), contextToken, runId);
|
||||
await this.#state.markSeen(messageId);
|
||||
return;
|
||||
|
|
@ -701,8 +775,8 @@ export class WeixinHarnessBridge {
|
|||
let artifacts = [];
|
||||
await this.#startTyping(sender, contextToken);
|
||||
try {
|
||||
let content = hasImages
|
||||
? await promptContentForMessage(promptMessage, { signal: this.#signal })
|
||||
let content = hasImages || hasReply
|
||||
? await promptContentForInboundMessage(promptMessage, { signal: this.#signal })
|
||||
: undefined;
|
||||
const snapshot = this.#acceptedMessageIds.get(messageId);
|
||||
let contextEnhanced = false;
|
||||
|
|
@ -1154,6 +1228,7 @@ export class WeixinHarnessBridge {
|
|||
const chunks = splitWeixinText(text, this.#maxMessageChars);
|
||||
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex += 1) {
|
||||
const chunk = chunks[chunkIndex];
|
||||
const sentAt = Date.now();
|
||||
try {
|
||||
const result = await this.#api.sendText({
|
||||
baseUrl: this.#baseUrl,
|
||||
|
|
@ -1164,7 +1239,19 @@ export class WeixinHarnessBridge {
|
|||
runId,
|
||||
signal: this.#signal,
|
||||
});
|
||||
providerMessageIds.push(...providerMessageIdsFor(result));
|
||||
const chunkMessageIds = providerMessageIdsFor(result);
|
||||
providerMessageIds.push(...chunkMessageIds);
|
||||
try {
|
||||
await this.#state.rememberOutboundMessage?.({
|
||||
toUserId,
|
||||
text: chunk,
|
||||
sentAt,
|
||||
completedAt: Date.now(),
|
||||
providerMessageIds: chunkMessageIds,
|
||||
});
|
||||
} catch (error) {
|
||||
this.#logger.warn?.('[dsh-weixin] failed to remember an outbound message:', error);
|
||||
}
|
||||
} catch (error) {
|
||||
throw weixinSendError(error, {
|
||||
baseUrl: this.#baseUrl,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue