Skip to content

Commit 5af7687

Browse files
committed
feat: Add HookOutput class hierarchy and documentation for hook system
Added comprehensive HookOutput class hierarchy with specialized implementations for different hook events (BeforeTool, BeforeModel, AfterModel, BeforeToolSelection). Included factory function for creating appropriate HookOutput instances and detailed documentation explaining each class's capabilities and methods. This enhances the hook system's flexibility and provides clear patterns for hook output handling and modification.
1 parent 614aeb3 commit 5af7687

3 files changed

Lines changed: 277 additions & 56 deletions

File tree

src/pages/HookSystem.tsx

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,81 @@ export class BeforeModelHookOutput extends DefaultHookOutput {
272272
}
273273
}`;
274274

275+
// 完整的 HookOutput 类层次结构
276+
const hookOutputHierarchyCode = `// packages/core/src/hooks/types.ts
277+
278+
// 基类:DefaultHookOutput
279+
export class DefaultHookOutput implements HookOutput {
280+
constructor(
281+
public readonly continue?: boolean,
282+
public readonly stopReason?: string,
283+
public readonly suppressOutput?: boolean,
284+
public readonly systemMessage?: string,
285+
public readonly decision?: HookDecision,
286+
public readonly reason?: string,
287+
public readonly hookSpecificOutput?: Record<string, unknown>,
288+
) {}
289+
290+
// 是否是阻止性决策(block/deny)
291+
isBlockingDecision(): boolean {
292+
return this.decision === 'block' || this.decision === 'deny';
293+
}
294+
295+
// 是否应该停止执行
296+
shouldStopExecution(): boolean {
297+
return this.continue === false || this.isBlockingDecision();
298+
}
299+
300+
// 获取有效的停止原因
301+
getEffectiveReason(): string | undefined {
302+
return this.reason ?? this.stopReason;
303+
}
304+
}
305+
306+
// AfterModel Hook 输出:可修改模型响应
307+
export class AfterModelHookOutput extends DefaultHookOutput {
308+
getModifiedResponse(): GenerateContentResponse | undefined {
309+
if (this.hookSpecificOutput?.['llm_response']) {
310+
return defaultHookTranslator.fromHookLLMResponse(
311+
this.hookSpecificOutput['llm_response'] as LLMResponse
312+
);
313+
}
314+
return undefined;
315+
}
316+
}
317+
318+
// BeforeToolSelection Hook 输出:可修改工具配置
319+
export class BeforeToolSelectionHookOutput extends DefaultHookOutput {
320+
applyToolConfigModifications(
321+
toolConfig: ToolConfig
322+
): ToolConfig {
323+
if (this.hookSpecificOutput?.['tool_config']) {
324+
const modifications = this.hookSpecificOutput['tool_config'] as ToolConfig;
325+
return { ...toolConfig, ...modifications };
326+
}
327+
return toolConfig;
328+
}
329+
}
330+
331+
// 工厂函数:根据事件类型创建对应的 HookOutput
332+
export function createHookOutput(
333+
eventName: HookEventName,
334+
rawOutput: HookOutput
335+
): DefaultHookOutput {
336+
switch (eventName) {
337+
case HookEventName.BeforeTool:
338+
return new BeforeToolHookOutput(...);
339+
case HookEventName.BeforeModel:
340+
return new BeforeModelHookOutput(...);
341+
case HookEventName.AfterModel:
342+
return new AfterModelHookOutput(...);
343+
case HookEventName.BeforeToolSelection:
344+
return new BeforeToolSelectionHookOutput(...);
345+
default:
346+
return new DefaultHookOutput(...);
347+
}
348+
}`;
349+
275350
return (
276351
<div className="space-y-8">
277352
<QuickSummary
@@ -542,6 +617,54 @@ export class BeforeModelHookOutput extends DefaultHookOutput {
542617
</div>
543618
</Layer>
544619

620+
{/* 6.5. HookOutput 类层次结构 */}
621+
<Layer title="HookOutput 类层次结构" icon="🏗️">
622+
<div className="space-y-4">
623+
<HighlightBox title="专用 HookOutput 类" variant="purple">
624+
<div className="text-sm space-y-2 text-gray-300">
625+
<p>不同事件类型有对应的专用 HookOutput 类,提供特定的修改能力:</p>
626+
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 mt-3">
627+
<div className="bg-black/30 p-2 rounded">
628+
<code className="text-cyan-300">BeforeToolHookOutput</code>
629+
<p className="text-xs text-gray-400 mt-1">getModifiedToolInput() - 修改工具输入</p>
630+
</div>
631+
<div className="bg-black/30 p-2 rounded">
632+
<code className="text-purple-300">BeforeModelHookOutput</code>
633+
<p className="text-xs text-gray-400 mt-1">getSyntheticResponse() - 绕过 LLM 调用</p>
634+
</div>
635+
<div className="bg-black/30 p-2 rounded">
636+
<code className="text-green-300">AfterModelHookOutput</code>
637+
<p className="text-xs text-gray-400 mt-1">getModifiedResponse() - 修改模型响应</p>
638+
</div>
639+
<div className="bg-black/30 p-2 rounded">
640+
<code className="text-amber-300">BeforeToolSelectionHookOutput</code>
641+
<p className="text-xs text-gray-400 mt-1">applyToolConfigModifications() - 修改工具配置</p>
642+
</div>
643+
</div>
644+
</div>
645+
</HighlightBox>
646+
647+
<CodeBlock code={hookOutputHierarchyCode} language="typescript" title="HookOutput 类层次结构与工厂函数" />
648+
649+
<HighlightBox title="DefaultHookOutput 基类方法" variant="blue">
650+
<div className="grid grid-cols-1 md:grid-cols-3 gap-3 text-sm">
651+
<div className="bg-black/30 p-3 rounded">
652+
<code className="text-cyan-300 font-semibold">isBlockingDecision()</code>
653+
<p className="text-gray-400 mt-1">判断是否为阻止性决策(block/deny)</p>
654+
</div>
655+
<div className="bg-black/30 p-3 rounded">
656+
<code className="text-cyan-300 font-semibold">shouldStopExecution()</code>
657+
<p className="text-gray-400 mt-1">判断是否应停止执行</p>
658+
</div>
659+
<div className="bg-black/30 p-3 rounded">
660+
<code className="text-cyan-300 font-semibold">getEffectiveReason()</code>
661+
<p className="text-gray-400 mt-1">获取有效的停止原因</p>
662+
</div>
663+
</div>
664+
</HighlightBox>
665+
</div>
666+
</Layer>
667+
545668
{/* 7. 与 Policy 集成 */}
546669
<Layer title="与 Policy Engine 集成" icon="🔗">
547670
<div className="space-y-4">

src/pages/MessageBus.tsx

Lines changed: 146 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -183,73 +183,120 @@ export class MessageBus extends EventEmitter {
183183
super();
184184
}
185185
186-
// 发布消息
186+
// 发布消息(带完整错误处理)
187187
async publish(message: Message): Promise<void> {
188-
if (!this.isValidMessage(message)) {
189-
throw new Error(\`Invalid message structure\`);
190-
}
191-
192-
if (message.type === MessageBusType.TOOL_CONFIRMATION_REQUEST) {
193-
// 工具确认请求:先经过 Policy 检查
194-
const { decision } = await this.policyEngine.check(
195-
message.toolCall,
196-
message.serverName,
197-
);
188+
try {
189+
if (!this.isValidMessage(message)) {
190+
throw new Error(\`Invalid message structure: \${safeJsonStringify(message)}\`);
191+
}
198192
199-
switch (decision) {
200-
case PolicyDecision.ALLOW:
201-
this.emitMessage({
202-
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
203-
correlationId: message.correlationId,
204-
confirmed: true,
205-
});
206-
break;
193+
if (message.type === MessageBusType.TOOL_CONFIRMATION_REQUEST) {
194+
// 工具确认请求:先经过 Policy 检查
195+
const { decision } = await this.policyEngine.check(
196+
message.toolCall,
197+
message.serverName,
198+
);
199+
200+
switch (decision) {
201+
case PolicyDecision.ALLOW:
202+
this.emitMessage({
203+
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
204+
correlationId: message.correlationId,
205+
confirmed: true,
206+
});
207+
break;
208+
209+
case PolicyDecision.DENY:
210+
this.emitMessage({
211+
type: MessageBusType.TOOL_POLICY_REJECTION,
212+
toolCall: message.toolCall,
213+
});
214+
this.emitMessage({
215+
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
216+
correlationId: message.correlationId,
217+
confirmed: false,
218+
});
219+
break;
220+
221+
case PolicyDecision.ASK_USER:
222+
// 传递给 UI 层处理
223+
this.emitMessage(message);
224+
break;
225+
226+
default:
227+
throw new Error(\`Unknown policy decision: \${decision}\`);
228+
}
229+
} else if (message.type === MessageBusType.HOOK_EXECUTION_REQUEST) {
230+
// Hook 执行请求:经过 Hook 策略检查
231+
const decision = await this.policyEngine.checkHook(message);
232+
233+
// 发送策略决策事件(用于可观测性)
234+
this.emitMessage({
235+
type: MessageBusType.HOOK_POLICY_DECISION,
236+
eventName: message.eventName,
237+
hookSource: getHookSource(message.input),
238+
decision: decision === PolicyDecision.ALLOW ? 'allow' : 'deny',
239+
reason: decision !== PolicyDecision.ALLOW
240+
? 'Hook execution denied by policy'
241+
: undefined,
242+
});
207243
208-
case PolicyDecision.DENY:
209-
this.emitMessage({
210-
type: MessageBusType.TOOL_POLICY_REJECTION,
211-
toolCall: message.toolCall,
212-
});
244+
if (decision === PolicyDecision.ALLOW) {
245+
this.emitMessage(message);
246+
} else {
247+
// Hook 不支持交互式确认,直接返回错误
213248
this.emitMessage({
214-
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
249+
type: MessageBusType.HOOK_EXECUTION_RESPONSE,
215250
correlationId: message.correlationId,
216-
confirmed: false,
251+
success: false,
252+
error: new Error('Hook execution denied by policy'),
217253
});
218-
break;
219-
220-
case PolicyDecision.ASK_USER:
221-
// 传递给 UI 层处理
222-
this.emitMessage(message);
223-
break;
224-
}
225-
} else if (message.type === MessageBusType.HOOK_EXECUTION_REQUEST) {
226-
// Hook 执行请求:经过 Hook 策略检查
227-
const decision = await this.policyEngine.checkHook(message);
228-
229-
this.emitMessage({
230-
type: MessageBusType.HOOK_POLICY_DECISION,
231-
eventName: message.eventName,
232-
hookSource: getHookSource(message.input),
233-
decision: decision === PolicyDecision.ALLOW ? 'allow' : 'deny',
234-
});
235-
236-
if (decision === PolicyDecision.ALLOW) {
237-
this.emitMessage(message);
254+
}
238255
} else {
239-
this.emitMessage({
240-
type: MessageBusType.HOOK_EXECUTION_RESPONSE,
241-
correlationId: message.correlationId,
242-
success: false,
243-
error: new Error('Hook execution denied by policy'),
244-
});
256+
// 其他消息类型直接转发
257+
this.emitMessage(message);
245258
}
246-
} else {
247-
// 其他消息类型直接转发
248-
this.emitMessage(message);
259+
} catch (error) {
260+
// 错误不抛出,而是通过 'error' 事件发送
261+
this.emit('error', error);
249262
}
250263
}
251264
}`;
252265

