feat: unify execution and add workspace file imports
Remove Ask/Execute modes and general product tool approvals so requests use the selected runtime, enabled capabilities, and execution account permissions. Migrate retired application metadata while preserving user content. Retire the Results page while retaining conversation images and stored artifacts. Add local and remote workspace file imports, timestamps and sorting, improve image discovery and saving, and refine workspace navigation and supervision responsiveness. Include the accompanying web-search changes, tests, probes, and product documentation. Remote imports require an updated Agent. Existing artifacts are not automatically copied into project directories. Validation evidence and outstanding failures remain documented in feature progress files; no full test suite or real-model calls were rerun for this commit. Release note: Run requests without switching work modes, import files into a selected local or remote workspace folder, sort workspace files by date, and keep using conversation images after removal of the Results page.
This commit is contained in:
@@ -57,7 +57,6 @@ function inbound(
|
||||
conversationType: 'direct',
|
||||
text: '你好',
|
||||
mentioned: false,
|
||||
workMode: 'ask',
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
@@ -72,7 +71,7 @@ async function waitForSent(
|
||||
}
|
||||
|
||||
describe('channel contracts', () => {
|
||||
it('normalizes text, defaults to ask, and refuses non-ask modes', () => {
|
||||
it('normalizes text without a mode and rejects obsolete mode fields', () => {
|
||||
expect(
|
||||
channelInboundTextSchema.parse({
|
||||
channel: ' fake ',
|
||||
@@ -90,8 +89,7 @@ describe('channel contracts', () => {
|
||||
conversationId: 'direct-1',
|
||||
conversationType: 'direct',
|
||||
text: '你好',
|
||||
mentioned: false,
|
||||
workMode: 'ask'
|
||||
mentioned: false
|
||||
})
|
||||
|
||||
expect(
|
||||
@@ -162,7 +160,7 @@ describe('ChannelService', () => {
|
||||
await service.stop()
|
||||
})
|
||||
|
||||
it('executes an allowed request asynchronously with the normalized ask mode', async () => {
|
||||
it('executes an allowed request asynchronously without a mode', async () => {
|
||||
const driver = new FakeChannelDriver()
|
||||
let finish: ((value: { status: string; output: string }) => void) | undefined
|
||||
const executor = vi.fn(
|
||||
@@ -191,12 +189,16 @@ describe('ChannelService', () => {
|
||||
})
|
||||
expect(executor).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
text: '帮我分析',
|
||||
workMode: 'ask'
|
||||
text: '帮我分析'
|
||||
}),
|
||||
expect.any(AbortSignal),
|
||||
expect.any(Function)
|
||||
)
|
||||
expect(executor).toHaveBeenCalledWith(
|
||||
expect.not.objectContaining({ workMode: expect.anything() }),
|
||||
expect.any(AbortSignal),
|
||||
expect.any(Function)
|
||||
)
|
||||
expect(driver.sent).toEqual([])
|
||||
|
||||
finish?.({ status: 'completed', output: '完成' })
|
||||
|
||||
@@ -73,7 +73,6 @@ describe('DingTalkChannelDriver', () => {
|
||||
conversationType: 'group',
|
||||
text: '请总结进展',
|
||||
mentioned: true,
|
||||
workMode: 'ask',
|
||||
receivedAt: 1_800_000_000_000
|
||||
}
|
||||
])
|
||||
|
||||
@@ -289,7 +289,6 @@ export class DingTalkChannelDriver implements ChannelDriver {
|
||||
conversationType: message.conversationType,
|
||||
text: message.text,
|
||||
mentioned: message.conversationType === 'group',
|
||||
workMode: 'ask',
|
||||
receivedAt: message.createdAt
|
||||
}
|
||||
await handler(inbound, () => undefined)
|
||||
|
||||
@@ -6,37 +6,37 @@ import {
|
||||
import { projectChannelLabels } from '../../shared/assistant-contracts'
|
||||
|
||||
describe('parseRemoteChannelPrompt', () => {
|
||||
it('uses the channel project default mode without changing the prompt', () => {
|
||||
it('trims the prompt without adding a mode', () => {
|
||||
expect(
|
||||
parseRemoteChannelPrompt(' 请整理下载目录 ', 'execute')
|
||||
parseRemoteChannelPrompt(' 请整理下载目录 ')
|
||||
).toEqual({
|
||||
workMode: 'execute',
|
||||
prompt: '请整理下载目录'
|
||||
})
|
||||
expect(parseRemoteChannelPrompt('总结进展', 'ask')).toEqual({
|
||||
workMode: 'ask',
|
||||
expect(parseRemoteChannelPrompt('总结进展')).toEqual({
|
||||
prompt: '总结进展'
|
||||
})
|
||||
})
|
||||
|
||||
it.each([
|
||||
['/ask 请只读分析', 'ask', '请只读分析'],
|
||||
['/execute: 创建文件', 'execute', '创建文件'],
|
||||
['/exec 执行测试', 'execute', '执行测试'],
|
||||
['对话:解释错误', 'ask', '解释错误'],
|
||||
['执行: 更新依赖', 'execute', '更新依赖']
|
||||
'/ask 请只读分析',
|
||||
'/execute: 创建文件',
|
||||
'/exec 执行测试',
|
||||
'对话:解释错误',
|
||||
'问答:解释错误',
|
||||
'执行: 更新依赖',
|
||||
'/ask',
|
||||
'/execute'
|
||||
] as const)(
|
||||
'parses explicit mode prefix %s',
|
||||
(text, workMode, prompt) => {
|
||||
expect(parseRemoteChannelPrompt(text, 'ask')).toEqual({
|
||||
workMode,
|
||||
prompt
|
||||
'preserves former mode prefix as literal text: %s',
|
||||
(text) => {
|
||||
expect(parseRemoteChannelPrompt(text)).toEqual({
|
||||
prompt: text
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
it('rejects a prefix without a request body', () => {
|
||||
expect(() => parseRemoteChannelPrompt('/execute', 'ask')).toThrow(
|
||||
it('rejects an empty request', () => {
|
||||
expect(() => parseRemoteChannelPrompt(' ')).toThrow(
|
||||
'远程请求内容不能为空'
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,39 +1,13 @@
|
||||
import type { InteractiveWorkMode } from '../../shared/assistant-contracts'
|
||||
|
||||
const COMMAND_PATTERN =
|
||||
/^\/(?<command>ask|execute|exec)(?=$|[\s::])[\s::]*/iu
|
||||
const CHINESE_PATTERN =
|
||||
/^(?<command>对话|问答|执行)(?=$|[\s::])[\s::]*/u
|
||||
|
||||
export function parseRemoteChannelPrompt(
|
||||
text: string,
|
||||
defaultWorkMode: InteractiveWorkMode
|
||||
text: string
|
||||
): {
|
||||
workMode: InteractiveWorkMode
|
||||
prompt: string
|
||||
} {
|
||||
const value = text.trim()
|
||||
const commandMatch = COMMAND_PATTERN.exec(value)
|
||||
const chineseMatch = commandMatch ? undefined : CHINESE_PATTERN.exec(value)
|
||||
const match = commandMatch ?? chineseMatch
|
||||
const command = (
|
||||
match?.groups?.command ?? ''
|
||||
).toLocaleLowerCase()
|
||||
const workMode =
|
||||
command === 'execute' ||
|
||||
command === 'exec' ||
|
||||
command === '执行'
|
||||
? 'execute'
|
||||
: command === 'ask' ||
|
||||
command === '对话' ||
|
||||
command === '问答'
|
||||
? 'ask'
|
||||
: defaultWorkMode
|
||||
const prompt = match ? value.slice(match[0].length).trim() : value
|
||||
const prompt = text.trim()
|
||||
if (!prompt) {
|
||||
throw new Error('远程请求内容不能为空')
|
||||
}
|
||||
return { workMode, prompt }
|
||||
return { prompt }
|
||||
}
|
||||
|
||||
export function requestsRemoteResultFile(text: string): boolean {
|
||||
|
||||
@@ -80,7 +80,6 @@ describe('WechatChannelDriver', () => {
|
||||
accountId: 'bot-account',
|
||||
eventId: 'event-1',
|
||||
senderId: 'sender-1',
|
||||
workMode: 'ask',
|
||||
attachments: [
|
||||
expect.objectContaining({ name: '说明.txt' })
|
||||
]
|
||||
|
||||
@@ -195,7 +195,6 @@ export class WechatChannelDriver implements ChannelDriver {
|
||||
attachments: message.attachments,
|
||||
attachmentError: message.attachmentError,
|
||||
mentioned: false,
|
||||
workMode: 'ask',
|
||||
receivedAt: Date.now()
|
||||
},
|
||||
() => undefined
|
||||
|
||||
@@ -94,7 +94,6 @@ describe('WeComChannelDriver', () => {
|
||||
conversationType: 'group',
|
||||
text: '@GoodBuddy 请规划下一步',
|
||||
mentioned: true,
|
||||
workMode: 'ask',
|
||||
receivedAt: 1_700_000_000
|
||||
})
|
||||
|
||||
|
||||
@@ -131,7 +131,6 @@ export class WeComChannelDriver implements ChannelDriver {
|
||||
message.chatType === 'group' ? 'group' : 'direct',
|
||||
text: message.text,
|
||||
mentioned: message.mentionedBot,
|
||||
workMode: 'ask',
|
||||
...(message.createdAt === undefined
|
||||
? {}
|
||||
: { receivedAt: message.createdAt })
|
||||
|
||||
Reference in New Issue
Block a user