tagword-codeagent 1.1.6__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 (173) hide show
  1. codeagent/__init__.py +10 -0
  2. codeagent/__main__.py +13 -0
  3. codeagent/cli/__init__.py +13 -0
  4. codeagent/cli/entrypoint.py +161 -0
  5. codeagent/cli/main.py +979 -0
  6. codeagent/core/__init__.py +2 -0
  7. codeagent/core/attachments.py +342 -0
  8. codeagent/core/audio_models.py +50 -0
  9. codeagent/core/bootstrap.py +178 -0
  10. codeagent/core/default_presets.py +159 -0
  11. codeagent/core/env.py +106 -0
  12. codeagent/core/folder_picker.py +151 -0
  13. codeagent/core/image_understanding.py +90 -0
  14. codeagent/core/models.py +6 -0
  15. codeagent/core/paths.py +238 -0
  16. codeagent/core/pricing.py +107 -0
  17. codeagent/core/process_ports.py +258 -0
  18. codeagent/core/seed_bridge.py +21 -0
  19. codeagent/core/settings.py +98 -0
  20. codeagent/core/speech_synth.py +330 -0
  21. codeagent/core/team_config.py +247 -0
  22. codeagent/core/team_manager.py +245 -0
  23. codeagent/core/token_counter.py +41 -0
  24. codeagent/core/tts_voices.py +103 -0
  25. codeagent/core/usage_billing.py +72 -0
  26. codeagent/core/vision_models.py +93 -0
  27. codeagent/memory/diary.py +144 -0
  28. codeagent/persona_defaults/README.md +12 -0
  29. codeagent/persona_defaults/agent.md +352 -0
  30. codeagent/persona_defaults/identity.md +29 -0
  31. codeagent/persona_defaults/memory.md +4 -0
  32. codeagent/persona_defaults/skills.md +325 -0
  33. codeagent/persona_defaults/soul.md +38 -0
  34. codeagent/persona_defaults/tools.md +41 -0
  35. codeagent/persona_defaults/user.md +18 -0
  36. codeagent/runtime/__init__.py +11 -0
  37. codeagent/runtime/agent_context.py +6 -0
  38. codeagent/runtime/agent_runtime.py +21 -0
  39. codeagent/runtime/compact_prompt.py +9 -0
  40. codeagent/runtime/compact_state.py +40 -0
  41. codeagent/runtime/llm_worker.py +132 -0
  42. codeagent/runtime/orchestrator.py +161 -0
  43. codeagent/runtime/prompt_enrichment.py +324 -0
  44. codeagent/runtime/session_manager.py +30 -0
  45. codeagent/runtime/task_split.py +38 -0
  46. codeagent/runtime/turn_loop.py +13 -0
  47. codeagent/runtime/worker.py +48 -0
  48. codeagent/server/__init__.py +505 -0
  49. codeagent/server/app_factory.py +1292 -0
  50. codeagent/server/attachment_api.py +261 -0
  51. codeagent/server/hub.py +175 -0
  52. codeagent/server/icon.png +0 -0
  53. codeagent/server/self_healing.py +143 -0
  54. codeagent/server/team_engine.py +243 -0
  55. codeagent/server/tts_api.py +51 -0
  56. codeagent/server/webui_api_app.py +3180 -0
  57. codeagent/skills/__init__.py +2 -0
  58. codeagent/skills/select.py +144 -0
  59. codeagent/tools/__init__.py +6 -0
  60. codeagent/tools/agent_tools.py +10 -0
  61. codeagent/web/__init__.py +1 -0
  62. codeagent/web/__pycache__/__init__.cpython-313.pyc +0 -0
  63. codeagent/web/__pycache__/app.cpython-313.pyc +0 -0
  64. codeagent/web/__pycache__/auth_impl.cpython-313.pyc +0 -0
  65. codeagent/web/auth_impl.py +326 -0
  66. codeagent/web/static/00-marked.min.js +6 -0
  67. codeagent/web/static/00-mermaid.min.js +2314 -0
  68. codeagent/web/static/00-purify.min.js +3 -0
  69. codeagent/web/static/00-storage.js +83 -0
  70. codeagent/web/static/00-utils.js +51 -0
  71. codeagent/web/static/01-theme.css +96 -0
  72. codeagent/web/static/01a-scroll.js +86 -0
  73. codeagent/web/static/01b-think.js +62 -0
  74. codeagent/web/static/01c-session-identity.js +81 -0
  75. codeagent/web/static/01c-session-thinking.js +61 -0
  76. codeagent/web/static/01c-session-tree-menus.js +349 -0
  77. codeagent/web/static/01c-session-tree.js +531 -0
  78. codeagent/web/static/01c-session-utilities.js +83 -0
  79. codeagent/web/static/01c-vision-model.js +132 -0
  80. codeagent/web/static/01d-image-gen-model.js +84 -0
  81. codeagent/web/static/01e-audio-model.js +97 -0
  82. codeagent/web/static/01f-compose-ui.js +88 -0
  83. codeagent/web/static/01g-tts-settings.js +128 -0
  84. codeagent/web/static/01h-music-model.js +85 -0
  85. codeagent/web/static/01i-model-stack.js +291 -0
  86. codeagent/web/static/01j-video-gen-model.js +85 -0
  87. codeagent/web/static/01k-model-stack-bootstrap.js +66 -0
  88. codeagent/web/static/01k-model-stack-state.js +209 -0
  89. codeagent/web/static/01q-inflight.js +195 -0
  90. codeagent/web/static/02-reset.css +22 -0
  91. codeagent/web/static/02-session-ui.js +201 -0
  92. codeagent/web/static/03-layout-p1.css +2 -0
  93. codeagent/web/static/03-layout-p2.css +2 -0
  94. codeagent/web/static/03a-links-stream.js +88 -0
  95. codeagent/web/static/03b-stream-text.js +119 -0
  96. codeagent/web/static/03c-render-bubble.js +520 -0
  97. codeagent/web/static/03d-agent-bubble.js +126 -0
  98. codeagent/web/static/03e-tool-trace.js +182 -0
  99. codeagent/web/static/03f-timeline.js +54 -0
  100. codeagent/web/static/03g-bubble-tts.js +292 -0
  101. codeagent/web/static/04-components-p1.css +1 -0
  102. codeagent/web/static/04-components-p2b.css +1 -0
  103. codeagent/web/static/04-components-p3.css +2 -0
  104. codeagent/web/static/04-components-p4.css +1 -0
  105. codeagent/web/static/04-components-p5.css +1 -0
  106. codeagent/web/static/04-components-p6.css +58 -0
  107. codeagent/web/static/04-components-p7.css +4 -0
  108. codeagent/web/static/04-components-p8.css +1 -0
  109. codeagent/web/static/04-components-p9.css +1 -0
  110. codeagent/web/static/04-ws-connect.js +351 -0
  111. codeagent/web/static/04-ws-helpers.js +116 -0
  112. codeagent/web/static/05-chat-p1.css +1058 -0
  113. codeagent/web/static/05-chat-p2.css +1 -0
  114. codeagent/web/static/05-chat-p3.css +295 -0
  115. codeagent/web/static/05a-session-history.js +176 -0
  116. codeagent/web/static/05b-session-list.js +114 -0
  117. codeagent/web/static/05c-dialog.js +123 -0
  118. codeagent/web/static/05d-project-create.js +172 -0
  119. codeagent/web/static/05e-project-remote.js +143 -0
  120. codeagent/web/static/06-chat.js +552 -0
  121. codeagent/web/static/06-utilities.css +181 -0
  122. codeagent/web/static/06b-attachments.js +304 -0
  123. codeagent/web/static/06c-camera.js +488 -0
  124. codeagent/web/static/07-activity-bar.css +127 -0
  125. codeagent/web/static/09-git-p1.css +1 -0
  126. codeagent/web/static/09-git-p2.css +1 -0
  127. codeagent/web/static/09a-init-bind.js +110 -0
  128. codeagent/web/static/09b-panel-toggle.js +80 -0
  129. codeagent/web/static/09c-api-refresh.js +100 -0
  130. codeagent/web/static/09d-status-log.js +80 -0
  131. codeagent/web/static/09e-commit.js +80 -0
  132. codeagent/web/static/09f-remote.js +93 -0
  133. codeagent/web/static/11a-cron-parse.js +117 -0
  134. codeagent/web/static/11b-cron-panel.js +100 -0
  135. codeagent/web/static/11c-cron-card.js +133 -0
  136. codeagent/web/static/11d-cron-form.js +105 -0
  137. codeagent/web/static/11e-env-config.js +168 -0
  138. codeagent/web/static/11f-env-llm-a-state.js +136 -0
  139. codeagent/web/static/11f-env-llm-b-form.js +409 -0
  140. codeagent/web/static/11f-env-llm-c-board.js +322 -0
  141. codeagent/web/static/11g-env-paths-git.js +973 -0
  142. codeagent/web/static/11h-env-mcp-a-state.js +161 -0
  143. codeagent/web/static/11h-env-mcp-b-form.js +229 -0
  144. codeagent/web/static/11h-env-mcp-c-board.js +565 -0
  145. codeagent/web/static/12-plugins.js +127 -0
  146. codeagent/web/static/13a-skills.js +156 -0
  147. codeagent/web/static/13a-todo-utils.js +140 -0
  148. codeagent/web/static/13b-todo-fetch.js +106 -0
  149. codeagent/web/static/13b-tools.js +127 -0
  150. codeagent/web/static/13c-todo-crud.js +92 -0
  151. codeagent/web/static/14-activity-bar.js +177 -0
  152. codeagent/web/static/15-plans.css +194 -0
  153. codeagent/web/static/15-plans.js +219 -0
  154. codeagent/web/static/16-mobile.css +612 -0
  155. codeagent/web/static/16-mobile.js +178 -0
  156. codeagent/web/static/17-preset-ux.css +287 -0
  157. codeagent/web/static/18-agent-mgr.css +616 -0
  158. codeagent/web/static/18-agent-mgr.js +204 -0
  159. codeagent/web/static/24-health-dashboard.js +68 -0
  160. codeagent/web/static/99-brand-polish.css +152 -0
  161. codeagent/web/static/body.html +824 -0
  162. codeagent/web/static/setup/setup_form.html +171 -0
  163. codeagent/web/static/setup/setup_head.html +100 -0
  164. codeagent/web/static/setup/setup_tail.html +264 -0
  165. codeagent/web/static/setup/setup_test.html +38 -0
  166. codeagent/web/web_login.html +265 -0
  167. codeagent/web/webui.html +35 -0
  168. tagword_codeagent-1.1.6.dist-info/METADATA +273 -0
  169. tagword_codeagent-1.1.6.dist-info/RECORD +173 -0
  170. tagword_codeagent-1.1.6.dist-info/WHEEL +5 -0
  171. tagword_codeagent-1.1.6.dist-info/entry_points.txt +3 -0
  172. tagword_codeagent-1.1.6.dist-info/licenses/LICENSE +21 -0
  173. tagword_codeagent-1.1.6.dist-info/top_level.txt +1 -0
