feat: 完善 Agent 标准交互与安全运行时

- 接入 AG-UI 运行投影、Turn 时间线和审批隔离

- 增加 Agent Skill 冻结绑定与运行时消费闭环

- 增加受控工作区、内置工具和私有 Artifact 生命周期
This commit is contained in:
2026-08-19 22:13:41 +08:00
parent 91d66e636d
commit 4e8640dcaf
241 changed files with 24382 additions and 2777 deletions

View File

@@ -0,0 +1,182 @@
// @vitest-environment happy-dom
import type { EasyFlowAguiRunOptions } from '../../shared/agent-agui/client';
import type { AgentInfo } from '../types';
import { EventType } from '@ag-ui/client';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { useAgentTryoutStream } from './useAgentTryoutStream';
const aguiMocks = vi.hoisted(() => ({
abort: vi.fn(),
runs: [] as EasyFlowAguiRunOptions[],
}));
vi.mock('../../shared/agent-agui/client', () => ({
EasyFlowAguiClient: class {
abort = aguiMocks.abort;
run(options: EasyFlowAguiRunOptions) {
aguiMocks.runs.push(options);
return new Promise(() => {});
}
},
}));
vi.mock('../api', () => ({
clearAgentDraftSession: vi.fn(async () => ({ errorCode: 0 })),
}));
describe('useAgentTryoutStream', () => {
beforeEach(() => {
sessionStorage.clear();
aguiMocks.runs.length = 0;
vi.clearAllMocks();
});
it('ignores late events from a stopped draft run after resending', async () => {
const stream = useAgentTryoutStream();
const payload = {
agent: { id: 'agent-1', name: 'Agent' } as AgentInfo,
knowledgeBindings: [],
prompt: '旧问题',
sessionId: 'draft-race',
skillBindings: [],
toolBindings: [],
};
void stream.sendDraft(payload);
await Promise.resolve();
stream.stop();
void stream.sendDraft({ ...payload, prompt: '新问题' });
await Promise.resolve();
aguiMocks.runs[0]?.onEvent({
delta: '旧流迟到正文',
messageId: 'old-assistant',
type: EventType.TEXT_MESSAGE_CONTENT,
});
aguiMocks.runs[1]?.onEvent({
delta: '新流正文',
messageId: 'new-assistant',
type: EventType.TEXT_MESSAGE_CONTENT,
});
const timeline = JSON.stringify(stream.timelineItems.value);
expect(timeline).toContain('新流正文');
expect(timeline).not.toContain('旧流迟到正文');
});
it('sends only the draft runtime snapshot through forwardedProps', async () => {
const stream = useAgentTryoutStream();
void stream.sendDraft({
agent: {
created: '2026-08-14 10:00:00',
displayPublishStatus: 'PUBLISHED',
id: 'agent-1',
modelId: 'model-1',
name: 'Agent',
publishedSnapshotJson: { internal: true },
},
knowledgeBindings: [
{
agentId: 'agent-1',
enabled: true,
id: 'knowledge-binding-1',
knowledgeId: 'knowledge-1',
localId: 'local-knowledge',
optionsJson: { topK: 5 },
resourceSnapshot: { internal: true },
resourceSummary: { displayName: '知识库' },
retrievalMode: 'HYBRID',
sortNo: 2,
},
],
prompt: '问题',
sessionId: 'draft-snapshot',
skillBindings: [
{
agentId: 'agent-1',
id: 'skill-binding-1',
resourceSummary: {
displayName: '合同审查',
toolCount: 2,
},
skillId: 'skill-1',
sortNo: 3,
},
],
toolBindings: [
{
agentId: 'agent-1',
enabled: true,
hitlConfigJson: { message: '请确认' },
hitlEnabled: true,
id: 'tool-binding-1',
localId: 'local-tool',
optionsJson: { timeout: 30 },
resourceSnapshot: { internal: true },
resourceSummary: { displayName: '工具' },
sortNo: 1,
targetId: 'tool-1',
toolName: 'search',
toolType: 'PLUGIN',
},
],
});
await Promise.resolve();
const forwardedProps = aguiMocks.runs[0]?.forwardedProps as {
easyflow: {
draft: {
agent: Record<string, unknown>;
knowledgeBindings: Record<string, unknown>[];
skillBindings: Record<string, unknown>[];
toolBindings: Record<string, unknown>[];
};
};
};
expect(forwardedProps.easyflow.draft.agent).toEqual(
expect.objectContaining({
id: 'agent-1',
modelId: 'model-1',
name: 'Agent',
}),
);
expect(forwardedProps.easyflow.draft.agent).not.toHaveProperty('created');
expect(forwardedProps.easyflow.draft.agent).not.toHaveProperty(
'publishedSnapshotJson',
);
expect(forwardedProps.easyflow.draft.toolBindings).toEqual([
{
enabled: true,
hitlConfigJson: { message: '请确认' },
hitlEnabled: true,
id: 'tool-binding-1',
optionsJson: { timeout: 30 },
sortNo: 1,
targetId: 'tool-1',
toolName: 'search',
toolType: 'PLUGIN',
},
]);
expect(forwardedProps.easyflow.draft.knowledgeBindings).toEqual([
{
enabled: true,
id: 'knowledge-binding-1',
knowledgeId: 'knowledge-1',
optionsJson: { topK: 5 },
retrievalMode: 'HYBRID',
sortNo: 2,
},
]);
expect(forwardedProps.easyflow.draft.skillBindings).toEqual([
{
skillId: 'skill-1',
sortNo: 3,
},
]);
stream.stop();
});
});