fix: let channels control reply length
ChannelService previously truncated every reply at 4,000 characters and discarded the remainder before the platform adapter could handle it. Complete executor output now reaches each channel driver, while error redaction and channel-specific platform limits remain intact. Release note: 消息通道回复不再由公共服务统一截断;完整结果现在交由微信、企业微信或钉钉按各自平台能力处理。
This commit is contained in:
+1
-1
@@ -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 加密设置、环境变量只读覆盖、连接测试、动态启停、发送者范围和状态诊断。
|
||||
|
||||
@@ -58,7 +58,7 @@ GoodBuddy 已将企业微信、钉钉和微信 ClawBot 远程消息通道纳入
|
||||
### 3.2 产品目标
|
||||
|
||||
- 将远程通道纳入 GoodBuddy 现有 Project、Conversation、Task、Activity 和 Artifact 信息架构。
|
||||
- 复用现有 ChannelService 的白名单、去重、并发、取消、输出限制和错误脱敏能力。
|
||||
- 复用现有 ChannelService 的白名单、去重、并发、取消和错误脱敏能力;回复长度与分段由各通道适配器按平台能力控制。
|
||||
- 保持 Electron Main、Preload、Renderer 和不可信子进程之间的安全边界。
|
||||
- 为后续语音、视频、多账号和更多通道提供稳定扩展点。
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}),
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user