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,127 @@
import type {
AgentSubscriber,
Message,
RunAgentInput,
State,
} from '@ag-ui/client';
import { EventSchemas, EventType, HttpAgent, randomUUID } from '@ag-ui/client';
import { createEventStreamHeaders, resolveApiUrl } from '#/api/request';
export interface EasyFlowAguiRunOptions {
forwardedProps?: Record<string, unknown>;
onEvent: (event: AguiEvent) => void;
onMessagesChanged?: (messages: ReadonlyArray<Readonly<Message>>) => void;
onStateChanged?: (state: Readonly<State>) => void;
threadId: string;
url: string;
userMessage: Message;
}
export type AguiEvent = ReturnType<(typeof EventSchemas)['parse']>;
interface ActiveAguiRun {
aborted: boolean;
agent: HttpAgent;
}
function toTransportJson<T>(value: T): T {
// AG-UI SDK 会在请求前 structuredClone先以真实传输格式解除 Vue Proxy避免草稿对象克隆失败。
// eslint-disable-next-line unicorn/prefer-structured-clone -- structuredClone 无法复制 Proxy
return JSON.parse(JSON.stringify(value)) as T;
}
/**
* EasyFlow 的无头 AG-UI 运行客户端。
*
* <p>SDK 持有标准 messages/state页面只消费投影回调。每次运行都会在出站边界再次裁剪
* tools、context、state 和历史消息,服务端仍会独立执行同样的安全校验。</p>
*/
export class EasyFlowAguiClient {
private activeRun?: ActiveAguiRun;
abort() {
if (!this.activeRun) return;
this.activeRun.aborted = true;
this.activeRun.agent.abortRun();
this.activeRun = undefined;
}
async run(options: EasyFlowAguiRunOptions) {
this.abort();
const requestUrl = options.url;
const agent = new EasyFlowHttpAgent({
headers: createEventStreamHeaders(requestUrl),
initialMessages: [options.userMessage],
threadId: options.threadId,
url: resolveApiUrl(requestUrl),
});
const activeRun: ActiveAguiRun = { aborted: false, agent };
this.activeRun = activeRun;
let terminalReceived = false;
let cancelledReceived = false;
const subscriber: AgentSubscriber = {
onEvent: ({ event }) => {
if (
event.type === EventType.RUN_FINISHED ||
event.type === EventType.RUN_ERROR
) {
terminalReceived = true;
}
if (
event.type === EventType.RUN_ERROR &&
event.code === 'RUN_CANCELLED'
) {
cancelledReceived = true;
}
options.onEvent(event as AguiEvent);
},
onMessagesChanged: ({ messages }) => {
options.onMessagesChanged?.(messages);
},
onStateChanged: ({ state }) => {
options.onStateChanged?.(state);
},
};
try {
await agent.runAgent(
{
context: [],
forwardedProps: options.forwardedProps
? toTransportJson(options.forwardedProps)
: undefined,
runId: `run_${randomUUID()}`,
tools: [],
},
subscriber,
);
if (!terminalReceived) {
if (activeRun.aborted) return;
throw new Error('Agent 事件流缺少终态,请重试');
}
} catch (error) {
if (activeRun.aborted || cancelledReceived) return;
throw error;
} finally {
if (this.activeRun === activeRun) {
this.activeRun = undefined;
}
}
}
}
class EasyFlowHttpAgent extends HttpAgent {
protected override requestInit(input: RunAgentInput): RequestInit {
const latestUserMessage = [...input.messages]
.reverse()
.find((message) => message.role === 'user');
return super.requestInit({
...input,
context: [],
messages: latestUserMessage ? [latestUserMessage] : [],
state: {},
tools: [],
});
}
}