@@ -0,0 +1,505 @@
1
+ """Resolve tool registry + executor for an agent (HTTP / cron)."""
2
+ from __future__ import annotations
3
+
4
+ import hashlib
5
+ import hmac
6
+ import logging
7
+ import os
8
+ import socket
9
+ import threading
10
+ from datetime import datetime, timezone
11
+ from pathlib import Path
12
+ from typing import TYPE_CHECKING, Any
13
+
14
+
15
+
16
+ if TYPE_CHECKING:
17
+ from codeagent.core.models import Session
18
+
19
+
20
+ def tools_for_agent(agent_id: str):
21
+ try:
22
+ from codeagent.tools.agent_tools import get_tools_for_agent
23
+
24
+ return get_tools_for_agent(agent_id)
25
+ except Exception:
26
+ from seed_tools import setup_builtin_tools
27
+
28
+ return setup_builtin_tools()
29
+
30
+
31
+ _tools_for_agent = tools_for_agent
32
+
33
+
34
+ """
35
+ HTTP + WebSocket + webhook (Starlette). Available by default with pip install codeagent.
36
+ """
37
+
38
+ logger = logging.getLogger(__name__)
39
+
40
+ # Shared with ``seed`` cron / runtime — re-export for ``app_factory`` / ``webui_api_app`` ``from . import``.
41
+ from seed.core._session_cache import ( # noqa: E402
42
+ ACTIVE_CHAT_CANCELS,
43
+ PENDING_INJECTIONS,
44
+ SESSIONS,
45
+ WS_BY_SESSION,
46
+ _memkey,
47
+ )
48
+
49
+ # 当前正在执行中的会话集合(mkey 格式:agent_id::session_id)
50
+ # 用于页面刷新后前端恢复运行状态指示
51
+ # 使用锁保护,因为 api_chat(asyncio)和 api_sessions_running 可能并发访问
52
+ _running_sessions: set[str] = set()
53
+ _running_sessions_lock: threading.Lock = threading.Lock()
54
+
55
+ def _running_add(mkey: str) -> None:
56
+ """线程安全地添加会话到运行中集合。"""
57
+ with _running_sessions_lock:
58
+ _running_sessions.add(mkey)
59
+
60
+ def _running_discard(mkey: str) -> None:
61
+ """线程安全地从运行中集合移除会话。"""
62
+ with _running_sessions_lock:
63
+ _running_sessions.discard(mkey)
64
+
65
+ def _running_contains(mkey: str) -> bool:
66
+ """线程安全地检查会话是否在运行中。"""
67
+ with _running_sessions_lock:
68
+ return mkey in _running_sessions
69
+
70
+ def _running_list(agent_id: str) -> list[str]:
71
+ """线程安全地获取指定 agent 的运行中会话 ID 列表。"""
72
+ prefix = f"{agent_id.strip() or 'default'}::"
73
+ with _running_sessions_lock:
74
+ return [
75
+ mkey.removeprefix(prefix)
76
+ for mkey in _running_sessions
77
+ if mkey.startswith(prefix)
78
+ ]
79
+
80
+ def _env_truthy(name: str, default: str = "0") -> bool:
81
+ return os.environ.get(name, default).strip().lower() in ("1", "true", "yes", "on")
82
+
83
+
84
+ _DEFAULT_AUTO_CONTINUE_NUDGE = (
85
+ "请继续完成未完成事项;基于已执行的工具结果继续推进,避免重复无效操作。"
86
+ "若已完成请直接总结结果。"
87
+ )
88
+
89
+
90
+
91
+
92
+
93
+ # Domain-specific "switch strategy" playbooks. Triggered when the previous
94
+ # tool-loop segment ended with a tail streak of same-domain errors. The goal
95
+ # is NOT to halt, but to keep the agent going with a *different* approach
96
+ # (different tool, different resource, or HTTP-layer fallback) so the user's
97
+ # task actually progresses instead of looping on a broken path.
98
+ _AUTO_CONTINUE_DOMAIN_PLAYBOOKS: dict[str, str] = {
99
+ "browser_": (
100
+ "上一段连续在 browser_* 域失败(调试超时 / 目标 ws 僵尸 / 405 /json/new)。"
101
+ "**禁止**再对同一 target_ws_url 重试同一操作。请换一条思路继续完成用户原本的任务:\n"
102
+ "1) **回退到 HTTP 层验证**:用 `bash` 执行 `curl.exe -sSi <url>` 或 `Invoke-WebRequest`,"
103
+ "或调用 `web_fetch` 直接取 HTML/JSON,跳过浏览器层即可回答用户问题;\n"
104
+ "2) **重建浏览器调试会话**:先调 `browser_targets` 查出现有 type=page 列表;"
105
+ "若有其它存活目标,将 `target_ws_url` 改为 `'active'` 或该目标的 ws 再试一次;"
106
+ "若没有,调 `browser_new_page`(已加 Target.createTarget 回退,可用)再做;\n"
107
+ "3) **退一步排查底层**:`bash` 跑 `netstat -ano | findstr :<port>` 和 "
108
+ "`tasklist | findstr <image>` 确认服务/端口/进程是否还活着,判明根因后再决定是否继续 UI 层调试。\n"
109
+ "请在下一步里选上面任意一条执行;不要把前面已失败的调用再重复一遍。"
110
+ ),
111
+ "bash": (
112
+ "上一段连续在 bash 域失败。请换思路:\n"
113
+ "1) 若启动的是长驻服务,请在下一次调用里显式 `detach=true`;\n"
114
+ "2) 若是跨盘 `cd` / `&&` 失败,请使用 PowerShell 7 (pwsh) 语法,或改用分步多次 `bash`;\n"
115
+ "3) 先 `tasklist` / `netstat` 查状态,再决定后续命令。"
116
+ ),
117
+ "file_": (
118
+ "上一段连续在 file_* 域失败。请换思路:\n"
119
+ "1) 先 `file_read` 核对实际内容,确认 `old_text` 与文件现状逐字符匹配(空格/制表符/换行);\n"
120
+ "2) 必要时改用 `file_write_tool` 重写小文件;\n"
121
+ "3) 路径疑问用 `bash` 先 `ls` / `Test-Path` 确认。"
122
+ ),
123
+ "web_": (
124
+ "上一段连续在网络类工具失败。请换思路:\n"
125
+ "1) 改用 `bash` 跑 `curl.exe -sSi` 看实际 HTTP 状态与 body;\n"
126
+ "2) 若 401/403,检查是否缺 Authorization header / token;\n"
127
+ "3) 若超时,先确认本地网络可达性(ping / Test-NetConnection)。"
128
+ ),
129
+ }
130
+
131
+
132
+ def _auto_continue_nudge(loop_meta: dict[str, Any] | None) -> str:
133
+ """Pick the auto-continue message for the next segment.
134
+
135
+ If the previous segment ended with a tail streak of same-domain failures,
136
+ inject a domain-specific "switch strategy" playbook so the agent moves to a
137
+ different approach instead of re-running the broken chain. Otherwise fall
138
+ back to the generic "please continue" nudge.
139
+ """
140
+ if not isinstance(loop_meta, dict):
141
+ return _DEFAULT_AUTO_CONTINUE_NUDGE
142
+ dom = loop_meta.get("failure_domain")
143
+ if not dom:
144
+ return _DEFAULT_AUTO_CONTINUE_NUDGE
145
+ playbook = _AUTO_CONTINUE_DOMAIN_PLAYBOOKS.get(str(dom))
146
+ if not playbook:
147
+ return _DEFAULT_AUTO_CONTINUE_NUDGE
148
+ streak = int(loop_meta.get("failure_streak") or 0)
149
+ recent_errors = loop_meta.get("recent_errors") or []
150
+ lines: list[str] = [
151
+ f"【策略切换触发】上一段连续 {streak} 次 tool 调用失败,均落在 `{dom}` 域。",
152
+ ]
153
+ if isinstance(recent_errors, list) and recent_errors:
154
+ lines.append("最近错误节选:")
155
+ for item in recent_errors[-3:]:
156
+ if not isinstance(item, dict):
157
+ continue
158
+ tool_name = str(item.get("tool") or "?")
159
+ err_msg = str(item.get("error") or "")
160
+ if len(err_msg) > 200:
161
+ err_msg = err_msg[:200] + "…"
162
+ lines.append(f" - {tool_name}: {err_msg}")
163
+ lines.append("")
164
+ lines.append(playbook)
165
+ return "\n".join(lines)
166
+
167
+
168
+ def _reply_append_tool_summary(reply: str, seg_summary: str) -> str:
169
+ """Append a tool-chain Markdown summary to the user-visible reply (final segment)."""
170
+ if not (seg_summary or "").strip():
171
+ return reply or ""
172
+ try:
173
+ base = (os.environ.get("CODEAGENT_LLM_BASEURL", "") or "").lower()
174
+ if "api.deepseek.com" in base and os.environ.get(
175
+ "CODEAGENT_REPLY_TOOL_SUMMARY_DEEPSEEK", ""
176
+ ).lower() not in ("1", "true", "yes", "on"):
177
+ return reply or ""
178
+ except Exception:
179
+ pass
180
+ if os.environ.get("CODEAGENT_REPLY_TOOL_SUMMARY", "1").lower() in (
181
+ "0",
182
+ "false",
183
+ "no",
184
+ ):
185
+ return reply or ""
186
+ r = (reply or "").rstrip()
187
+ s = seg_summary.strip()
188
+ return (r + "\n\n" + s) if r else s
189
+
190
+
191
+ def _webui_history_rows_from_session(sess: Session, max_chars: int) -> list[dict[str, Any]]:
192
+ """Flatten session to user/assistant rows for Web UI history replay."""
193
+ from seed.core.llm_exec import msg_text_to_str
194
+
195
+ try:
196
+ rc_max = int(os.environ.get("CODEAGENT_WEBUI_SESSION_HISTORY_REASONING_MAX_CHARS", "50000"))
197
+ except ValueError:
198
+ rc_max = 50000
199
+ rc_max = max(0, min(rc_max, 500_000))
200
+ raw: list[dict[str, Any]] = []
201
+ for i, m in enumerate(sess.messages):
202
+ if not isinstance(m, dict):
203
+ continue
204
+ role = m.get("role")
205
+ if role not in ("user", "assistant"):
206
+ continue
207
+ c = msg_text_to_str(m.get("content"))
208
+ if len(c) > max_chars:
209
+ c = c[: max_chars - 24] + "\n…[内容已截断]"
210
+ row: dict[str, Any] = {"role": str(role), "content": c, "text": c, "idx": i}
211
+ atts = m.get("attachments")
212
+ if isinstance(atts, list) and atts:
213
+ row["attachments"] = atts
214
+ tt = m.get("tool_trace")
215
+ if isinstance(tt, list) and tt:
216
+ row["tool_trace"] = tt
217
+ ts = m.get("ts")
218
+ if ts is not None and str(ts).strip():
219
+ row["ts"] = str(ts).strip()
220
+ raw.append(row)
221
+ return raw
222
+
223
+
224
+ def _webui_history_partition_user_blocks(rows: list[dict[str, Any]]) -> list[list[dict[str, Any]]]:
225
+ """Each block starts at a user message; following assistant rows attach until the next user."""
226
+ blocks: list[list[dict[str, Any]]] = []
227
+ cur: list[dict[str, Any]] = []
228
+ for row in rows:
229
+ if row.get("role") == "user":
230
+ if cur:
231
+ blocks.append(cur)
232
+ cur = [row]
233
+ else:
234
+ if not cur:
235
+ cur = [row]
236
+ else:
237
+ cur.append(row)
238
+ if cur:
239
+ blocks.append(cur)
240
+ return blocks
241
+
242
+
243
+ def _listens_on_all_interfaces(host: str) -> bool:
244
+ h = (host or "").strip().lower()
245
+ return h in ("0.0.0.0", "::", "*", "")
246
+
247
+
248
+ def _request_listen_port(request: Any) -> int:
249
+ """TCP port for building Webhook URLs (URL, Host header, or CODEAGENT_PORT / 8765)."""
250
+ try:
251
+ p = request.url.port
252
+ if p:
253
+ return int(p)
254
+ except Exception:
255
+ pass
256
+ host = (request.headers.get("host") or "").strip()
257
+ if host.startswith("[") and "]:" in host:
258
+ tail = host.split("]:", 1)[1]
259
+ if tail.isdigit():
260
+ return int(tail)
261
+ if ":" in host and not host.startswith("["):
262
+ tail = host.rsplit(":", 1)[1]
263
+ if tail.isdigit():
264
+ return int(tail)
265
+ try:
266
+ return int(os.environ.get("CODEAGENT_PORT", "8765"))
267
+ except ValueError:
268
+ return 8765
269
+
270
+
271
+ def _guess_lan_ipv4_addresses() -> list[str]:
272
+ """Best-effort LAN IPv4 list for sharing Web UI / webhook URL (no extra deps)."""
273
+ seen: set[str] = set()
274
+ ordered: list[str] = []
275
+
276
+ def add(ip: str) -> None:
277
+ if not ip or ip.startswith("127."):
278
+ return
279
+ if ip in seen:
280
+ return
281
+ seen.add(ip)
282
+ ordered.append(ip)
283
+
284
+ try:
285
+ s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
286
+ try:
287
+ s.settimeout(0.25)
288
+ s.connect(("8.8.8.8", 80))
289
+ add(s.getsockname()[0])
290
+ except OSError:
291
+ pass
292
+ finally:
293
+ s.close()
294
+ except OSError:
295
+ pass
296
+
297
+ try:
298
+ _, _, addr_list = socket.gethostbyname_ex(socket.gethostname())
299
+ for a in addr_list:
300
+ if a:
301
+ add(a)
302
+ except OSError:
303
+ pass
304
+
305
+ return ordered
306
+
307
+
308
+ def _print_serve_access_hints(host: str, port: int) -> None:
309
+ """Help users open the Web UI from other devices and paste URLs into webhook config."""
310
+ print()
311
+ print(f"CodeAgent 本机访问: http://127.0.0.1:{port}/")
312
+ if _listens_on_all_interfaces(host):
313
+ ips = _guess_lan_ipv4_addresses()
314
+ if ips:
315
+ print("局域网访问(其它设备浏览器、Webhook 等可填下列地址):")
316
+ for ip in ips:
317
+ print(f" http://{ip}:{port}/")
318
+ print(f" 示例 Webhook: http://{ips[0]}:{port}/webhook/in")
319
+ else:
320
+ print(
321
+ f"未能自动检测局域网 IP,请在本机用 ipconfig / ifconfig 查看后手动填写 "
322
+ f"http://<本机IP>:{port}/"
323
+ )
324
+ else:
325
+ h = (host or "").strip()
326
+ print(f"已绑定 {h},访问: http://{h}:{port}/")
327
+ print()
328
+
329
+
330
+ def main(host: str = "0.0.0.0", port: int = 8765) -> None:
331
+ import uvicorn
332
+
333
+ # Cron 调度在 Starlette on_startup 里启动(AsyncIOScheduler 需要已有运行中的事件循环)。
334
+ _print_serve_access_hints(host, port)
335
+ config = uvicorn.Config(
336
+ create_app(),
337
+ host=host,
338
+ port=port,
339
+ log_level="info",
340
+ timeout_graceful_shutdown=5,
341
+ )
342
+ server = uvicorn.Server(config)
343
+ try:
344
+ server.run()
345
+ except KeyboardInterrupt:
346
+ print("\n正在退出(若仍有任务在跑,最多等待数秒;可先点 Web UI「停止」再 Ctrl+C)…")
347
+
348
+
349
+ def _webui_root() -> Path:
350
+ # server/../ -> codeagent/ (web/ and web/static/ live here)
351
+ return Path(__file__).resolve().parent.parent
352
+
353
+
354
+ def _persist_long_user_input(*, agent_id: str, session_id: str, text: str) -> str | None:
355
+ """
356
+ Save long raw user input to disk and return an absolute path hint.
357
+ Best-effort; failures do not block chat.
358
+ """
359
+ try:
360
+ from seed.core.llm_sess import agent_sessions_dir
361
+
362
+ base = Path(agent_sessions_dir(agent_id)) / "_user_inputs"
363
+ base.mkdir(parents=True, exist_ok=True)
364
+ ts = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
365
+ p = base / f"{session_id}_{ts}.txt"
366
+ p.write_text(text, encoding="utf-8")
367
+ return str(p)
368
+ except Exception:
369
+ return None
370
+
371
+
372
+ def _summarize_user_input_with_fallback(
373
+ *,
374
+ raw_text: str,
375
+ baseurl: str,
376
+ model: str,
377
+ max_tokens: int,
378
+ ) -> str:
379
+ from seed.core.llm_exec import get_llm_executor
380
+
381
+ sum_url = os.environ.get("CODEAGENT_USER_INPUT_SUMMARY_BASEURL", "").strip()
382
+ sum_model = os.environ.get("CODEAGENT_USER_INPUT_SUMMARY_MODEL", "").strip()
383
+ exec_url = (sum_url or baseurl).rstrip("/")
384
+ exec_model = sum_model or model
385
+ llm = get_llm_executor(baseURL=exec_url, model=exec_model)
386
+
387
+ try:
388
+ max_in = int(os.environ.get("CODEAGENT_USER_INPUT_SUMMARY_MAX_INPUT_CHARS", "60000") or 60000)
389
+ except Exception:
390
+ max_in = 60000
391
+ max_in = max(2000, min(max_in, 400_000))
392
+
393
+ blob = (raw_text or "").strip()
394
+ if len(blob) > max_in:
395
+ head = max_in // 2
396
+ tail = max_in - head
397
+ blob = blob[:head] + "\n\n...[内容过长,已截断中间部分用于摘要]...\n\n" + blob[-tail:]
398
+
399
+ sys_prompt = (
400
+ "你是一个摘要助手。请将用户提供的超长材料压缩成便于后续执行的摘要(中文输出)。\n"
401
+ "要求:\n"
402
+ "- 保留关键目标、约束、数字、路径、命令、接口、错误信息、决策点。\n"
403
+ "- 将任务拆成 5-12 条要点;必要时给出 TODO 列表与风险点。\n"
404
+ "- 不要编造不存在的信息。\n"
405
+ "- 输出尽量短(建议 600-1500 汉字)。"
406
+ )
407
+ content, _meta = llm.generate(
408
+ [{"role": "system", "content": sys_prompt}, {"role": "user", "content": blob}],
409
+ tools=None,
410
+ max_tokens=max_tokens,
411
+ )
412
+ return (content or "").strip()
413
+
414
+
415
+ def _verify_webhook_signature(body: bytes, sig: str | None) -> bool:
416
+ secret = os.environ.get("CODEAGENT_WEBHOOK_SECRET", "").strip()
417
+ if not secret:
418
+ logger.warning("CODEAGENT_WEBHOOK_SECRET unset — rejecting all webhook/in requests")
419
+ return False
420
+ if not sig or not sig.startswith("sha256="):
421
+ logger.warning("webhook/in missing or malformed signature header")
422
+ return False
423
+ expected = hmac.new(secret.encode("utf-8"), body, hashlib.sha256).hexdigest()
424
+ if not hmac.compare_digest(sig[7:], expected):
425
+ logger.warning("webhook/in signature mismatch")
426
+ return False
427
+ return True
428
+
429
+ _app_html_cache: tuple[str, str, float] | None = None # (html, etag, mtime)
430
+
431
+
432
+ def get_app_html() -> tuple[str, str]:
433
+ """Return (html_content, etag). ETag 基于文件 mtime,浏览器可缓存复用。"""
434
+ global _app_html_cache
435
+ root = _webui_root()
436
+ shell = root / "web" / "webui.html"
437
+ parts_dir = root / "web" / "static"
438
+ body_primary = parts_dir / "body.html"
439
+
440
+ latest = shell.stat().st_mtime if shell.is_file() else 0
441
+ if body_primary.is_file():
442
+ latest = max(latest, body_primary.stat().st_mtime)
443
+ for p in parts_dir.glob("*.css"):
444
+ latest = max(latest, p.stat().st_mtime)
445
+ for p in parts_dir.glob("*.js"):
446
+ latest = max(latest, p.stat().st_mtime)
447
+
448
+ if _app_html_cache is not None and latest <= _app_html_cache[2]:
449
+ return _app_html_cache[0], _app_html_cache[1]
450
+
451
+ shell_html = shell.read_text(encoding="utf-8")
452
+ css_files = sorted(p for p in parts_dir.glob("*.css"))
453
+ styles = "\n\n".join(p.read_text(encoding="utf-8") for p in css_files)
454
+ body = (
455
+ body_primary.read_text(encoding="utf-8")
456
+ if body_primary.is_file()
457
+ else "<div class=\"app\"><p>Web UI body 缺失:缺少 web/static/body.html</p></div>"
458
+ )
459
+
460
+ js_files = sorted(p for p in parts_dir.glob("*.js"))
461
+ scripts = "\n\n".join(p.read_text(encoding="utf-8") for p in js_files)
462
+
463
+ html = shell_html.replace("/*{{STYLES}}*/", styles).replace("{{BODY}}", body).replace("{{SCRIPTS}}", scripts)
464
+ etag = f'w"{int(latest):x}"'
465
+ _app_html_cache = (html, etag, latest)
466
+ return html, etag
467
+
468
+
469
+ def get_setup_html() -> str:
470
+ """Full-page HTML for ``GET /setup``. Prefer repo-root ``web_setup.html`` if present."""
471
+ root = _webui_root()
472
+ single = root / "web_setup.html"
473
+ if single.is_file():
474
+ return single.read_text(encoding="utf-8")
475
+
476
+ parts_dir = root / "web" / "static" / "setup"
477
+ names = (
478
+ "setup_head.html",
479
+ "setup_form.html",
480
+ "setup_test.html",
481
+ "setup_tail.html",
482
+ )
483
+ parts: list[str] = []
484
+ for name in names:
485
+ p = parts_dir / name
486
+ if not p.is_file():
487
+ logger.warning("setup wizard fragment missing: %s", p)
488
+ parts = []
489
+ break
490
+ parts.append(p.read_text(encoding="utf-8"))
491
+ if parts:
492
+ return "".join(parts)
493
+
494
+ return (
495
+ "<!DOCTYPE html><html lang=\"zh-CN\"><meta charset=\"utf-8\"/>"
496
+ "<title>CodeAgent Setup</title><body><h1>初始化</h1>"
497
+ "<p>缺少向导片段(<code>web/static/setup/*.html</code>)。"
498
+ "可在仓库根放置 <code>web_setup.html</code>,或运行 <code>codeagent config init</code>。"
499
+ "</p></body></html>"
500
+ )
501
+
502
+
503
+ # --- Re-exports ---
504
+
505
+ from codeagent.server.app_factory import create_app # noqa: E402