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.
Files changed (214) hide show
  1. illusion/__init__.py +24 -0
  2. illusion/__main__.py +15 -0
  3. illusion/_frontend/dist/index.mjs +39208 -0
  4. illusion/_frontend/package.json +27 -0
  5. illusion/_frontend/src/App.tsx +624 -0
  6. illusion/_frontend/src/components/CommandPicker.tsx +98 -0
  7. illusion/_frontend/src/components/Composer.tsx +55 -0
  8. illusion/_frontend/src/components/ComposerController.tsx +128 -0
  9. illusion/_frontend/src/components/ConversationView.tsx +750 -0
  10. illusion/_frontend/src/components/Footer.tsx +25 -0
  11. illusion/_frontend/src/components/MarkdownContent.tsx +537 -0
  12. illusion/_frontend/src/components/MarkdownTable.tsx +245 -0
  13. illusion/_frontend/src/components/ModalHost.tsx +425 -0
  14. illusion/_frontend/src/components/MultilineTextInput.tsx +250 -0
  15. illusion/_frontend/src/components/PromptInput.tsx +64 -0
  16. illusion/_frontend/src/components/SelectModal.tsx +78 -0
  17. illusion/_frontend/src/components/SidePanel.tsx +175 -0
  18. illusion/_frontend/src/components/Spinner.tsx +77 -0
  19. illusion/_frontend/src/components/StatusBar.tsx +142 -0
  20. illusion/_frontend/src/components/SwarmPanel.tsx +141 -0
  21. illusion/_frontend/src/components/TodoPanel.tsx +126 -0
  22. illusion/_frontend/src/components/ToolCallDisplay.tsx +202 -0
  23. illusion/_frontend/src/components/TranscriptPane.tsx +79 -0
  24. illusion/_frontend/src/components/WelcomeBanner.tsx +37 -0
  25. illusion/_frontend/src/hooks/useBackendSession.ts +468 -0
  26. illusion/_frontend/src/hooks/useTerminalSize.ts +9 -0
  27. illusion/_frontend/src/i18n.ts +78 -0
  28. illusion/_frontend/src/index.tsx +42 -0
  29. illusion/_frontend/src/theme/ThemeContext.tsx +19 -0
  30. illusion/_frontend/src/theme/builtinThemes.ts +89 -0
  31. illusion/_frontend/src/types.ts +110 -0
  32. illusion/_frontend/src/utils/markdown.ts +33 -0
  33. illusion/_frontend/src/utils/thinking.ts +191 -0
  34. illusion/_frontend/tsconfig.json +13 -0
  35. illusion/_web_dist/assets/index-BseIw-ik.css +10 -0
  36. illusion/_web_dist/assets/index-C_0ZWMuW.js +82 -0
  37. illusion/_web_dist/index.html +16 -0
  38. illusion/api/__init__.py +36 -0
  39. illusion/api/client.py +568 -0
  40. illusion/api/codex_client.py +563 -0
  41. illusion/api/compat.py +138 -0
  42. illusion/api/effort.py +128 -0
  43. illusion/api/errors.py +57 -0
  44. illusion/api/openai_client.py +819 -0
  45. illusion/api/provider.py +148 -0
  46. illusion/api/registry.py +479 -0
  47. illusion/api/usage.py +45 -0
  48. illusion/auth/__init__.py +50 -0
  49. illusion/auth/copilot.py +419 -0
  50. illusion/auth/external.py +612 -0
  51. illusion/auth/flows.py +58 -0
  52. illusion/auth/manager.py +214 -0
  53. illusion/auth/storage.py +372 -0
  54. illusion/bridge/__init__.py +38 -0
  55. illusion/bridge/manager.py +190 -0
  56. illusion/bridge/session_runner.py +84 -0
  57. illusion/bridge/types.py +113 -0
  58. illusion/bridge/work_secret.py +131 -0
  59. illusion/cli.py +1228 -0
  60. illusion/commands/__init__.py +32 -0
  61. illusion/commands/registry.py +1934 -0
  62. illusion/config/__init__.py +39 -0
  63. illusion/config/i18n.py +522 -0
  64. illusion/config/paths.py +259 -0
  65. illusion/config/settings.py +564 -0
  66. illusion/coordinator/__init__.py +41 -0
  67. illusion/coordinator/agent_definitions.py +1093 -0
  68. illusion/coordinator/coordinator_mode.py +127 -0
  69. illusion/engine/__init__.py +95 -0
  70. illusion/engine/cost_tracker.py +55 -0
  71. illusion/engine/messages.py +369 -0
  72. illusion/engine/query.py +632 -0
  73. illusion/engine/query_engine.py +343 -0
  74. illusion/engine/stream_events.py +169 -0
  75. illusion/hooks/__init__.py +67 -0
  76. illusion/hooks/events.py +43 -0
  77. illusion/hooks/executor.py +397 -0
  78. illusion/hooks/hot_reload.py +74 -0
  79. illusion/hooks/loader.py +133 -0
  80. illusion/hooks/schemas.py +121 -0
  81. illusion/hooks/types.py +86 -0
  82. illusion/mcp/__init__.py +104 -0
  83. illusion/mcp/client.py +377 -0
  84. illusion/mcp/config.py +140 -0
  85. illusion/mcp/types.py +175 -0
  86. illusion/memory/__init__.py +36 -0
  87. illusion/memory/manager.py +94 -0
  88. illusion/memory/memdir.py +58 -0
  89. illusion/memory/paths.py +57 -0
  90. illusion/memory/scan.py +120 -0
  91. illusion/memory/search.py +83 -0
  92. illusion/memory/types.py +43 -0
  93. illusion/output_styles/__init__.py +15 -0
  94. illusion/output_styles/loader.py +64 -0
  95. illusion/permissions/__init__.py +39 -0
  96. illusion/permissions/checker.py +174 -0
  97. illusion/permissions/modes.py +38 -0
  98. illusion/platforms.py +148 -0
  99. illusion/plugins/__init__.py +71 -0
  100. illusion/plugins/bundled/__init__.py +0 -0
  101. illusion/plugins/installer.py +59 -0
  102. illusion/plugins/loader.py +301 -0
  103. illusion/plugins/schemas.py +51 -0
  104. illusion/plugins/types.py +56 -0
  105. illusion/prompts/__init__.py +29 -0
  106. illusion/prompts/claudemd.py +74 -0
  107. illusion/prompts/context.py +187 -0
  108. illusion/prompts/environment.py +189 -0
  109. illusion/prompts/system_prompt.py +155 -0
  110. illusion/py.typed +0 -0
  111. illusion/sandbox/__init__.py +29 -0
  112. illusion/sandbox/adapter.py +174 -0
  113. illusion/services/__init__.py +59 -0
  114. illusion/services/compact/__init__.py +1015 -0
  115. illusion/services/cron.py +338 -0
  116. illusion/services/cron_scheduler.py +715 -0
  117. illusion/services/file_history.py +258 -0
  118. illusion/services/lsp/__init__.py +455 -0
  119. illusion/services/session_storage.py +237 -0
  120. illusion/services/token_estimation.py +72 -0
  121. illusion/skills/__init__.py +60 -0
  122. illusion/skills/bundled/__init__.py +110 -0
  123. illusion/skills/bundled/content/batch.md +86 -0
  124. illusion/skills/bundled/content/coding-guidelines.md +70 -0
  125. illusion/skills/bundled/content/debug.md +38 -0
  126. illusion/skills/bundled/content/loop.md +82 -0
  127. illusion/skills/bundled/content/remember.md +105 -0
  128. illusion/skills/bundled/content/simplify.md +53 -0
  129. illusion/skills/bundled/content/skillify.md +113 -0
  130. illusion/skills/bundled/content/stuck.md +54 -0
  131. illusion/skills/bundled/content/update-config.md +329 -0
  132. illusion/skills/bundled/content/verify.md +74 -0
  133. illusion/skills/loader.py +219 -0
  134. illusion/skills/registry.py +40 -0
  135. illusion/skills/types.py +24 -0
  136. illusion/state/__init__.py +18 -0
  137. illusion/state/app_state.py +67 -0
  138. illusion/state/store.py +93 -0
  139. illusion/swarm/__init__.py +71 -0
  140. illusion/swarm/agent_executor.py +857 -0
  141. illusion/swarm/in_process.py +259 -0
  142. illusion/swarm/subprocess_backend.py +136 -0
  143. illusion/swarm/team_helpers.py +123 -0
  144. illusion/swarm/types.py +159 -0
  145. illusion/swarm/worktree.py +347 -0
  146. illusion/tasks/__init__.py +33 -0
  147. illusion/tasks/local_agent_task.py +42 -0
  148. illusion/tasks/local_shell_task.py +27 -0
  149. illusion/tasks/manager.py +377 -0
  150. illusion/tasks/stop_task.py +21 -0
  151. illusion/tasks/types.py +88 -0
  152. illusion/tools/__init__.py +126 -0
  153. illusion/tools/agent_tool.py +388 -0
  154. illusion/tools/ask_user_question_tool.py +186 -0
  155. illusion/tools/base.py +149 -0
  156. illusion/tools/bash_tool.py +413 -0
  157. illusion/tools/config_tool.py +90 -0
  158. illusion/tools/cron_tool.py +473 -0
  159. illusion/tools/enter_plan_mode_tool.py +147 -0
  160. illusion/tools/enter_worktree_tool.py +188 -0
  161. illusion/tools/exit_plan_mode_tool.py +69 -0
  162. illusion/tools/exit_worktree_tool.py +225 -0
  163. illusion/tools/file_edit_tool.py +283 -0
  164. illusion/tools/file_read_tool.py +294 -0
  165. illusion/tools/file_write_tool.py +184 -0
  166. illusion/tools/glob_tool.py +165 -0
  167. illusion/tools/grep_tool.py +190 -0
  168. illusion/tools/list_mcp_resources_tool.py +80 -0
  169. illusion/tools/lsp_tool.py +333 -0
  170. illusion/tools/mcp_auth_tool.py +100 -0
  171. illusion/tools/mcp_tool.py +75 -0
  172. illusion/tools/notebook_edit_tool.py +242 -0
  173. illusion/tools/powershell_tool.py +334 -0
  174. illusion/tools/read_mcp_resource_tool.py +63 -0
  175. illusion/tools/repl_tool.py +100 -0
  176. illusion/tools/send_message_tool.py +112 -0
  177. illusion/tools/shell_common.py +187 -0
  178. illusion/tools/skill_tool.py +86 -0
  179. illusion/tools/sleep_tool.py +62 -0
  180. illusion/tools/structured_output_tool.py +58 -0
  181. illusion/tools/task_create_tool.py +98 -0
  182. illusion/tools/task_get_tool.py +94 -0
  183. illusion/tools/task_list_tool.py +94 -0
  184. illusion/tools/task_output_tool.py +55 -0
  185. illusion/tools/task_stop_tool.py +52 -0
  186. illusion/tools/task_update_tool.py +224 -0
  187. illusion/tools/team_create_tool.py +236 -0
  188. illusion/tools/team_delete_tool.py +104 -0
  189. illusion/tools/todo_write_tool.py +198 -0
  190. illusion/tools/tool_search_tool.py +156 -0
  191. illusion/tools/web_fetch_tool.py +264 -0
  192. illusion/tools/web_search_tool.py +186 -0
  193. illusion/ui/__init__.py +23 -0
  194. illusion/ui/app.py +258 -0
  195. illusion/ui/backend_host.py +1180 -0
  196. illusion/ui/input.py +86 -0
  197. illusion/ui/output.py +363 -0
  198. illusion/ui/permission_dialog.py +47 -0
  199. illusion/ui/permission_store.py +99 -0
  200. illusion/ui/protocol.py +384 -0
  201. illusion/ui/react_launcher.py +280 -0
  202. illusion/ui/runtime.py +787 -0
  203. illusion/ui/textual_app.py +603 -0
  204. illusion/ui/web/__init__.py +10 -0
  205. illusion/ui/web/server.py +87 -0
  206. illusion/ui/web/ws_host.py +1197 -0
  207. illusion/utils/__init__.py +0 -0
  208. illusion/utils/ripgrep.py +299 -0
  209. illusion/utils/shell.py +248 -0
  210. illusion_code-0.1.0.dist-info/METADATA +1159 -0
  211. illusion_code-0.1.0.dist-info/RECORD +214 -0
  212. illusion_code-0.1.0.dist-info/WHEEL +4 -0
  213. illusion_code-0.1.0.dist-info/entry_points.txt +2 -0
  214. illusion_code-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,127 @@
