illusion-code 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- illusion/__init__.py +24 -0
- illusion/__main__.py +15 -0
- illusion/_frontend/dist/index.mjs +39208 -0
- illusion/_frontend/package.json +27 -0
- illusion/_frontend/src/App.tsx +624 -0
- illusion/_frontend/src/components/CommandPicker.tsx +98 -0
- illusion/_frontend/src/components/Composer.tsx +55 -0
- illusion/_frontend/src/components/ComposerController.tsx +128 -0
- illusion/_frontend/src/components/ConversationView.tsx +750 -0
- illusion/_frontend/src/components/Footer.tsx +25 -0
- illusion/_frontend/src/components/MarkdownContent.tsx +537 -0
- illusion/_frontend/src/components/MarkdownTable.tsx +245 -0
- illusion/_frontend/src/components/ModalHost.tsx +425 -0
- illusion/_frontend/src/components/MultilineTextInput.tsx +250 -0
- illusion/_frontend/src/components/PromptInput.tsx +64 -0
- illusion/_frontend/src/components/SelectModal.tsx +78 -0
- illusion/_frontend/src/components/SidePanel.tsx +175 -0
- illusion/_frontend/src/components/Spinner.tsx +77 -0
- illusion/_frontend/src/components/StatusBar.tsx +142 -0
- illusion/_frontend/src/components/SwarmPanel.tsx +141 -0
- illusion/_frontend/src/components/TodoPanel.tsx +126 -0
- illusion/_frontend/src/components/ToolCallDisplay.tsx +202 -0
- illusion/_frontend/src/components/TranscriptPane.tsx +79 -0
- illusion/_frontend/src/components/WelcomeBanner.tsx +37 -0
- illusion/_frontend/src/hooks/useBackendSession.ts +468 -0
- illusion/_frontend/src/hooks/useTerminalSize.ts +9 -0
- illusion/_frontend/src/i18n.ts +78 -0
- illusion/_frontend/src/index.tsx +42 -0
- illusion/_frontend/src/theme/ThemeContext.tsx +19 -0
- illusion/_frontend/src/theme/builtinThemes.ts +89 -0
- illusion/_frontend/src/types.ts +110 -0
- illusion/_frontend/src/utils/markdown.ts +33 -0
- illusion/_frontend/src/utils/thinking.ts +191 -0
- illusion/_frontend/tsconfig.json +13 -0
- illusion/_web_dist/assets/index-BseIw-ik.css +10 -0
- illusion/_web_dist/assets/index-C_0ZWMuW.js +82 -0
- illusion/_web_dist/index.html +16 -0
- illusion/api/__init__.py +36 -0
- illusion/api/client.py +568 -0
- illusion/api/codex_client.py +563 -0
- illusion/api/compat.py +138 -0
- illusion/api/effort.py +128 -0
- illusion/api/errors.py +57 -0
- illusion/api/openai_client.py +819 -0
- illusion/api/provider.py +148 -0
- illusion/api/registry.py +479 -0
- illusion/api/usage.py +45 -0
- illusion/auth/__init__.py +50 -0
- illusion/auth/copilot.py +419 -0
- illusion/auth/external.py +612 -0
- illusion/auth/flows.py +58 -0
- illusion/auth/manager.py +214 -0
- illusion/auth/storage.py +372 -0
- illusion/bridge/__init__.py +38 -0
- illusion/bridge/manager.py +190 -0
- illusion/bridge/session_runner.py +84 -0
- illusion/bridge/types.py +113 -0
- illusion/bridge/work_secret.py +131 -0
- illusion/cli.py +1228 -0
- illusion/commands/__init__.py +32 -0
- illusion/commands/registry.py +1934 -0
- illusion/config/__init__.py +39 -0
- illusion/config/i18n.py +522 -0
- illusion/config/paths.py +259 -0
- illusion/config/settings.py +564 -0
- illusion/coordinator/__init__.py +41 -0
- illusion/coordinator/agent_definitions.py +1093 -0
- illusion/coordinator/coordinator_mode.py +127 -0
- illusion/engine/__init__.py +95 -0
- illusion/engine/cost_tracker.py +55 -0
- illusion/engine/messages.py +369 -0
- illusion/engine/query.py +632 -0
- illusion/engine/query_engine.py +343 -0
- illusion/engine/stream_events.py +169 -0
- illusion/hooks/__init__.py +67 -0
- illusion/hooks/events.py +43 -0
- illusion/hooks/executor.py +397 -0
- illusion/hooks/hot_reload.py +74 -0
- illusion/hooks/loader.py +133 -0
- illusion/hooks/schemas.py +121 -0
- illusion/hooks/types.py +86 -0
- illusion/mcp/__init__.py +104 -0
- illusion/mcp/client.py +377 -0
- illusion/mcp/config.py +140 -0
- illusion/mcp/types.py +175 -0
- illusion/memory/__init__.py +36 -0
- illusion/memory/manager.py +94 -0
- illusion/memory/memdir.py +58 -0
- illusion/memory/paths.py +57 -0
- illusion/memory/scan.py +120 -0
- illusion/memory/search.py +83 -0
- illusion/memory/types.py +43 -0
- illusion/output_styles/__init__.py +15 -0
- illusion/output_styles/loader.py +64 -0
- illusion/permissions/__init__.py +39 -0
- illusion/permissions/checker.py +174 -0
- illusion/permissions/modes.py +38 -0
- illusion/platforms.py +148 -0
- illusion/plugins/__init__.py +71 -0
- illusion/plugins/bundled/__init__.py +0 -0
- illusion/plugins/installer.py +59 -0
- illusion/plugins/loader.py +301 -0
- illusion/plugins/schemas.py +51 -0
- illusion/plugins/types.py +56 -0
- illusion/prompts/__init__.py +29 -0
- illusion/prompts/claudemd.py +74 -0
- illusion/prompts/context.py +187 -0
- illusion/prompts/environment.py +189 -0
- illusion/prompts/system_prompt.py +155 -0
- illusion/py.typed +0 -0
- illusion/sandbox/__init__.py +29 -0
- illusion/sandbox/adapter.py +174 -0
- illusion/services/__init__.py +59 -0
- illusion/services/compact/__init__.py +1015 -0
- illusion/services/cron.py +338 -0
- illusion/services/cron_scheduler.py +715 -0
- illusion/services/file_history.py +258 -0
- illusion/services/lsp/__init__.py +455 -0
- illusion/services/session_storage.py +237 -0
- illusion/services/token_estimation.py +72 -0
- illusion/skills/__init__.py +60 -0
- illusion/skills/bundled/__init__.py +110 -0
- illusion/skills/bundled/content/batch.md +86 -0
- illusion/skills/bundled/content/coding-guidelines.md +70 -0
- illusion/skills/bundled/content/debug.md +38 -0
- illusion/skills/bundled/content/loop.md +82 -0
- illusion/skills/bundled/content/remember.md +105 -0
- illusion/skills/bundled/content/simplify.md +53 -0
- illusion/skills/bundled/content/skillify.md +113 -0
- illusion/skills/bundled/content/stuck.md +54 -0
- illusion/skills/bundled/content/update-config.md +329 -0
- illusion/skills/bundled/content/verify.md +74 -0
- illusion/skills/loader.py +219 -0
- illusion/skills/registry.py +40 -0
- illusion/skills/types.py +24 -0
- illusion/state/__init__.py +18 -0
- illusion/state/app_state.py +67 -0
- illusion/state/store.py +93 -0
- illusion/swarm/__init__.py +71 -0
- illusion/swarm/agent_executor.py +857 -0
- illusion/swarm/in_process.py +259 -0
- illusion/swarm/subprocess_backend.py +136 -0
- illusion/swarm/team_helpers.py +123 -0
- illusion/swarm/types.py +159 -0
- illusion/swarm/worktree.py +347 -0
- illusion/tasks/__init__.py +33 -0
- illusion/tasks/local_agent_task.py +42 -0
- illusion/tasks/local_shell_task.py +27 -0
- illusion/tasks/manager.py +377 -0
- illusion/tasks/stop_task.py +21 -0
- illusion/tasks/types.py +88 -0
- illusion/tools/__init__.py +126 -0
- illusion/tools/agent_tool.py +388 -0
- illusion/tools/ask_user_question_tool.py +186 -0
- illusion/tools/base.py +149 -0
- illusion/tools/bash_tool.py +413 -0
- illusion/tools/config_tool.py +90 -0
- illusion/tools/cron_tool.py +473 -0
- illusion/tools/enter_plan_mode_tool.py +147 -0
- illusion/tools/enter_worktree_tool.py +188 -0
- illusion/tools/exit_plan_mode_tool.py +69 -0
- illusion/tools/exit_worktree_tool.py +225 -0
- illusion/tools/file_edit_tool.py +283 -0
- illusion/tools/file_read_tool.py +294 -0
- illusion/tools/file_write_tool.py +184 -0
- illusion/tools/glob_tool.py +165 -0
- illusion/tools/grep_tool.py +190 -0
- illusion/tools/list_mcp_resources_tool.py +80 -0
- illusion/tools/lsp_tool.py +333 -0
- illusion/tools/mcp_auth_tool.py +100 -0
- illusion/tools/mcp_tool.py +75 -0
- illusion/tools/notebook_edit_tool.py +242 -0
- illusion/tools/powershell_tool.py +334 -0
- illusion/tools/read_mcp_resource_tool.py +63 -0
- illusion/tools/repl_tool.py +100 -0
- illusion/tools/send_message_tool.py +112 -0
- illusion/tools/shell_common.py +187 -0
- illusion/tools/skill_tool.py +86 -0
- illusion/tools/sleep_tool.py +62 -0
- illusion/tools/structured_output_tool.py +58 -0
- illusion/tools/task_create_tool.py +98 -0
- illusion/tools/task_get_tool.py +94 -0
- illusion/tools/task_list_tool.py +94 -0
- illusion/tools/task_output_tool.py +55 -0
- illusion/tools/task_stop_tool.py +52 -0
- illusion/tools/task_update_tool.py +224 -0
- illusion/tools/team_create_tool.py +236 -0
- illusion/tools/team_delete_tool.py +104 -0
- illusion/tools/todo_write_tool.py +198 -0
- illusion/tools/tool_search_tool.py +156 -0
- illusion/tools/web_fetch_tool.py +264 -0
- illusion/tools/web_search_tool.py +186 -0
- illusion/ui/__init__.py +23 -0
- illusion/ui/app.py +258 -0
- illusion/ui/backend_host.py +1180 -0
- illusion/ui/input.py +86 -0
- illusion/ui/output.py +363 -0
- illusion/ui/permission_dialog.py +47 -0
- illusion/ui/permission_store.py +99 -0
- illusion/ui/protocol.py +384 -0
- illusion/ui/react_launcher.py +280 -0
- illusion/ui/runtime.py +787 -0
- illusion/ui/textual_app.py +603 -0
- illusion/ui/web/__init__.py +10 -0
- illusion/ui/web/server.py +87 -0
- illusion/ui/web/ws_host.py +1197 -0
- illusion/utils/__init__.py +0 -0
- illusion/utils/ripgrep.py +299 -0
- illusion/utils/shell.py +248 -0
- illusion_code-0.1.0.dist-info/METADATA +1159 -0
- illusion_code-0.1.0.dist-info/RECORD +214 -0
- illusion_code-0.1.0.dist-info/WHEEL +4 -0
- illusion_code-0.1.0.dist-info/entry_points.txt +2 -0
- illusion_code-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
"""
|
|
2
|
+
钩子执行引擎
|
|
3
|
+
============
|
|
4
|
+
|
|
5
|
+
本模块实现钩子的核心执行逻辑,支持多种钩子类型的异步执行。
|
|
6
|
+
|
|
7
|
+
支持的钩子类型:
|
|
8
|
+
- CommandHookDefinition: 执行 Shell 命令
|
|
9
|
+
- HttpHookDefinition: 发送 HTTP 请求
|
|
10
|
+
- PromptHookDefinition: 使用模型验证
|
|
11
|
+
- AgentHookDefinition: 使用 Agent 深度验证
|
|
12
|
+
|
|
13
|
+
主要组件:
|
|
14
|
+
- HookExecutionContext: 钩子执行上下文
|
|
15
|
+
- HookExecutor: 钩子执行器
|
|
16
|
+
|
|
17
|
+
使用示例:
|
|
18
|
+
>>> from illusion.hooks.executor import HookExecutor, HookExecutionContext
|
|
19
|
+
>>> executor = HookExecutor(registry, context)
|
|
20
|
+
>>> result = await executor.execute(event, payload)
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import asyncio
|
|
26
|
+
import fnmatch
|
|
27
|
+
import json
|
|
28
|
+
import os
|
|
29
|
+
import shlex
|
|
30
|
+
from dataclasses import dataclass
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
from typing import Any
|
|
33
|
+
|
|
34
|
+
import httpx
|
|
35
|
+
|
|
36
|
+
from illusion.api.client import ApiMessageCompleteEvent, ApiMessageRequest, SupportsStreamingMessages
|
|
37
|
+
from illusion.engine.messages import ConversationMessage
|
|
38
|
+
from illusion.hooks.events import HookEvent
|
|
39
|
+
from illusion.hooks.loader import HookRegistry
|
|
40
|
+
from illusion.hooks.schemas import (
|
|
41
|
+
AgentHookDefinition,
|
|
42
|
+
CommandHookDefinition,
|
|
43
|
+
HookDefinition,
|
|
44
|
+
HttpHookDefinition,
|
|
45
|
+
PromptHookDefinition,
|
|
46
|
+
)
|
|
47
|
+
from illusion.hooks.types import AggregatedHookResult, HookResult
|
|
48
|
+
from illusion.sandbox import SandboxUnavailableError
|
|
49
|
+
from illusion.utils.shell import create_shell_subprocess
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class HookExecutionContext:
|
|
54
|
+
"""
|
|
55
|
+
钩子执行上下文
|
|
56
|
+
|
|
57
|
+
存储钩子执行所需的环境信息。
|
|
58
|
+
|
|
59
|
+
Attributes:
|
|
60
|
+
cwd: 当前工作目录
|
|
61
|
+
api_client: API 客户端实例
|
|
62
|
+
default_model: 默认模型名称
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
cwd: Path # 当前工作目录
|
|
66
|
+
api_client: SupportsStreamingMessages # API 客户端
|
|
67
|
+
default_model: str # 默认模型名称
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class HookExecutor:
|
|
71
|
+
"""
|
|
72
|
+
钩子执行器
|
|
73
|
+
|
|
74
|
+
管理钩子注册表和执行上下文,提供异步钩子执行能力。
|
|
75
|
+
|
|
76
|
+
Attributes:
|
|
77
|
+
_registry: 钩子注册表
|
|
78
|
+
_context: 执行上下文
|
|
79
|
+
|
|
80
|
+
使用示例:
|
|
81
|
+
>>> executor = HookExecutor(registry, context)
|
|
82
|
+
>>> result = await executor.execute(HookEvent.PRE_TOOL_USE, payload)
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
def __init__(self, registry: HookRegistry, context: HookExecutionContext) -> None:
|
|
86
|
+
self._registry = registry # 钩子注册表
|
|
87
|
+
self._context = context # 执行上下文
|
|
88
|
+
|
|
89
|
+
def update_registry(self, registry: HookRegistry) -> None:
|
|
90
|
+
"""
|
|
91
|
+
替换活动的钩子注册表
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
registry: 新的钩子注册表
|
|
95
|
+
"""
|
|
96
|
+
self._registry = registry
|
|
97
|
+
|
|
98
|
+
def update_context(
|
|
99
|
+
self,
|
|
100
|
+
*,
|
|
101
|
+
api_client: SupportsStreamingMessages | None = None,
|
|
102
|
+
default_model: str | None = None,
|
|
103
|
+
) -> None:
|
|
104
|
+
"""
|
|
105
|
+
更新活动的钩子执行上下文
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
api_client: 新的 API 客户端(可选)
|
|
109
|
+
default_model: 新的默认模型(可选)
|
|
110
|
+
"""
|
|
111
|
+
if api_client is not None:
|
|
112
|
+
self._context.api_client = api_client
|
|
113
|
+
if default_model is not None:
|
|
114
|
+
self._context.default_model = default_model
|
|
115
|
+
|
|
116
|
+
async def execute(self, event: HookEvent, payload: dict[str, Any]) -> AggregatedHookResult:
|
|
117
|
+
"""
|
|
118
|
+
执行事件对应的所有匹配钩子
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
event: 钩子事件类型
|
|
122
|
+
payload: 事件载荷数据
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
AggregatedHookResult: 聚合的钩子执行结果
|
|
126
|
+
"""
|
|
127
|
+
results: list[HookResult] = [] # 存储执行结果
|
|
128
|
+
# 遍历注册表中该事件的所有钩子
|
|
129
|
+
for hook in self._registry.get(event):
|
|
130
|
+
# 检查钩子是否与 payload 匹配
|
|
131
|
+
if not _matches_hook(hook, payload):
|
|
132
|
+
continue
|
|
133
|
+
# 根据钩子类型执行相应的处理方法
|
|
134
|
+
if isinstance(hook, CommandHookDefinition):
|
|
135
|
+
results.append(await self._run_command_hook(hook, event, payload))
|
|
136
|
+
elif isinstance(hook, HttpHookDefinition):
|
|
137
|
+
results.append(await self._run_http_hook(hook, event, payload))
|
|
138
|
+
elif isinstance(hook, PromptHookDefinition):
|
|
139
|
+
results.append(await self._run_prompt_like_hook(hook, event, payload, agent_mode=False))
|
|
140
|
+
elif isinstance(hook, AgentHookDefinition):
|
|
141
|
+
results.append(await self._run_prompt_like_hook(hook, event, payload, agent_mode=True))
|
|
142
|
+
return AggregatedHookResult(results=results)
|
|
143
|
+
|
|
144
|
+
async def _run_command_hook(
|
|
145
|
+
self,
|
|
146
|
+
hook: CommandHookDefinition,
|
|
147
|
+
event: HookEvent,
|
|
148
|
+
payload: dict[str, Any],
|
|
149
|
+
) -> HookResult:
|
|
150
|
+
"""
|
|
151
|
+
执行命令钩子
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
hook: 命令钩子定义
|
|
155
|
+
event: 钩子事件
|
|
156
|
+
payload: 事件载荷
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
HookResult: 钩子执行结果
|
|
160
|
+
"""
|
|
161
|
+
# 注入参数到命令中
|
|
162
|
+
command = _inject_arguments(hook.command, payload, shell_escape=True)
|
|
163
|
+
try:
|
|
164
|
+
# 创建子进程执行命令
|
|
165
|
+
process = await create_shell_subprocess(
|
|
166
|
+
command,
|
|
167
|
+
cwd=self._context.cwd,
|
|
168
|
+
stdout=asyncio.subprocess.PIPE,
|
|
169
|
+
stderr=asyncio.subprocess.PIPE,
|
|
170
|
+
env={
|
|
171
|
+
**os.environ,
|
|
172
|
+
"illusion_HOOK_EVENT": event.value, # 设置事件环境变量
|
|
173
|
+
"illusion_HOOK_PAYLOAD": json.dumps(payload), # 设置载荷环境变量
|
|
174
|
+
},
|
|
175
|
+
)
|
|
176
|
+
except SandboxUnavailableError as exc:
|
|
177
|
+
# 沙箱不可用时返回失败结果
|
|
178
|
+
return HookResult(
|
|
179
|
+
hook_type=hook.type,
|
|
180
|
+
success=False,
|
|
181
|
+
blocked=hook.block_on_failure,
|
|
182
|
+
reason=str(exc),
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
# 等待命令完成,设置超时
|
|
187
|
+
stdout, stderr = await asyncio.wait_for(
|
|
188
|
+
process.communicate(),
|
|
189
|
+
timeout=hook.timeout_seconds,
|
|
190
|
+
)
|
|
191
|
+
except asyncio.TimeoutError:
|
|
192
|
+
# 超时杀死进程
|
|
193
|
+
process.kill()
|
|
194
|
+
await process.wait()
|
|
195
|
+
return HookResult(
|
|
196
|
+
hook_type=hook.type,
|
|
197
|
+
success=False,
|
|
198
|
+
blocked=hook.block_on_failure,
|
|
199
|
+
reason=f"command hook timed out after {hook.timeout_seconds}s",
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
# 合并 stdout 和 stderr
|
|
203
|
+
output = "\n".join(
|
|
204
|
+
part for part in (
|
|
205
|
+
stdout.decode("utf-8", errors="replace").strip(),
|
|
206
|
+
stderr.decode("utf-8", errors="replace").strip(),
|
|
207
|
+
) if part
|
|
208
|
+
)
|
|
209
|
+
success = process.returncode == 0 # 检查退出码
|
|
210
|
+
return HookResult(
|
|
211
|
+
hook_type=hook.type,
|
|
212
|
+
success=success,
|
|
213
|
+
output=output,
|
|
214
|
+
blocked=hook.block_on_failure and not success,
|
|
215
|
+
reason=output or f"command hook failed with exit code {process.returncode}",
|
|
216
|
+
metadata={"returncode": process.returncode},
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
async def _run_http_hook(
|
|
220
|
+
self,
|
|
221
|
+
hook: HttpHookDefinition,
|
|
222
|
+
event: HookEvent,
|
|
223
|
+
payload: dict[str, Any],
|
|
224
|
+
) -> HookResult:
|
|
225
|
+
"""
|
|
226
|
+
执行 HTTP 钩子
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
hook: HTTP 钩子定义
|
|
230
|
+
event: 钩子事件
|
|
231
|
+
payload: 事件载荷
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
HookResult: 钩子执行结果
|
|
235
|
+
"""
|
|
236
|
+
try:
|
|
237
|
+
# 创建异步 HTTP 客户端并发送请求
|
|
238
|
+
async with httpx.AsyncClient(timeout=hook.timeout_seconds) as client:
|
|
239
|
+
response = await client.post(
|
|
240
|
+
hook.url,
|
|
241
|
+
json={"event": event.value, "payload": payload},
|
|
242
|
+
headers=hook.headers,
|
|
243
|
+
)
|
|
244
|
+
success = response.is_success # 检查响应状态
|
|
245
|
+
output = response.text # 响应内容
|
|
246
|
+
return HookResult(
|
|
247
|
+
hook_type=hook.type,
|
|
248
|
+
success=success,
|
|
249
|
+
output=output,
|
|
250
|
+
blocked=hook.block_on_failure and not success,
|
|
251
|
+
reason=output or f"http hook returned {response.status_code}",
|
|
252
|
+
metadata={"status_code": response.status_code},
|
|
253
|
+
)
|
|
254
|
+
except Exception as exc:
|
|
255
|
+
# 异常时返回失败结果
|
|
256
|
+
return HookResult(
|
|
257
|
+
hook_type=hook.type,
|
|
258
|
+
success=False,
|
|
259
|
+
blocked=hook.block_on_failure,
|
|
260
|
+
reason=str(exc),
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
async def _run_prompt_like_hook(
|
|
264
|
+
self,
|
|
265
|
+
hook: PromptHookDefinition | AgentHookDefinition,
|
|
266
|
+
event: HookEvent,
|
|
267
|
+
payload: dict[str, Any],
|
|
268
|
+
*,
|
|
269
|
+
agent_mode: bool,
|
|
270
|
+
) -> HookResult:
|
|
271
|
+
"""
|
|
272
|
+
执行提示词或 Agent 钩子
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
hook: 提示词或 Agent 钩子定义
|
|
276
|
+
event: 钩子事件
|
|
277
|
+
payload: 事件载荷
|
|
278
|
+
agent_mode: 是否使用 Agent 模式
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
HookResult: 钩子执行结果
|
|
282
|
+
"""
|
|
283
|
+
# 注入参数到提示词中
|
|
284
|
+
prompt = _inject_arguments(hook.prompt, payload)
|
|
285
|
+
# 构建系统提示词前缀
|
|
286
|
+
prefix = (
|
|
287
|
+
"You are validating whether a hook condition passes in illusion. "
|
|
288
|
+
"Return strict JSON: {\"ok\": true} or {\"ok\": false, \"reason\": \"...\"}."
|
|
289
|
+
)
|
|
290
|
+
if agent_mode:
|
|
291
|
+
# Agent 模式需要更详细的推理
|
|
292
|
+
prefix += " Be more thorough and reason over the payload before deciding."
|
|
293
|
+
|
|
294
|
+
# 构建 API 请求
|
|
295
|
+
request = ApiMessageRequest(
|
|
296
|
+
model=hook.model or self._context.default_model, # 使用指定模型或默认模型
|
|
297
|
+
messages=[ConversationMessage.from_user_text(prompt)],
|
|
298
|
+
system_prompt=prefix,
|
|
299
|
+
max_tokens=512,
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
text_chunks: list[str] = [] # 存储文本块
|
|
303
|
+
final_event: ApiMessageCompleteEvent | None = None # 最终事件
|
|
304
|
+
# 流式获取响应
|
|
305
|
+
async for event_item in self._context.api_client.stream_message(request):
|
|
306
|
+
if isinstance(event_item, ApiMessageCompleteEvent):
|
|
307
|
+
final_event = event_item
|
|
308
|
+
else:
|
|
309
|
+
text_chunks.append(event_item.text)
|
|
310
|
+
|
|
311
|
+
# 合并文本块
|
|
312
|
+
text = "".join(text_chunks)
|
|
313
|
+
# 如果有最终消息,使用最终消息的文本
|
|
314
|
+
if final_event is not None and final_event.message.text:
|
|
315
|
+
text = final_event.message.text
|
|
316
|
+
|
|
317
|
+
# 解析钩子返回的 JSON
|
|
318
|
+
parsed = _parse_hook_json(text)
|
|
319
|
+
if parsed["ok"]:
|
|
320
|
+
return HookResult(hook_type=hook.type, success=True, output=text)
|
|
321
|
+
return HookResult(
|
|
322
|
+
hook_type=hook.type,
|
|
323
|
+
success=False,
|
|
324
|
+
output=text,
|
|
325
|
+
blocked=hook.block_on_failure,
|
|
326
|
+
reason=parsed.get("reason", "hook rejected the event"),
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _matches_hook(hook: HookDefinition, payload: dict[str, Any]) -> bool:
|
|
331
|
+
"""
|
|
332
|
+
检查钩子是否与 payload 匹配
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
hook: 钩子定义
|
|
336
|
+
payload: 事件载荷
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
bool: 是否匹配
|
|
340
|
+
"""
|
|
341
|
+
# 获取匹配器
|
|
342
|
+
matcher = getattr(hook, "matcher", None)
|
|
343
|
+
if not matcher:
|
|
344
|
+
return True # 没有匹配器则匹配所有
|
|
345
|
+
# 从 payload 中提取匹配主题
|
|
346
|
+
subject = str(payload.get("tool_name") or payload.get("prompt") or payload.get("event") or "")
|
|
347
|
+
# 使用 fnmatch 进行模式匹配
|
|
348
|
+
return fnmatch.fnmatch(subject, matcher)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _inject_arguments(
|
|
352
|
+
template: str, payload: dict[str, Any], *, shell_escape: bool = False
|
|
353
|
+
) -> str:
|
|
354
|
+
"""
|
|
355
|
+
将 payload 注入到模板字符串中
|
|
356
|
+
|
|
357
|
+
Args:
|
|
358
|
+
template: 包含 $ARGUMENTS 占位符的模板
|
|
359
|
+
payload: 要注入的数据
|
|
360
|
+
shell_escape: 是否对 payload 进行 Shell 转义
|
|
361
|
+
|
|
362
|
+
Returns:
|
|
363
|
+
str: 注入后的字符串
|
|
364
|
+
"""
|
|
365
|
+
# 序列化 payload 为 JSON 字符串
|
|
366
|
+
serialized = json.dumps(payload, ensure_ascii=True)
|
|
367
|
+
if shell_escape:
|
|
368
|
+
# 对 Shell 命令进行转义
|
|
369
|
+
serialized = shlex.quote(serialized)
|
|
370
|
+
# 替换模板中的占位符
|
|
371
|
+
return template.replace("$ARGUMENTS", serialized)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _parse_hook_json(text: str) -> dict[str, Any]:
|
|
375
|
+
"""
|
|
376
|
+
解析钩子返回的 JSON 响应
|
|
377
|
+
|
|
378
|
+
Args:
|
|
379
|
+
text: 钩子返回的文本
|
|
380
|
+
|
|
381
|
+
Returns:
|
|
382
|
+
dict: 解析后的结果字典
|
|
383
|
+
"""
|
|
384
|
+
try:
|
|
385
|
+
# 尝试解析 JSON
|
|
386
|
+
parsed = json.loads(text)
|
|
387
|
+
# 验证格式
|
|
388
|
+
if isinstance(parsed, dict) and isinstance(parsed.get("ok"), bool):
|
|
389
|
+
return parsed
|
|
390
|
+
except json.JSONDecodeError:
|
|
391
|
+
pass
|
|
392
|
+
# 尝试简单文本匹配
|
|
393
|
+
lowered = text.strip().lower()
|
|
394
|
+
if lowered in {"ok", "true", "yes"}:
|
|
395
|
+
return {"ok": True}
|
|
396
|
+
# 返回失败结果
|
|
397
|
+
return {"ok": False, "reason": text.strip() or "hook returned invalid JSON"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""
|
|
2
|
+
钩子热重载模块
|
|
3
|
+
==============
|
|
4
|
+
|
|
5
|
+
本模块提供基于设置文件的热重载功能,当设置文件发生变化时自动重新加载钩子。
|
|
6
|
+
|
|
7
|
+
主要组件:
|
|
8
|
+
- HookReloader: 钩子定义热重载器
|
|
9
|
+
|
|
10
|
+
使用示例:
|
|
11
|
+
>>> from pathlib import Path
|
|
12
|
+
>>> from illusion.hooks.hot_reload import HookReloader
|
|
13
|
+
>>> reloader = HookReloader(Path("settings.yaml"))
|
|
14
|
+
>>> registry = reloader.current_registry()
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
from illusion.config import load_settings
|
|
22
|
+
from illusion.hooks.loader import HookRegistry, load_hook_registry
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class HookReloader:
|
|
26
|
+
"""
|
|
27
|
+
钩子热重载器
|
|
28
|
+
|
|
29
|
+
监控设置文件变化,在文件修改时自动重新加载钩子注册表。
|
|
30
|
+
|
|
31
|
+
Attributes:
|
|
32
|
+
_settings_path: 设置文件路径
|
|
33
|
+
_last_mtime_ns: 上次文件修改时间(纳秒)
|
|
34
|
+
_registry: 当前钩子注册表
|
|
35
|
+
|
|
36
|
+
使用示例:
|
|
37
|
+
>>> reloader = HookReloader(Path("settings.yaml"))
|
|
38
|
+
>>> registry = reloader.current_registry()
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, settings_path: Path) -> None:
|
|
42
|
+
self._settings_path = settings_path # 设置文件路径
|
|
43
|
+
self._last_mtime_ns = -1 # 初始为无效时间戳
|
|
44
|
+
self._registry = HookRegistry() # 初始空注册表
|
|
45
|
+
|
|
46
|
+
def current_registry(self) -> HookRegistry:
|
|
47
|
+
"""
|
|
48
|
+
获取当前注册表,必要时重新加载
|
|
49
|
+
|
|
50
|
+
检查设置文件的修改时间,如果发生变化则重新加载钩子。
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
HookRegistry: 当前的钩子注册表
|
|
54
|
+
|
|
55
|
+
注意:
|
|
56
|
+
- 如果文件不存在,返回空注册表
|
|
57
|
+
- 只有当文件修改时间变化时才重新加载
|
|
58
|
+
"""
|
|
59
|
+
try:
|
|
60
|
+
# 获取文件状态信息
|
|
61
|
+
stat = self._settings_path.stat()
|
|
62
|
+
except FileNotFoundError:
|
|
63
|
+
# 文件不存在时,重置注册表
|
|
64
|
+
self._registry = HookRegistry()
|
|
65
|
+
self._last_mtime_ns = -1
|
|
66
|
+
return self._registry
|
|
67
|
+
|
|
68
|
+
# 检查文件是否被修改
|
|
69
|
+
if stat.st_mtime_ns != self._last_mtime_ns:
|
|
70
|
+
self._last_mtime_ns = stat.st_mtime_ns # 更新修改时间
|
|
71
|
+
# 重新加载设置并构建注册表
|
|
72
|
+
self._registry = load_hook_registry(load_settings(self._settings_path))
|
|
73
|
+
|
|
74
|
+
return self._registry # 返回当前注册表
|
illusion/hooks/loader.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""
|
|
2
|
+
钩子加载器模块
|
|
3
|
+
==============
|
|
4
|
+
|
|
5
|
+
本模块提供从设置和插件加载钩子注册表的功能。
|
|
6
|
+
|
|
7
|
+
主要功能:
|
|
8
|
+
- HookRegistry: 按事件分组存储钩子
|
|
9
|
+
- load_hook_registry: 从设置对象加载钩子注册表
|
|
10
|
+
|
|
11
|
+
使用示例:
|
|
12
|
+
>>> from illusion.hooks.loader import HookRegistry, load_hook_registry
|
|
13
|
+
>>> registry = load_hook_registry(settings, plugins)
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from collections import defaultdict
|
|
19
|
+
from illusion.hooks.events import HookEvent
|
|
20
|
+
from illusion.hooks.schemas import HookDefinition
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class HookRegistry:
|
|
24
|
+
"""
|
|
25
|
+
钩子注册表
|
|
26
|
+
|
|
27
|
+
按事件类型分组存储钩子定义,支持注册、获取和摘要生成。
|
|
28
|
+
|
|
29
|
+
Attributes:
|
|
30
|
+
_hooks: 事件到钩子列表的映射字典
|
|
31
|
+
|
|
32
|
+
使用示例:
|
|
33
|
+
>>> registry = HookRegistry()
|
|
34
|
+
>>> registry.register(HookEvent.PRE_TOOL_USE, hook_def)
|
|
35
|
+
>>> hooks = registry.get(HookEvent.PRE_TOOL_USE)
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(self) -> None:
|
|
39
|
+
# 初始化钩子字典,使用 defaultdict 自动创建空列表
|
|
40
|
+
self._hooks: dict[HookEvent, list[HookDefinition]] = defaultdict(list)
|
|
41
|
+
|
|
42
|
+
def register(self, event: HookEvent, hook: HookDefinition) -> None:
|
|
43
|
+
"""
|
|
44
|
+
注册一个钩子
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
event: 钩子触发事件
|
|
48
|
+
hook: 钩子定义
|
|
49
|
+
"""
|
|
50
|
+
# 将钩子添加到对应事件的列表中
|
|
51
|
+
self._hooks[event].append(hook)
|
|
52
|
+
|
|
53
|
+
def get(self, event: HookEvent) -> list[HookDefinition]:
|
|
54
|
+
"""
|
|
55
|
+
获取指定事件的所有钩子
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
event: 钩子触发事件
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
list[HookDefinition]: 钩子定义列表
|
|
62
|
+
"""
|
|
63
|
+
# 返回事件对应的钩子列表的副本
|
|
64
|
+
return list(self._hooks.get(event, []))
|
|
65
|
+
|
|
66
|
+
def summary(self) -> str:
|
|
67
|
+
"""
|
|
68
|
+
生成人类可读的钩子摘要
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
str: 格式化的钩子摘要字符串
|
|
72
|
+
"""
|
|
73
|
+
lines: list[str] = [] # 存储摘要行
|
|
74
|
+
# 遍历所有事件类型
|
|
75
|
+
for event in HookEvent:
|
|
76
|
+
hooks = self.get(event) # 获取事件对应的钩子
|
|
77
|
+
if not hooks:
|
|
78
|
+
continue # 跳过没有钩子的事件
|
|
79
|
+
lines.append(f"{event.value}:") # 添加事件名称
|
|
80
|
+
# 遍历每个钩子
|
|
81
|
+
for hook in hooks:
|
|
82
|
+
# 获取匹配器属性
|
|
83
|
+
matcher = getattr(hook, "matcher", None)
|
|
84
|
+
# 获取详情属性(command/prompt/url 之一)
|
|
85
|
+
detail = getattr(hook, "command", None) or getattr(hook, "prompt", None) or getattr(hook, "url", None) or ""
|
|
86
|
+
suffix = f" matcher={matcher}" if matcher else "" # 匹配器后缀
|
|
87
|
+
lines.append(f" - {hook.type}{suffix}: {detail}") # 添加钩子详情
|
|
88
|
+
return "\n".join(lines) # 拼接所有行
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def load_hook_registry(settings, plugins=None) -> HookRegistry:
|
|
92
|
+
"""
|
|
93
|
+
从设置对象加载钩子注册表
|
|
94
|
+
|
|
95
|
+
从主设置和插件中收集钩子定义,构建完整的注册表。
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
settings: 包含 hooks 属性的设置对象
|
|
99
|
+
plugins: 可选的插件列表
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
HookRegistry: 加载完成的钩子注册表
|
|
103
|
+
|
|
104
|
+
使用示例:
|
|
105
|
+
>>> registry = load_hook_registry(settings, plugins)
|
|
106
|
+
"""
|
|
107
|
+
registry = HookRegistry() # 创建新的注册表实例
|
|
108
|
+
|
|
109
|
+
# 遍历设置中的钩子配置
|
|
110
|
+
for raw_event, hooks in settings.hooks.items():
|
|
111
|
+
try:
|
|
112
|
+
# 尝试将字符串转换为 HookEvent 枚举
|
|
113
|
+
event = HookEvent(raw_event)
|
|
114
|
+
except ValueError:
|
|
115
|
+
continue # 跳过无效的事件名称
|
|
116
|
+
# 遍历事件中的钩子定义并注册
|
|
117
|
+
for hook in hooks:
|
|
118
|
+
registry.register(event, hook)
|
|
119
|
+
|
|
120
|
+
# 遍历插件中的钩子配置
|
|
121
|
+
for plugin in plugins or []:
|
|
122
|
+
if not plugin.enabled:
|
|
123
|
+
continue # 跳过未启用的插件
|
|
124
|
+
# 遍历插件的钩子配置
|
|
125
|
+
for raw_event, hooks in plugin.hooks.items():
|
|
126
|
+
try:
|
|
127
|
+
event = HookEvent(raw_event)
|
|
128
|
+
except ValueError:
|
|
129
|
+
continue # 跳过无效的事件名称
|
|
130
|
+
for hook in hooks:
|
|
131
|
+
registry.register(event, hook)
|
|
132
|
+
|
|
133
|
+
return registry # 返回加载完成的注册表
|