266+
// 错误处理机制代码
267+
const errorHandlingCode = `// 错误处理机制
268+
269+
// 1. 订阅错误事件
270+
messageBus.on('error', (error: Error) => {
271+
console.error('[MessageBus Error]', error.message);
272+
// 可以发送到日志系统或监控平台
273+
telemetry.recordError('message_bus', error);
274+
});
275+
276+
// 2. ToolExecutionFailure 接口
277+
export interface ToolExecutionFailure<E = Error> {
278+
type: MessageBusType.TOOL_EXECUTION_FAILURE;
279+
correlationId: string;
280+
error: E;
281+
}
282+
283+
// 3. HookExecutionResponse 可包含错误
284+
export interface HookExecutionResponse {
285+
type: MessageBusType.HOOK_EXECUTION_RESPONSE;
286+
correlationId: string;
287+
success: boolean;
288+
error?: Error; // 失败时包含错误信息
289+
}
290+
291+
// 4. 使用示例:处理工具执行失败
292+
messageBus.subscribe(
293+
MessageBusType.TOOL_EXECUTION_FAILURE,
294+
(message: ToolExecutionFailure) => {
295+
console.error(\`Tool execution failed: \${message.error.message}\`);
296+
// 可以触发重试逻辑或通知用户
297+
}
298+
);`;
299+
253300
const subscribePatternCode = `// 订阅消息
254301
subscribe<T extends Message>(
255302
type: T['type'],
@@ -556,7 +603,50 @@ if (response.confirmed) {
556603
</div>
557604
</Layer>
558605

559-
{/* 7. 策略更新 */}
606+
{/* 7. 错误处理机制 */}
607+
<Layer title="错误处理机制" icon="⚠️">
608+
<div className="space-y-4">
609+
<HighlightBox title="错误不抛出,通过事件传递" variant="red">
610+
<div className="text-sm space-y-2 text-gray-300">
611+
<p>
612+
MessageBus 的 <code className="bg-black/30 px-1 rounded">publish()</code> 方法将整个逻辑包裹在 try-catch 中,
613+
错误不会抛出导致程序崩溃,而是通过 <code className="bg-black/30 px-1 rounded">'error'</code> 事件发送。
614+
</p>
615+
<p className="text-amber-400">
616+
这保证了消息总线的稳定性,即使某个消息处理失败,其他消息仍可正常处理。
617+
</p>
618+
</div>
619+
</HighlightBox>
620+
621+
<CodeBlock code={errorHandlingCode} language="typescript" title="错误处理示例" />
622+
623+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
624+
<HighlightBox title="错误类型" variant="blue">
625+
<div className="text-sm space-y-2">
626+
<ul className="text-gray-400 space-y-1">
627+
<li><code className="text-red-300">Invalid message structure</code>: 消息格式错误</li>
628+
<li><code className="text-red-300">Unknown policy decision</code>: 未知策略决策</li>
629+
<li><code className="text-red-300">Request timed out</code>: 请求超时</li>
630+
<li><code className="text-red-300">Hook execution denied</code>: Hook 执行被拒绝</li>
631+
</ul>
632+
</div>
633+
</HighlightBox>
634+
635+
<HighlightBox title="错误观测性" variant="green">
636+
<div className="text-sm space-y-2">
637+
<ul className="text-gray-400 space-y-1">
638+
<li>• 订阅 <code className="text-cyan-300">'error'</code> 事件监控错误</li>
639+
<li>• 错误可发送到遥测系统</li>
640+
<li>• 支持自定义错误处理逻辑</li>
641+
<li>• 可结合日志系统记录</li>
642+
</ul>
643+
</div>
644+
</HighlightBox>
645+
</div>
646+
</div>
647+
</Layer>
648+
649+
{/* 8. 策略更新 */}
560650
<Layer title="动态策略更新" icon="🔄">
561651
<div className="space-y-4">
562652
<CodeBlock

src/pages/PolicyEngine.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,14 @@ export type SafetyCheckerConfig =
162162
// 内置 Checker 类型
163163
export enum InProcessCheckerType {
164164
ALLOWED_PATH = 'allowed-path', // 路径白名单检查
165+
}
166+
167+
// Hook Checker 规则(用于 Hook 执行的安全检查)
168+
export interface HookCheckerRule {
169+
eventName?: string; // Hook 事件名(BeforeTool, AfterModel 等)
170+
hookSource?: HookSource; // Hook 来源(project, user, system, extension)
171+
checker: string; // 检查器名称
172+
priority?: number; // 优先级(越高越先匹配)
165173
}`;
166174

167175
const policyEngineCode = `// packages/core/src/policy/policy-engine.ts

0 commit comments

Comments
 (0)