1
+ """
2
+ 协调器模式模块
3
+ ==============
4
+
5
+ 本模块提供任务通知的 XML 序列化/反序列化功能。
6
+
7
+ 主要组件:
8
+ - TaskNotification: 已完成任务的结果结构
9
+ - format_task_notification: 序列化为 XML
10
+ - parse_task_notification: 从 XML 解析
11
+
12
+ 使用示例:
13
+ >>> from illusion.coordinator.coordinator_mode import TaskNotification, format_task_notification
14
+ >>> n = TaskNotification(task_id="agent-1", status="completed", summary="Done")
15
+ >>> xml = format_task_notification(n)
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import re
21
+ from dataclasses import dataclass
22
+ from typing import Optional
23
+
24
+
25
+ # ---------------------------------------------------------------------------
26
+ # TaskNotification 数据类
27
+ # ---------------------------------------------------------------------------
28
+
29
+
30
+ @dataclass
31
+ class TaskNotification:
32
+ """已完成代理任务的结构化结果。
33
+
34
+ Attributes:
35
+ task_id: 任务 ID
36
+ status: 状态 (completed/failed/killed)
37
+ summary: 人类可读的状态摘要
38
+ result: 代理的最终文本响应 (可选)
39
+ usage: 使用统计信息 (可选)
40
+ """
41
+
42
+ task_id: str
43
+ """任务 ID。"""
44
+
45
+ status: str
46
+ """状态 (completed/failed/killed)。"""
47
+
48
+ summary: str
49
+ """人类可读的状态摘要。"""
50
+
51
+ result: Optional[str] = None
52
+ """代理的最终文本响应。"""
53
+
54
+ usage: Optional[dict[str, int]] = None
55
+ """使用统计信息。"""
56
+
57
+
58
+ # ---------------------------------------------------------------------------
59
+ # XML 序列化/反序列化
60
+ # ---------------------------------------------------------------------------
61
+
62
+ # 使用统计字段名
63
+ _USAGE_FIELDS = ("total_tokens", "tool_uses", "duration_ms")
64
+
65
+
66
+ def format_task_notification(n: TaskNotification) -> str:
67
+ """将 TaskNotification 序列化为标准 XML envelope。
68
+
69
+ Args:
70
+ n: 任务通知对象
71
+
72
+ Returns:
73
+ str: XML 格式的字符串
74
+ """
75
+ parts = [
76
+ "<task-notification>",
77
+ f"<task-id>{n.task_id}</task-id>",
78
+ f"<status>{n.status}</status>",
79
+ f"<summary>{n.summary}</summary>",
80
+ ]
81
+ if n.result is not None:
82
+ parts.append(f"<result>{n.result}</result>")
83
+ if n.usage:
84
+ parts.append("<usage>")
85
+ for key in _USAGE_FIELDS:
86
+ if key in n.usage:
87
+ parts.append(f" <{key}>{n.usage[key]}</{key}>")
88
+ parts.append("</usage>")
89
+ parts.append("</task-notification>")
90
+ return "\n".join(parts)
91
+
92
+
93
+ def parse_task_notification(xml: str) -> TaskNotification:
94
+ """从 XML 字符串解析 TaskNotification。
95
+
96
+ Args:
97
+ xml: XML 格式的字符串
98
+
99
+ Returns:
100
+ TaskNotification: 解析后的任务通知对象
101
+ """
102
+
103
+ def _extract(tag: str) -> Optional[str]:
104
+ m = re.search(rf"<{tag}>(.*?)</{tag}>", xml, re.DOTALL)
105
+ return m.group(1).strip() if m else None
106
+
107
+ task_id = _extract("task-id") or ""
108
+ status = _extract("status") or ""
109
+ summary = _extract("summary") or ""
110
+ result = _extract("result")
111
+
112
+ usage: Optional[dict[str, int]] = None
113
+ usage_block = re.search(r"<usage>(.*?)</usage>", xml, re.DOTALL)
114
+ if usage_block:
115
+ usage = {}
116
+ for key in _USAGE_FIELDS:
117
+ m = re.search(rf"<{key}>(\d+)</{key}>", usage_block.group(1))
118
+ if m:
119
+ usage[key] = int(m.group(1))
120
+
121
+ return TaskNotification(
122
+ task_id=task_id,
123
+ status=status,
124
+ summary=summary,
125
+ result=result,
126
+ usage=usage,
127
+ )
@@ -0,0 +1,95 @@
1
+ """
2
+ 核心引擎模块
3
+ ============
4
+
5
+ 本模块提供 IllusionCode 核心引擎的导出接口。
6
+
7
+ 主要组件:
8
+ - ConversationMessage: 对话消息模型
9
+ - TextBlock: 文本内容块
10
+ - ToolUseBlock: 工具调用块
11
+ - ToolResultBlock: 工具结果块
12
+ - QueryEngine: 查询引擎
13
+ - AssistantTextDelta: 助手文本增量事件
14
+ - AssistantTurnComplete: 助手轮次完成事件
15
+ - ToolExecutionStarted: 工具执行开始事件
16
+ - ToolExecutionCompleted: 工具执行完成事件
17
+
18
+ 使用示例:
19
+ >>> from illusion.engine import ConversationMessage, QueryEngine
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from typing import TYPE_CHECKING
25
+
26
+ if TYPE_CHECKING: # 类型检查时导入,避免循环依赖
27
+ from illusion.engine.messages import (
28
+ ConversationMessage,
29
+ TextBlock,
30
+ ToolResultBlock,
31
+ ToolUseBlock,
32
+ )
33
+ from illusion.engine.query_engine import QueryEngine
34
+ from illusion.engine.stream_events import (
35
+ AssistantTextDelta,
36
+ AssistantTurnComplete,
37
+ ToolExecutionCompleted,
38
+ ToolExecutionStarted,
39
+ )
40
+
41
+ __all__ = [
42
+ "AssistantTextDelta",
43
+ "AssistantTurnComplete",
44
+ "ConversationMessage",
45
+ "QueryEngine",
46
+ "TextBlock",
47
+ "ToolExecutionCompleted",
48
+ "ToolExecutionStarted",
49
+ "ToolResultBlock",
50
+ "ToolUseBlock",
51
+ ]
52
+
53
+
54
+ def __getattr__(name: str):
55
+ if name in {"ConversationMessage", "TextBlock", "ToolResultBlock", "ToolUseBlock"}:
56
+ from illusion.engine.messages import (
57
+ ConversationMessage,
58
+ TextBlock,
59
+ ToolResultBlock,
60
+ ToolUseBlock,
61
+ )
62
+
63
+ return {
64
+ "ConversationMessage": ConversationMessage,
65
+ "TextBlock": TextBlock,
66
+ "ToolResultBlock": ToolResultBlock,
67
+ "ToolUseBlock": ToolUseBlock,
68
+ }[name]
69
+
70
+ if name == "QueryEngine":
71
+ from illusion.engine.query_engine import QueryEngine
72
+
73
+ return QueryEngine
74
+
75
+ if name in {
76
+ "AssistantTextDelta",
77
+ "AssistantTurnComplete",
78
+ "ToolExecutionCompleted",
79
+ "ToolExecutionStarted",
80
+ }:
81
+ from illusion.engine.stream_events import (
82
+ AssistantTextDelta,
83
+ AssistantTurnComplete,
84
+ ToolExecutionCompleted,
85
+ ToolExecutionStarted,
86
+ )
87
+
88
+ return {
89
+ "AssistantTextDelta": AssistantTextDelta,
90
+ "AssistantTurnComplete": AssistantTurnComplete,
91
+ "ToolExecutionCompleted": ToolExecutionCompleted,
92
+ "ToolExecutionStarted": ToolExecutionStarted,
93
+ }[name]
94
+
95
+ raise AttributeError(name)
@@ -0,0 +1,55 @@
1
+ """简单的使用量聚合。
2
+
3
+ 本模块提供成本跟踪功能,用于累积会话期间的使用量统计。
4
+
5
+ 主要类:
6
+ - CostTracker: 使用量累积器
7
+
8
+ 使用示例:
9
+ >>> from illusion.engine.cost_tracker import CostTracker
10
+ >>> tracker = CostTracker()
11
+ >>> tracker.add(usage_snapshot)
12
+ >>> print(tracker.total)
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from illusion.api.usage import UsageSnapshot
18
+
19
+
20
+ class CostTracker:
21
+ """在整个会话期间累积使用量。
22
+
23
+ 用于跟踪对话过程中的令牌使用情况,包括输入和输出令牌数。
24
+
25
+ Attributes:
26
+ total: 累积的总使用量(只读属性)
27
+
28
+ 使用示例:
29
+ >>> tracker = CostTracker()
30
+ >>> tracker.add(UsageSnapshot(input_tokens=100, output_tokens=50))
31
+ >>> print(tracker.total.input_tokens) # 100
32
+ """
33
+
34
+ def __init__(self) -> None:
35
+ self._usage = UsageSnapshot() # 初始化使用量快照
36
+
37
+ def add(self, usage: UsageSnapshot) -> None:
38
+ """将使用量快照添加到运行总和。
39
+
40
+ Args:
41
+ usage: 要添加的使用量快照
42
+ """
43
+ self._usage = UsageSnapshot(
44
+ input_tokens=self._usage.input_tokens + usage.input_tokens,
45
+ output_tokens=self._usage.output_tokens + usage.output_tokens,
46
+ )
47
+
48
+ @property
49
+ def total(self) -> UsageSnapshot:
50
+ """返回聚合后的使用量。
51
+
52
+ Returns:
53
+ UsageSnapshot: 累积的使用量快照
54
+ """
55
+ return self._usage
@@ -0,0 +1,369 @@
1
+ """
2
+ 对话消息模型模块
3
+ ================
4
+
5
+ 本模块提供查询引擎使用的对话消息模型。
6
+
7
+ 主要类:
8
+ - TextBlock: 纯文本内容块
9
+ - ToolUseBlock: 模型执行命名工具的请求
10
+ - ToolResultBlock: 发送回模型的工具结果内容
11
+ - ConversationMessage: 单个助手或用户消息
12
+
13
+ 使用示例:
14
+ >>> from illusion.engine.messages import ConversationMessage, TextBlock
15
+ >>> msg = ConversationMessage.from_user_text("Hello")
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from typing import Any, Annotated, Literal
21
+ from uuid import uuid4
22
+
23
+ from pydantic import BaseModel, Field
24
+
25
+
26
+ class TextBlock(BaseModel):
27
+ """纯文本内容块
28
+
29
+ Attributes:
30
+ type: 块类型(固定为 "text")
31
+ text: 文本内容
32
+ """
33
+
34
+ type: Literal["text"] = "text"
35
+ text: str
36
+
37
+
38
+ class ToolUseBlock(BaseModel):
39
+ """模型执行命名工具的请求
40
+
41
+ Attributes:
42
+ type: 块类型(固定为 "tool_use")
43
+ id: 工具调用唯一标识
44
+ name: 工具名称
45
+ input: 工具输入参数
46
+ """
47
+
48
+ type: Literal["tool_use"] = "tool_use"
49
+ id: str = Field(default_factory=lambda: f"toolu_{uuid4().hex}")
50
+ name: str
51
+ input: dict[str, Any] = Field(default_factory=dict)
52
+
53
+
54
+ class ToolResultBlock(BaseModel):
55
+ """发送回模型的工具结果内容
56
+
57
+ Attributes:
58
+ type: 块类型(固定为 "tool_result")
59
+ tool_use_id: 对应的工具调用 ID
60
+ content: 工具返回的内容(纯文本或内容块列表)
61
+ is_error: 是否为错误结果
62
+ """
63
+
64
+ type: Literal["tool_result"] = "tool_result"
65
+ tool_use_id: str
66
+ content: str | list[ContentBlock] = ""
67
+ is_error: bool = False
68
+
69
+ @property
70
+ def text_content(self) -> str:
71
+ """返回纯文本内容。
72
+
73
+ Returns:
74
+ str: 文本字符串(content 为列表时提取 TextBlock 的文本)
75
+ """
76
+ if isinstance(self.content, str):
77
+ return self.content
78
+ return "".join(
79
+ block.text for block in self.content if isinstance(block, TextBlock)
80
+ )
81
+
82
+
83
+ class ThinkingBlock(BaseModel):
84
+ """思考内容块(Anthropic extended thinking / DeepSeek thinking mode)
85
+
86
+ Attributes:
87
+ type: 块类型(固定为 "thinking")
88
+ thinking: 思考文本
89
+ signature: 加密签名(Anthropic API 需要回传以验证思考内容未被篡改)
90
+ """
91
+
92
+ type: Literal["thinking"] = "thinking"
93
+ thinking: str
94
+ signature: str = ""
95
+
96
+
97
+ class MediaBlock(BaseModel):
98
+ """图片文件内容块。
99
+
100
+ Attributes:
101
+ type: 块类型(固定为 "media")
102
+ file_path: 文件绝对路径
103
+ media_type: MIME 类型,如 "image/png"
104
+ data: base64 编码的文件数据
105
+ metadata: 额外信息(文件大小等)
106
+ """
107
+
108
+ type: Literal["media"] = "media"
109
+ file_path: str
110
+ media_type: str
111
+ data: str = ""
112
+ metadata: dict[str, Any] = Field(default_factory=dict)
113
+
114
+
115
+ def _build_tool_result_content(
116
+ output: str,
117
+ metadata: dict[str, Any],
118
+ ) -> str | list[ContentBlock]:
119
+ """从工具输出和元数据构建 ToolResultBlock 的内容。
120
+
121
+ 如果元数据中包含媒体信息,返回 TextBlock + MediaBlock 的列表;
122
+ 否则返回原始文本。
123
+ """
124
+ if "media_category" not in metadata:
125
+ return output
126
+
127
+ media_block = MediaBlock(
128
+ file_path=metadata.get("media_path", ""),
129
+ media_type=metadata.get("media_type", "application/octet-stream"),
130
+ data=metadata.get("media_data", ""),
131
+ metadata={"size": metadata.get("media_size", 0)},
132
+ )
133
+ return [TextBlock(text=output), media_block]
134
+
135
+
136
+ def _messages_have_media(messages: list[ConversationMessage]) -> bool:
137
+ """检查消息列表中是否包含 MediaBlock"""
138
+ for msg in messages:
139
+ for block in msg.content:
140
+ if isinstance(block, MediaBlock):
141
+ return True
142
+ if isinstance(block, ToolResultBlock) and isinstance(block.content, list):
143
+ if any(isinstance(b, MediaBlock) for b in block.content):
144
+ return True
145
+ return False
146
+
147
+
148
+ def _strip_media_from_messages(messages: list[ConversationMessage]) -> list[ConversationMessage]:
149
+ """将消息中的 MediaBlock 替换为文本描述,用于不支持图片的模型优雅降级"""
150
+ result: list[ConversationMessage] = []
151
+ for msg in messages:
152
+ new_blocks: list[Any] = []
153
+ for block in msg.content:
154
+ if isinstance(block, MediaBlock):
155
+ size_str = f" ({block.metadata['size']} bytes)" if "size" in block.metadata else ""
156
+ new_blocks.append(TextBlock(
157
+ text=f"[image file: {block.file_path}{size_str}, {block.media_type}] "
158
+ "This model does not support image input",
159
+ ))
160
+ elif isinstance(block, ToolResultBlock) and isinstance(block.content, list):
161
+ stripped: list[Any] = []
162
+ for b in block.content:
163
+ if isinstance(b, MediaBlock):
164
+ size_str = f" ({b.metadata['size']} bytes)" if "size" in b.metadata else ""
165
+ stripped.append(TextBlock(
166
+ text=f"[image file: {b.file_path}{size_str}, {b.media_type}] "
167
+ "This model does not support image input",
168
+ ))
169
+ else:
170
+ stripped.append(b)
171
+ new_blocks.append(ToolResultBlock(
172
+ tool_use_id=block.tool_use_id,
173
+ content=stripped,
174
+ is_error=block.is_error,
175
+ ))
176
+ else:
177
+ new_blocks.append(block)
178
+ result.append(ConversationMessage(role=msg.role, content=new_blocks))
179
+ return result
180
+
181
+
182
+ # 内容块联合类型
183
+ ContentBlock = Annotated[
184
+ TextBlock | ToolUseBlock | ToolResultBlock | ThinkingBlock | MediaBlock,
185
+ Field(discriminator="type"),
186
+ ]
187
+
188
+
189
+ class ConversationMessage(BaseModel):
190
+ """单个助手或用户消息
191
+
192
+ Attributes:
193
+ role: 消息角色("user" 或 "assistant")
194
+ content: 内容块列表
195
+ """
196
+
197
+ role: Literal["user", "assistant"]
198
+ content: list[ContentBlock] = Field(default_factory=list)
199
+
200
+ @classmethod
201
+ def from_user_text(cls, text: str) -> "ConversationMessage":
202
+ """从原始文本构造用户消息
203
+
204
+ Args:
205
+ text: 用户输入文本
206
+
207
+ Returns:
208
+ ConversationMessage: 用户消息
209
+ """
210
+ return cls(role="user", content=[TextBlock(text=text)])
211
+
212
+ @property
213
+ def text(self) -> str:
214
+ """返回连接的文本块
215
+
216
+ Returns:
217
+ str: 所有文本块的连接字符串
218
+ """
219
+ return "".join(
220
+ block.text for block in self.content if isinstance(block, TextBlock)
221
+ )
222
+
223
+ @property
224
+ def tool_uses(self) -> list[ToolUseBlock]:
225
+ """返回消息中包含的所有工具调用
226
+
227
+ Returns:
228
+ list[ToolUseBlock]: 工具调用列表
229
+ """
230
+ return [block for block in self.content if isinstance(block, ToolUseBlock)]
231
+
232
+ @property
233
+ def thinking_text(self) -> str:
234
+ """返回连接后的思考文本块。"""
235
+ return "\n\n".join(
236
+ block.thinking.strip()
237
+ for block in self.content
238
+ if isinstance(block, ThinkingBlock) and block.thinking.strip()
239
+ )
240
+
241
+ def to_api_param(self, *, provider_type: str = "anthropic") -> dict[str, Any]:
242
+ """将消息转换为提供商 SDK 消息参数。
243
+
244
+ Args:
245
+ provider_type: 提供商类型
246
+
247
+ Returns:
248
+ dict[str, Any]: API 参数格式的字典
249
+ """
250
+ content_blocks = list(self.content)
251
+
252
+ # MiMo 等 API 要求 Text block 的 text 字段最小长度为 1
253
+ # 过滤掉空文本块,避免 "content or tool_calls must be set" 错误
254
+ if self.role == "assistant":
255
+ content_blocks = [
256
+ b for b in content_blocks
257
+ if not (isinstance(b, TextBlock) and not b.text.strip())
258
+ ]
259
+ # 如果过滤后没有内容,添加占位符
260
+ if not content_blocks:
261
+ content_blocks = [TextBlock(text="...")]
262
+
263
+ return {
264
+ "role": self.role,
265
+ "content": [serialize_content_block(block, provider_type=provider_type) for block in content_blocks],
266
+ }
267
+
268
+
269
+ def serialize_content_block(block: ContentBlock, *, provider_type: str = "anthropic") -> dict[str, Any]:
270
+ """将本地内容块转换为提供商线格式。
271
+
272
+ Args:
273
+ block: 内容块
274
+ provider_type: 提供商类型("anthropic"、"openai_compat"、"openai_codex")
275
+
276
+ Returns:
277
+ dict[str, Any]: 线格式字典
278
+ """
279
+ if isinstance(block, TextBlock):
280
+ return {"type": "text", "text": block.text}
281
+
282
+ if isinstance(block, ToolUseBlock):
283
+ return {
284
+ "type": "tool_use",
285
+ "id": block.id,
286
+ "name": block.name,
287
+ "input": block.input,
288
+ }
289
+
290
+ if isinstance(block, ThinkingBlock):
291
+ result: dict[str, Any] = {"type": "thinking", "thinking": block.thinking}
292
+ if block.signature:
293
+ result["signature"] = block.signature
294
+ return result
295
+
296
+ if isinstance(block, MediaBlock):
297
+ return _serialize_media_block(block, provider_type)
298
+
299
+ # tool_result
300
+ if isinstance(block.content, list):
301
+ serialized_content = [
302
+ serialize_content_block(inner, provider_type=provider_type)
303
+ for inner in block.content
304
+ ]
305
+ else:
306
+ serialized_content = block.content
307
+ return {
308
+ "type": "tool_result",
309
+ "tool_use_id": block.tool_use_id,
310
+ "content": serialized_content,
311
+ "is_error": block.is_error,
312
+ }
313
+
314
+
315
+ def _serialize_media_block(block: MediaBlock, provider_type: str) -> dict[str, Any]:
316
+ """将图片 MediaBlock 按提供商格式序列化。"""
317
+ if provider_type == "anthropic":
318
+ return {
319
+ "type": "image",
320
+ "source": {
321
+ "type": "base64",
322
+ "media_type": block.media_type,
323
+ "data": block.data,
324
+ },
325
+ }
326
+ if provider_type == "openai_codex":
327
+ return {
328
+ "type": "input_image",
329
+ "image_url": f"data:{block.media_type};base64,{block.data}",
330
+ }
331
+ # openai_compat
332
+ return {
333
+ "type": "image_url",
334
+ "image_url": {"url": f"data:{block.media_type};base64,{block.data}"},
335
+ }
336
+
337
+
338
+ def assistant_message_from_api(raw_message: Any) -> ConversationMessage:
339
+ """将 Anthropic SDK 消息对象转换为对话消息
340
+
341
+ Args:
342
+ raw_message: Anthropic SDK 原始消息
343
+
344
+ Returns:
345
+ ConversationMessage: 转换后的对话消息
346
+ """
347
+ content: list[ContentBlock] = []
348
+
349
+ for raw_block in getattr(raw_message, "content", []):
350
+ block_type = getattr(raw_block, "type", None)
351
+ if block_type == "text":
352
+ content.append(TextBlock(text=getattr(raw_block, "text", "")))
353
+ elif block_type == "tool_use":
354
+ content.append(
355
+ ToolUseBlock(
356
+ id=getattr(raw_block, "id", f"toolu_{uuid4().hex}"),
357
+ name=getattr(raw_block, "name", ""),
358
+ input=dict(getattr(raw_block, "input", {}) or {}),
359
+ )
360
+ )
361
+ elif block_type == "thinking":
362
+ content.append(
363
+ ThinkingBlock(
364
+ thinking=getattr(raw_block, "thinking", ""),
365
+ signature=getattr(raw_block, "signature", "") or "",
366
+ )
367
+ )
368
+
369
+ return ConversationMessage(role="assistant", content=content)