diff --git a/FEATURES.md b/FEATURES.md index f3f9c1c..ba82ecd 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -66,7 +66,7 @@ - [x] **直连模型内置浏览器**:使用 GoodBuddy 内置的隔离 Chromium,不控制客户端已安装的浏览器;用户通过独立总开关决定是否提供给 Execute,开启后不逐次询问。 - [x] **客户端电脑控制工具**:与内置浏览器分开管理,并保留范围、取消、超时、输出边界和执行记录。 -- [x] **远程消息通道项目**:微信 ClawBot、企业微信和钉钉分别拥有系统管理的项目、独立远程会话、工作目录、处理后端、默认 Ask/Execute 模式及任务活动归属。 +- [x] **远程消息通道项目**:微信 ClawBot、企业微信和钉钉分别拥有系统管理的项目、独立远程会话、工作目录、处理后端、默认 Ask/Execute 模式及任务活动归属;完整回复交由各通道按平台能力控制长度与分段,不再由公共服务统一截断。 - [x] **微信 ClawBot 扫码与媒体**:通过独立 Sidecar 完成本机扫码、验证码、加密凭据和文字收发;支持个人微信私聊图片与文件,单条消息最多 4 个附件、解密后合计不超过 12MB。 - [x] **微信安全回传**:支持返回当前任务生成的图片,或在用户明确要求时将本次最终文本生成为 Markdown 附件;不自动读取或发送已有工作区文件。 - [x] **企业微信与钉钉连接**:支持 Main-only 加密设置、环境变量只读覆盖、连接测试、动态启停、发送者范围和状态诊断。 diff --git a/docs/features/wechat-clawbot-channel-project-prd.md b/docs/features/wechat-clawbot-channel-project-prd.md index 7856b32..6fcebc4 100644 --- a/docs/features/wechat-clawbot-channel-project-prd.md +++ b/docs/features/wechat-clawbot-channel-project-prd.md @@ -58,7 +58,7 @@ GoodBuddy 已将企业微信、钉钉和微信 ClawBot 远程消息通道纳入 ### 3.2 产品目标 - 将远程通道纳入 GoodBuddy 现有 Project、Conversation、Task、Activity 和 Artifact 信息架构。 -- 复用现有 ChannelService 的白名单、去重、并发、取消、输出限制和错误脱敏能力。 +- 复用现有 ChannelService 的白名单、去重、并发、取消和错误脱敏能力;回复长度与分段由各通道适配器按平台能力控制。 - 保持 Electron Main、Preload、Renderer 和不可信子进程之间的安全边界。 - 为后续语音、视频、多账号和更多通道提供稳定扩展点。 diff --git a/src/main/channels/channel-service.test.ts b/src/main/channels/channel-service.test.ts index 1ca6cc7..45dc621 100644 --- a/src/main/channels/channel-service.test.ts +++ b/src/main/channels/channel-service.test.ts @@ -519,14 +519,15 @@ describe('ChannelService', () => { await service.stop() }) - it('bounds output and redacts executor-provided error details', async () => { + it('preserves output and redacts executor-provided error details', async () => { const driver = new FakeChannelDriver() const outbox = new MemoryOutbox() + const output = 'x'.repeat(20_000) const executor = vi .fn() .mockResolvedValueOnce({ status: 'completed', - output: 'x'.repeat(100) + output }) .mockResolvedValueOnce({ status: 'failed', @@ -535,7 +536,6 @@ describe('ChannelService', () => { }) const service = new ChannelService(driver, executor, { allowedSenderIds: ['allowed-user'], - maximumResultLength: 32, outbox }) await service.start() @@ -544,7 +544,7 @@ describe('ChannelService', () => { await driver.emit(inbound({ eventId: 'secret-error' })) await waitForSent(driver, 2) - expect(driver.sent[0]?.output).toHaveLength(32) + expect(driver.sent[0]?.output).toBe(output) const serialized = JSON.stringify(driver.sent[1]) expect(serialized).not.toContain('top-secret') expect(serialized).not.toContain('abc123') diff --git a/src/main/channels/channel-service.ts b/src/main/channels/channel-service.ts index ca23103..e25835d 100644 --- a/src/main/channels/channel-service.ts +++ b/src/main/channels/channel-service.ts @@ -15,14 +15,11 @@ import { type Outbox } from './channel-driver' -const TRUNCATION_MARKER = '\n…(结果已截断)' - export type ChannelServiceOptions = { allowedSenderIds?: readonly string[] allowGroupMessages?: boolean maximumConcurrency?: number maximumInputLength?: number - maximumResultLength?: number dedupStore?: DedupStore outbox?: Outbox onDeliveryFailure?: (error: unknown) => void @@ -48,19 +45,6 @@ function boundedInteger( return candidate } -function truncate(value: string, maximumLength: number): string { - if (value.length <= maximumLength) { - return value - } - if (maximumLength <= TRUNCATION_MARKER.length) { - return value.slice(0, maximumLength) - } - return ( - value.slice(0, maximumLength - TRUNCATION_MARKER.length) + - TRUNCATION_MARKER - ) -} - export function redactChannelError(value: string): string { return value .replace(/\bBearer\s+[^\s,;]+/giu, 'Bearer [已隐藏]') @@ -84,7 +68,6 @@ export class ChannelService { private readonly allowGroupMessages: boolean private readonly maximumConcurrency: number private readonly maximumInputLength: number - private readonly maximumResultLength: number private readonly dedupStore: DedupStore private readonly outbox: Outbox private readonly onDeliveryFailure?: (error: unknown) => void @@ -126,12 +109,6 @@ export class ChannelService { CHANNEL_LIMITS.maximumTextLength, '通道输入长度限制' ) - this.maximumResultLength = boundedInteger( - options.maximumResultLength, - 4_000, - CHANNEL_LIMITS.maximumResultLength, - '通道结果长度限制' - ) this.dedupStore = options.dedupStore ?? new MemoryDedupStore() this.outbox = options.outbox ?? new MemoryOutbox() this.onDeliveryFailure = options.onDeliveryFailure @@ -426,13 +403,13 @@ export class ChannelService { ...(result.output === undefined ? {} : { - output: truncate(result.output, this.maximumResultLength) + output: result.output }), ...(result.error === undefined ? {} : { - error: truncate( - redactChannelError(result.error), + error: redactChannelError(result.error).slice( + 0, CHANNEL_LIMITS.maximumErrorLength ) }), diff --git a/src/shared/channel-contracts.ts b/src/shared/channel-contracts.ts index d26dada..8eca5d6 100644 --- a/src/shared/channel-contracts.ts +++ b/src/shared/channel-contracts.ts @@ -5,7 +5,6 @@ export const CHANNEL_LIMITS = { maximumEventIdLength: 256, maximumIdentityLength: 256, maximumTextLength: 32_000, - maximumResultLength: 16_000, maximumErrorLength: 1_000, maximumStatusLength: 64, maximumAttachmentCount: 4, @@ -185,10 +184,7 @@ export const channelResultMessageSchema = z .trim() .min(1) .max(CHANNEL_LIMITS.maximumStatusLength), - output: z - .string() - .max(CHANNEL_LIMITS.maximumResultLength) - .optional(), + output: z.string().optional(), error: z .string() .max(CHANNEL_LIMITS.maximumErrorLength)