fix: align WeChat channel modes

This commit is contained in:
lofyer
2026-08-10 11:47:03 +08:00
parent a9ae00a845
commit 2cb712e4ba
11 changed files with 36 additions and 27 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "goodbuddy", "name": "goodbuddy",
"version": "0.8.10", "version": "0.8.11",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "goodbuddy", "name": "goodbuddy",
"version": "0.8.10", "version": "0.8.11",
"license": "UNLICENSED", "license": "UNLICENSED",
"dependencies": { "dependencies": {
"@modelcontextprotocol/sdk": "^1.30.0", "@modelcontextprotocol/sdk": "^1.30.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "goodbuddy", "name": "goodbuddy",
"version": "0.8.10", "version": "0.8.11",
"private": true, "private": true,
"description": "Secure desktop AI workspace with controlled Agent Runtimes", "description": "Secure desktop AI workspace with controlled Agent Runtimes",
"desktopName": "GoodBuddy", "desktopName": "GoodBuddy",
+1 -1
View File
@@ -226,5 +226,5 @@ export function startEnvironmentChannels(
export function isReadOnlyChannelMessage( export function isReadOnlyChannelMessage(
message: ChannelInboundText message: ChannelInboundText
): boolean { ): boolean {
return message.workMode === 'ask' || message.workMode === 'plan' return message.workMode === 'ask'
} }
+7 -1
View File
@@ -71,7 +71,7 @@ async function waitForSent(
} }
describe('channel contracts', () => { describe('channel contracts', () => {
it('normalizes text, defaults to ask, and strictly refuses execute mode', () => { it('normalizes text, defaults to ask, and refuses non-ask modes', () => {
expect( expect(
channelInboundTextSchema.parse({ channelInboundTextSchema.parse({
channel: ' fake ', channel: ' fake ',
@@ -98,6 +98,12 @@ describe('channel contracts', () => {
workMode: 'execute' workMode: 'execute'
}).success }).success
).toBe(false) ).toBe(false)
expect(
channelInboundTextSchema.safeParse({
...inbound(),
workMode: 'plan'
}).success
).toBe(false)
expect( expect(
channelInboundTextSchema.parse({ channelInboundTextSchema.parse({
channel: 'fake', channel: 'fake',
@@ -13,8 +13,8 @@ describe('parseRemoteChannelPrompt', () => {
workMode: 'execute', workMode: 'execute',
prompt: '请整理下载目录' prompt: '请整理下载目录'
}) })
expect(parseRemoteChannelPrompt('总结进展', 'plan')).toEqual({ expect(parseRemoteChannelPrompt('总结进展', 'ask')).toEqual({
workMode: 'plan', workMode: 'ask',
prompt: '总结进展' prompt: '总结进展'
}) })
}) })
+3 -3
View File
@@ -1,4 +1,4 @@
import type { WorkMode } from '../../shared/assistant-contracts' import type { InteractiveWorkMode } from '../../shared/assistant-contracts'
const COMMAND_PATTERN = const COMMAND_PATTERN =
/^\/(?<command>ask|execute|exec)(?=$|[\s:])[\s:]*/iu /^\/(?<command>ask|execute|exec)(?=$|[\s:])[\s:]*/iu
@@ -7,9 +7,9 @@ const CHINESE_PATTERN =
export function parseRemoteChannelPrompt( export function parseRemoteChannelPrompt(
text: string, text: string,
defaultWorkMode: WorkMode defaultWorkMode: InteractiveWorkMode
): { ): {
workMode: WorkMode workMode: InteractiveWorkMode
prompt: string prompt: string
} { } {
const value = text.trim() const value = text.trim()
+9 -9
View File
@@ -41,7 +41,7 @@ const channelMocks = vi.hoisted(() => ({
conversationType: 'direct' | 'group' conversationType: 'direct' | 'group'
text: string text: string
mentioned: boolean mentioned: boolean
workMode: 'ask' | 'plan' workMode: 'ask'
attachments?: Array<{ attachments?: Array<{
name: string name: string
mimeType: string mimeType: string
@@ -276,7 +276,7 @@ vi.mock('./agent/create-runtime', () => runtimeFactoryMocks)
vi.mock('./channels/channel-env', () => ({ vi.mock('./channels/channel-env', () => ({
isReadOnlyChannelMessage: (message: { workMode: string }) => isReadOnlyChannelMessage: (message: { workMode: string }) =>
message.workMode === 'ask' || message.workMode === 'plan', message.workMode === 'ask',
startEnvironmentChannels: vi.fn( startEnvironmentChannels: vi.fn(
(options: { executor: typeof channelMocks.executor }) => { (options: { executor: typeof channelMocks.executor }) => {
channelMocks.executor = options.executor channelMocks.executor = options.executor
@@ -1873,7 +1873,7 @@ describe('registerIpcHandlers agent terminal state', () => {
await harness.dispose() await harness.dispose()
}) })
it('bridges channel requests to read-only delegation tasks without approval', async () => { it('bridges channel ask requests to read-only tasks without approval', async () => {
let received: let received:
| { | {
request: { request: {
@@ -1928,9 +1928,9 @@ describe('registerIpcHandlers agent terminal state', () => {
senderId: 'user-1', senderId: 'user-1',
conversationId: 'conversation-1', conversationId: 'conversation-1',
conversationType: 'direct', conversationType: 'direct',
text: '请制定只读计划', text: '请只读分析',
mentioned: false, mentioned: false,
workMode: 'plan' workMode: 'ask'
}, },
new AbortController().signal new AbortController().signal
) )
@@ -1939,8 +1939,8 @@ describe('registerIpcHandlers agent terminal state', () => {
output: '只读结果' output: '只读结果'
}) })
expect(received?.request).toMatchObject({ expect(received?.request).toMatchObject({
workMode: 'plan', workMode: 'ask',
prompt: expect.stringContaining('请制定只读计划') prompt: expect.stringContaining('请只读分析')
}) })
await expect( await expect(
received?.authorize?.({ received?.authorize?.({
@@ -1953,8 +1953,8 @@ describe('registerIpcHandlers agent terminal state', () => {
expect(harness.assistantDatabase.createTask).toHaveBeenCalledWith( expect(harness.assistantDatabase.createTask).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
title: '企业微信远程请求', title: '企业微信远程请求',
instructions: '请制定只读计划', instructions: '请只读分析',
workMode: 'plan', workMode: 'ask',
origin: 'delegation' origin: 'delegation'
}) })
) )
+2 -6
View File
@@ -1352,9 +1352,7 @@ export function registerIpcHandlers(
try { try {
parsed = parseRemoteChannelPrompt( parsed = parseRemoteChannelPrompt(
remoteInput, remoteInput,
message.workMode === 'plan' normalizeInteractiveWorkMode(project.defaultWorkMode)
? 'plan'
: project.defaultWorkMode
) )
} catch (error) { } catch (error) {
return { return {
@@ -1432,9 +1430,7 @@ export function registerIpcHandlers(
status: `${channelLabel} · ${ status: `${channelLabel} · ${
parsed.workMode === 'execute' parsed.workMode === 'execute'
? '执行' ? '执行'
: parsed.workMode === 'plan' : '对话'
? '规划'
: '对话'
}` }`
}) })
publishRemoteConversationChange() publishRemoteConversationChange()
@@ -330,6 +330,11 @@ describe('ChannelSettingsSection', () => {
const close = await screen.findByRole('button', { const close = await screen.findByRole('button', {
name: '关闭微信绑定' name: '关闭微信绑定'
}) })
expect(
screen.getByText(
'请在微信中依次打开“设置 → ClawBot → 开始扫一扫”,扫描下方二维码。二维码不会发送到第三方页面。'
)
).toBeInTheDocument()
await waitFor(() => expect(close).toHaveFocus()) await waitFor(() => expect(close).toHaveFocus())
fireEvent.keyDown(document, { key: 'Escape' }) fireEvent.keyDown(document, { key: 'Escape' })
+3 -1
View File
@@ -681,7 +681,9 @@ function WeixinQrDialog({
<header> <header>
<div> <div>
<strong id="channel-qr-title"> ClawBot</strong> <strong id="channel-qr-title"> ClawBot</strong>
<small>使</small> <small>
ClawBot
</small>
</div> </div>
<button <button
aria-label="关闭微信绑定" aria-label="关闭微信绑定"
+1 -1
View File
@@ -20,7 +20,7 @@ const channelIdentifierSchema = z
.min(1) .min(1)
.max(CHANNEL_LIMITS.maximumIdentityLength) .max(CHANNEL_LIMITS.maximumIdentityLength)
export const channelWorkModeSchema = z.enum(['ask', 'plan']) export const channelWorkModeSchema = z.literal('ask')
export type ChannelWorkMode = z.infer<typeof channelWorkModeSchema> export type ChannelWorkMode = z.infer<typeof channelWorkModeSchema>
const attachmentBase64Schema = z const attachmentBase64Schema = z