mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-30 20:50:34 +00:00
feat(hooks): Add HTTP Hook, Function Hook and Async Hook support (#2827)
* add http/async/function type * fix url error * resolve comment * align cc non blocking error * fix hookRunner for async * fix(hooks): update hook type validation to support http and function types - Change validated hook types from ['command', 'plugin'] to ['command', 'http', 'function'] - Add validation for HTTP hooks requiring url field - Add validation for function hooks requiring callback field - Add comprehensive test coverage for all hook type validations Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(hooks): align SSRF protection with Claude Code behavior - Allow 127.0.0.0/8 (loopback) for local dev hooks - Allow localhost hostname for local dev hooks - Allow ::1 (IPv6 loopback) for local dev hooks - Add 100.64.0.0/10 (CGNAT) to blocked ranges (RFC 6598) - Update tests to match Claude Code's ssrfGuard.ts behavior This fixes HTTP hooks failing to connect to local dev servers. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * refactor(hooks): align HTTP hook security with Claude Code behavior - Add CRLF/NUL sanitization for env var interpolation (header injection) - Implement combined abort signal (external signal + timeout) - Upgrade SSRF protection to DNS-level with ssrfGuard - Allow loopback (127.0.0.0/8, ::1) for local dev hooks - Block CGNAT (100.64.0.0/10) and IPv6 private ranges - Increase default HTTP hook timeout to 10 minutes - Fix VS Code hooks schema to support http type - Add url, headers, allowedEnvVars, async, once, statusMessage, shell fields - Note: "function" type is SDK-only (callback cannot be serialized to JSON) * feat(hooks): enhance Function Hook with messages, skillRoot, shell, and matcher support - Add MessagesProvider for automatic conversation history passing to function hooks - Add FunctionHookContext with messages, toolUseID, and signal - Add skillRoot support for skill-scoped session hooks - Add shell parameter support for command hooks (bash/powershell) - Add regex matcher support for hook pattern matching - Add statusMessage to CommandHookConfig - Change default function hook timeout from 60s to 5s - Add comprehensive unit tests for all new features Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * add session hook for skill * fix function hook parsing * refactor ui for http hook/async hook/function hook * update doc and add integration test * change telemetryn type and refactor SSRF * fix project level bug --------- Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
parent
70396d1276
commit
b5115e731e
63 changed files with 9301 additions and 469 deletions
|
|
@ -12,7 +12,10 @@ import type {
|
|||
} from './types.js';
|
||||
import { CommandKind } from './types.js';
|
||||
import { t } from '../../i18n/index.js';
|
||||
import type { HookRegistryEntry } from '@qwen-code/qwen-code-core';
|
||||
import type {
|
||||
HookRegistryEntry,
|
||||
SessionHookEntry,
|
||||
} from '@qwen-code/qwen-code-core';
|
||||
|
||||
/**
|
||||
* Format hook source for display
|
||||
|
|
@ -27,18 +30,13 @@ function formatHookSource(source: string): string {
|
|||
return t('System');
|
||||
case 'extensions':
|
||||
return t('Extension');
|
||||
case 'session':
|
||||
return t('Session (temporary)');
|
||||
default:
|
||||
return source;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format hook status for display
|
||||
*/
|
||||
function formatHookStatus(enabled: boolean): string {
|
||||
return enabled ? t('✓ Enabled') : t('✗ Disabled');
|
||||
}
|
||||
|
||||
const listCommand: SlashCommand = {
|
||||
name: 'list',
|
||||
get description() {
|
||||
|
|
@ -70,38 +68,105 @@ const listCommand: SlashCommand = {
|
|||
}
|
||||
|
||||
const registry = hookSystem.getRegistry();
|
||||
const allHooks = registry.getAllHooks();
|
||||
const configHooks = registry.getAllHooks();
|
||||
|
||||
if (allHooks.length === 0) {
|
||||
// Get session hooks
|
||||
const sessionId = config.getSessionId();
|
||||
const sessionHooksManager = hookSystem.getSessionHooksManager();
|
||||
const sessionHooks = sessionId
|
||||
? sessionHooksManager.getAllSessionHooks(sessionId)
|
||||
: [];
|
||||
|
||||
const totalHooks = configHooks.length + sessionHooks.length;
|
||||
|
||||
if (totalHooks === 0) {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: t(
|
||||
'No hooks configured. Add hooks in your settings.json file.',
|
||||
'No hooks configured. Add hooks in your settings.json file or invoke a skill with hooks.',
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
// Group hooks by event
|
||||
const hooksByEvent = new Map<string, HookRegistryEntry[]>();
|
||||
for (const hook of allHooks) {
|
||||
const hooksByEvent = new Map<
|
||||
string,
|
||||
Array<{ hook: HookRegistryEntry | SessionHookEntry; isSession: boolean }>
|
||||
>();
|
||||
|
||||
// Add config hooks
|
||||
for (const hook of configHooks) {
|
||||
const eventName = hook.eventName;
|
||||
if (!hooksByEvent.has(eventName)) {
|
||||
hooksByEvent.set(eventName, []);
|
||||
}
|
||||
hooksByEvent.get(eventName)!.push(hook);
|
||||
hooksByEvent.get(eventName)!.push({ hook, isSession: false });
|
||||
}
|
||||
|
||||
let output = `**Configured Hooks (${allHooks.length} total)**\n\n`;
|
||||
// Add session hooks
|
||||
for (const hook of sessionHooks) {
|
||||
const eventName = hook.eventName;
|
||||
if (!hooksByEvent.has(eventName)) {
|
||||
hooksByEvent.set(eventName, []);
|
||||
}
|
||||
hooksByEvent.get(eventName)!.push({ hook, isSession: true });
|
||||
}
|
||||
|
||||
let output = `**Configured Hooks (${totalHooks} total)**\n\n`;
|
||||
|
||||
for (const [eventName, hooks] of hooksByEvent) {
|
||||
output += `### ${eventName}\n`;
|
||||
for (const hook of hooks) {
|
||||
const name = hook.config.name || hook.config.command || 'unnamed';
|
||||
const source = formatHookSource(hook.source);
|
||||
const status = formatHookStatus(hook.enabled);
|
||||
const matcher = hook.matcher ? ` (matcher: ${hook.matcher})` : '';
|
||||
output += `- **${name}** [${source}] ${status}${matcher}\n`;
|
||||
for (const { hook, isSession } of hooks) {
|
||||
let name: string;
|
||||
let source: string;
|
||||
let matcher: string;
|
||||
let config: {
|
||||
type: string;
|
||||
command?: string;
|
||||
url?: string;
|
||||
name?: string;
|
||||
};
|
||||
|
||||
if (isSession) {
|
||||
// Session hook
|
||||
const sessionHook = hook as SessionHookEntry;
|
||||
config = sessionHook.config as {
|
||||
type: string;
|
||||
command?: string;
|
||||
url?: string;
|
||||
name?: string;
|
||||
};
|
||||
name =
|
||||
config.name ||
|
||||
(config.type === 'command' ? config.command : undefined) ||
|
||||
(config.type === 'http' ? config.url : undefined) ||
|
||||
'unnamed';
|
||||
source = formatHookSource('session');
|
||||
matcher = sessionHook.matcher
|
||||
? ` (matcher: ${sessionHook.matcher})`
|
||||
: '';
|
||||
} else {
|
||||
// Config hook
|
||||
const configHook = hook as HookRegistryEntry;
|
||||
config = configHook.config as {
|
||||
type: string;
|
||||
command?: string;
|
||||
url?: string;
|
||||
name?: string;
|
||||
};
|
||||
name =
|
||||
config.name ||
|
||||
(config.type === 'command' ? config.command : undefined) ||
|
||||
(config.type === 'http' ? config.url : undefined) ||
|
||||
'unnamed';
|
||||
source = formatHookSource(configHook.source);
|
||||
matcher = configHook.matcher
|
||||
? ` (matcher: ${configHook.matcher})`
|
||||
: '';
|
||||
}
|
||||
|
||||
output += `- **${name}** [${source}]${matcher}\n`;
|
||||
}
|
||||
output += '\n';
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue