Merge remote-tracking branch 'upstream/main' into feat/per-bot-agent-preset

Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	lib/index.js
This commit is contained in:
iamyaojie 2026-08-21 22:34:11 +08:00
commit a1bdf2c1f8
17 changed files with 438 additions and 144 deletions

View file

@ -108,7 +108,7 @@ export class DiscordApi {
headers: {
authorization: `Bot ${this.#token}`,
'content-type': 'application/json',
'user-agent': 'DeepSeek-Harness-dsh-im (https://github.com/xmanrui/dsh-im, 0.17.0)',
'user-agent': 'DeepSeek-Harness-dsh-im (https://github.com/xmanrui/dsh-im, 0.17.1)',
},
...(body === undefined ? {} : { body: JSON.stringify(body) }),
signal: requestSignal(signal, timeoutMs),

View file

@ -13,6 +13,18 @@ export class ImagePromptError extends Error {
}
}
const HOST_ATTACHMENT_USER_MESSAGES = Object.freeze({
MODEL_DOES_NOT_SUPPORT_IMAGES:
'当前模型不支持图片,请用 /models 查看可用模型,再用 /model <序号> 切换后重发。',
IMAGE_TOO_LARGE: '图片超过宿主允许的大小,请压缩后重试。',
IMAGE_TOO_MANY_PIXELS: '图片分辨率过高,请压缩后重试。',
INVALID_IMAGE: '图片内容无效或格式不受支持,请重新发送。',
INVALID_IMAGE_BASE64: '未能读取图片内容,请重新发送。',
IMAGE_TYPE_MISMATCH: '图片格式与实际内容不一致,请重新发送。',
TOO_MANY_IMAGES: '一次发送的图片数量超过宿主限制,请减少后重试。',
IMAGES_TOO_LARGE: '图片总大小超过宿主限制,请减少图片或压缩后重试。',
});
function requestSignal(signal, timeoutMs) {
const timeout = AbortSignal.timeout(timeoutMs);
return signal ? AbortSignal.any([signal, timeout]) : timeout;
@ -263,6 +275,25 @@ export async function promptContentForMessage(message, {
return content;
}
export function imagePromptUserMessage(error) {
return error instanceof ImagePromptError ? error.userMessage : null;
/** Return only allowlisted, user-safe image failure details. */
export function imagePromptDiagnostic(error) {
if (error instanceof ImagePromptError) {
return {
code: 'image-prompt-error',
reason: error.code,
userMessage: error.userMessage,
};
}
if (error?.code !== 'attachment-error' || typeof error?.details?.reason !== 'string') {
return null;
}
const reason = error.details.reason;
const userMessage = Object.hasOwn(HOST_ATTACHMENT_USER_MESSAGES, reason)
? HOST_ATTACHMENT_USER_MESSAGES[reason]
: null;
return userMessage ? { code: 'attachment-error', reason, userMessage } : null;
}
export function imagePromptUserMessage(error) {
return imagePromptDiagnostic(error)?.userMessage ?? null;
}

View file

@ -184,7 +184,7 @@ function authenticatedHeaders(token) {
function baseInfo() {
return {
channel_version: WEIXIN_PROTOCOL_VERSION,
bot_agent: 'DeepSeekHarness/0.17.0',
bot_agent: 'DeepSeekHarness/0.17.1',
};
}

View file

@ -23,12 +23,14 @@ import { runWorkspaceCommand } from '../shared/workspace-command.mjs';
import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
import {
hasInboundImages,
imagePromptDiagnostic,
imagePromptUserMessage,
promptContentForMessage,
} from '../shared/image-prompt.mjs';
import { rememberConnectionTestTarget } from '../shared/connection-test.mjs';
const INTERACTION_RESOLVED_TEXT = '这个问题已在其他客户端处理,无需再次回答。';
const GENERIC_PROCESSING_ERROR = '消息处理失败,请稍后重试。';
const HELP_TEXT = [
'微信已连接 DeepSeek Harness。',
@ -69,6 +71,16 @@ function canClaimInteractionReply(message, pending) {
&& nonEmptyString(extractWeixinText(message));
}
function safeMessageError(error, userMessage = GENERIC_PROCESSING_ERROR) {
const diagnostic = imagePromptDiagnostic(error);
return {
code: diagnostic?.code ?? 'message-processing-failed',
reason: diagnostic?.reason ?? 'UNKNOWN',
message: diagnostic?.userMessage ?? userMessage,
at: Date.now(),
};
}
export function createWeixinBridgeStatus() {
return {
messagesReceived: 0,
@ -78,6 +90,7 @@ export function createWeixinBridgeStatus() {
lastReplyAt: null,
lastRejectedAt: null,
lastError: null,
lastMessageError: null,
};
}
@ -168,8 +181,9 @@ export class WeixinHarnessBridge {
).catch((error) => {
if (error?.code === 'turn-stopped' || this.#signal?.aborted) return;
this.#status.lastError = error?.message ?? String(error);
this.#status.lastMessageError = safeMessageError(error);
this.#logger.error?.('[dsh-weixin] failed to process a command:', error);
return this.#send(sender, '消息处理失败,请稍后重试。', contextToken, runId)
return this.#send(sender, GENERIC_PROCESSING_ERROR, contextToken, runId)
.catch(() => undefined);
}).finally(() => {
this.#acceptedMessageIds.delete(messageId);
@ -289,6 +303,7 @@ export class WeixinHarnessBridge {
if (reply) await this.#send(sender, reply, contextToken, runId);
}
this.#status.lastError = null;
this.#status.lastMessageError = null;
}
async #process(message, key, { alreadyRecorded = false } = {}) {
@ -401,6 +416,7 @@ export class WeixinHarnessBridge {
this.#status.messagesReplied += 1;
this.#status.lastReplyAt = new Date().toISOString();
this.#status.lastError = null;
this.#status.lastMessageError = null;
} catch (error) {
if (error?.code === 'turn-stopped') {
await this.#state.markSeen(messageId);
@ -408,11 +424,13 @@ export class WeixinHarnessBridge {
}
if (this.#signal?.aborted) return;
this.#status.lastError = error?.message ?? String(error);
const userMessage = imagePromptUserMessage(error) ?? GENERIC_PROCESSING_ERROR;
this.#status.lastMessageError = safeMessageError(error, userMessage);
this.#logger.error?.('[dsh-weixin] failed to process an inbound message:', error);
try {
await this.#send(
sender,
imagePromptUserMessage(error) ?? '消息处理失败,请稍后重试。',
userMessage,
contextToken,
runId,
);
@ -526,6 +544,7 @@ export class WeixinHarnessBridge {
});
this.#clearPendingInteraction(key, pending.interactionId);
this.#status.lastError = null;
this.#status.lastMessageError = null;
} catch (error) {
if (this.#signal?.aborted) return;
if (error?.code === 'interaction-not-pending') {
@ -719,13 +738,14 @@ export class WeixinHarnessBridge {
async #handleInteractionFailure(message, messageId, error) {
if (this.#signal?.aborted) return;
this.#status.lastError = error?.message ?? String(error);
this.#status.lastMessageError = safeMessageError(error);
this.#logger.error?.('[dsh-weixin] failed to process an interaction reply:', error);
if (!this.#state.hasSeen(messageId)) {
await this.#state.markSeen(messageId).catch(() => undefined);
}
await this.#send(
nonEmptyString(message?.from_user_id),
'消息处理失败,请稍后重试。',
GENERIC_PROCESSING_ERROR,
nonEmptyString(message?.context_token) ?? undefined,
nonEmptyString(message?.run_id) ?? undefined,
).catch(() => undefined);

View file

@ -62,6 +62,20 @@ function safeAccountError(code, message) {
return Object.freeze({ code, message });
}
function publicMessageError(value) {
if (!value || typeof value !== 'object'
|| typeof value.code !== 'string' || !value.code
|| typeof value.reason !== 'string' || !value.reason
|| typeof value.message !== 'string' || !value.message
|| !Number.isFinite(value.at)) return null;
return {
code: value.code.slice(0, 64),
reason: value.reason.slice(0, 128),
message: value.message.slice(0, 500),
at: value.at,
};
}
function activationStageError(code, cause) {
const error = new Error(`Weixin activation failed during ${code}`, { cause });
error.name = 'WeixinActivationStageError';
@ -351,6 +365,7 @@ export class WeixinController {
messagesReceived: runtimeStatus?.messagesReceived ?? 0,
messagesReplied: runtimeStatus?.messagesReplied ?? 0,
},
lastMessageError: publicMessageError(runtimeStatus?.lastMessageError),
error: error ? structuredClone(error) : null,
};
});