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.
- codeagent/__init__.py +10 -0
- codeagent/__main__.py +13 -0
- codeagent/cli/__init__.py +13 -0
- codeagent/cli/entrypoint.py +161 -0
- codeagent/cli/main.py +979 -0
- codeagent/core/__init__.py +2 -0
- codeagent/core/attachments.py +342 -0
- codeagent/core/audio_models.py +50 -0
- codeagent/core/bootstrap.py +178 -0
- codeagent/core/default_presets.py +159 -0
- codeagent/core/env.py +106 -0
- codeagent/core/folder_picker.py +151 -0
- codeagent/core/image_understanding.py +90 -0
- codeagent/core/models.py +6 -0
- codeagent/core/paths.py +238 -0
- codeagent/core/pricing.py +107 -0
- codeagent/core/process_ports.py +258 -0
- codeagent/core/seed_bridge.py +21 -0
- codeagent/core/settings.py +98 -0
- codeagent/core/speech_synth.py +330 -0
- codeagent/core/team_config.py +247 -0
- codeagent/core/team_manager.py +245 -0
- codeagent/core/token_counter.py +41 -0
- codeagent/core/tts_voices.py +103 -0
- codeagent/core/usage_billing.py +72 -0
- codeagent/core/vision_models.py +93 -0
- codeagent/memory/diary.py +144 -0
- codeagent/persona_defaults/README.md +12 -0
- codeagent/persona_defaults/agent.md +352 -0
- codeagent/persona_defaults/identity.md +29 -0
- codeagent/persona_defaults/memory.md +4 -0
- codeagent/persona_defaults/skills.md +325 -0
- codeagent/persona_defaults/soul.md +38 -0
- codeagent/persona_defaults/tools.md +41 -0
- codeagent/persona_defaults/user.md +18 -0
- codeagent/runtime/__init__.py +11 -0
- codeagent/runtime/agent_context.py +6 -0
- codeagent/runtime/agent_runtime.py +21 -0
- codeagent/runtime/compact_prompt.py +9 -0
- codeagent/runtime/compact_state.py +40 -0
- codeagent/runtime/llm_worker.py +132 -0
- codeagent/runtime/orchestrator.py +161 -0
- codeagent/runtime/prompt_enrichment.py +324 -0
- codeagent/runtime/session_manager.py +30 -0
- codeagent/runtime/task_split.py +38 -0
- codeagent/runtime/turn_loop.py +13 -0
- codeagent/runtime/worker.py +48 -0
- codeagent/server/__init__.py +505 -0
- codeagent/server/app_factory.py +1292 -0
- codeagent/server/attachment_api.py +261 -0
- codeagent/server/hub.py +175 -0
- codeagent/server/icon.png +0 -0
- codeagent/server/self_healing.py +143 -0
- codeagent/server/team_engine.py +243 -0
- codeagent/server/tts_api.py +51 -0
- codeagent/server/webui_api_app.py +3180 -0
- codeagent/skills/__init__.py +2 -0
- codeagent/skills/select.py +144 -0
- codeagent/tools/__init__.py +6 -0
- codeagent/tools/agent_tools.py +10 -0
- codeagent/web/__init__.py +1 -0
- codeagent/web/__pycache__/__init__.cpython-313.pyc +0 -0
- codeagent/web/__pycache__/app.cpython-313.pyc +0 -0
- codeagent/web/__pycache__/auth_impl.cpython-313.pyc +0 -0
- codeagent/web/auth_impl.py +326 -0
- codeagent/web/static/00-marked.min.js +6 -0
- codeagent/web/static/00-mermaid.min.js +2314 -0
- codeagent/web/static/00-purify.min.js +3 -0
- codeagent/web/static/00-storage.js +83 -0
- codeagent/web/static/00-utils.js +51 -0
- codeagent/web/static/01-theme.css +96 -0
- codeagent/web/static/01a-scroll.js +86 -0
- codeagent/web/static/01b-think.js +62 -0
- codeagent/web/static/01c-session-identity.js +81 -0
- codeagent/web/static/01c-session-thinking.js +61 -0
- codeagent/web/static/01c-session-tree-menus.js +349 -0
- codeagent/web/static/01c-session-tree.js +531 -0
- codeagent/web/static/01c-session-utilities.js +83 -0
- codeagent/web/static/01c-vision-model.js +132 -0
- codeagent/web/static/01d-image-gen-model.js +84 -0
- codeagent/web/static/01e-audio-model.js +97 -0
- codeagent/web/static/01f-compose-ui.js +88 -0
- codeagent/web/static/01g-tts-settings.js +128 -0
- codeagent/web/static/01h-music-model.js +85 -0
- codeagent/web/static/01i-model-stack.js +291 -0
- codeagent/web/static/01j-video-gen-model.js +85 -0
- codeagent/web/static/01k-model-stack-bootstrap.js +66 -0
- codeagent/web/static/01k-model-stack-state.js +209 -0
- codeagent/web/static/01q-inflight.js +195 -0
- codeagent/web/static/02-reset.css +22 -0
- codeagent/web/static/02-session-ui.js +201 -0
- codeagent/web/static/03-layout-p1.css +2 -0
- codeagent/web/static/03-layout-p2.css +2 -0
- codeagent/web/static/03a-links-stream.js +88 -0
- codeagent/web/static/03b-stream-text.js +119 -0
- codeagent/web/static/03c-render-bubble.js +520 -0
- codeagent/web/static/03d-agent-bubble.js +126 -0
- codeagent/web/static/03e-tool-trace.js +182 -0
- codeagent/web/static/03f-timeline.js +54 -0
- codeagent/web/static/03g-bubble-tts.js +292 -0
- codeagent/web/static/04-components-p1.css +1 -0
- codeagent/web/static/04-components-p2b.css +1 -0
- codeagent/web/static/04-components-p3.css +2 -0
- codeagent/web/static/04-components-p4.css +1 -0
- codeagent/web/static/04-components-p5.css +1 -0
- codeagent/web/static/04-components-p6.css +58 -0
- codeagent/web/static/04-components-p7.css +4 -0
- codeagent/web/static/04-components-p8.css +1 -0
- codeagent/web/static/04-components-p9.css +1 -0
- codeagent/web/static/04-ws-connect.js +351 -0
- codeagent/web/static/04-ws-helpers.js +116 -0
- codeagent/web/static/05-chat-p1.css +1058 -0
- codeagent/web/static/05-chat-p2.css +1 -0
- codeagent/web/static/05-chat-p3.css +295 -0
- codeagent/web/static/05a-session-history.js +176 -0
- codeagent/web/static/05b-session-list.js +114 -0
- codeagent/web/static/05c-dialog.js +123 -0
- codeagent/web/static/05d-project-create.js +172 -0
- codeagent/web/static/05e-project-remote.js +143 -0
- codeagent/web/static/06-chat.js +552 -0
- codeagent/web/static/06-utilities.css +181 -0
- codeagent/web/static/06b-attachments.js +304 -0
- codeagent/web/static/06c-camera.js +488 -0
- codeagent/web/static/07-activity-bar.css +127 -0
- codeagent/web/static/09-git-p1.css +1 -0
- codeagent/web/static/09-git-p2.css +1 -0
- codeagent/web/static/09a-init-bind.js +110 -0
- codeagent/web/static/09b-panel-toggle.js +80 -0
- codeagent/web/static/09c-api-refresh.js +100 -0
- codeagent/web/static/09d-status-log.js +80 -0
- codeagent/web/static/09e-commit.js +80 -0
- codeagent/web/static/09f-remote.js +93 -0
- codeagent/web/static/11a-cron-parse.js +117 -0
- codeagent/web/static/11b-cron-panel.js +100 -0
- codeagent/web/static/11c-cron-card.js +133 -0
- codeagent/web/static/11d-cron-form.js +105 -0
- codeagent/web/static/11e-env-config.js +168 -0
- codeagent/web/static/11f-env-llm-a-state.js +136 -0
- codeagent/web/static/11f-env-llm-b-form.js +409 -0
- codeagent/web/static/11f-env-llm-c-board.js +322 -0
- codeagent/web/static/11g-env-paths-git.js +973 -0
- codeagent/web/static/11h-env-mcp-a-state.js +161 -0
- codeagent/web/static/11h-env-mcp-b-form.js +229 -0
- codeagent/web/static/11h-env-mcp-c-board.js +565 -0
- codeagent/web/static/12-plugins.js +127 -0
- codeagent/web/static/13a-skills.js +156 -0
- codeagent/web/static/13a-todo-utils.js +140 -0
- codeagent/web/static/13b-todo-fetch.js +106 -0
- codeagent/web/static/13b-tools.js +127 -0
- codeagent/web/static/13c-todo-crud.js +92 -0
- codeagent/web/static/14-activity-bar.js +177 -0
- codeagent/web/static/15-plans.css +194 -0
- codeagent/web/static/15-plans.js +219 -0
- codeagent/web/static/16-mobile.css +612 -0
- codeagent/web/static/16-mobile.js +178 -0
- codeagent/web/static/17-preset-ux.css +287 -0
- codeagent/web/static/18-agent-mgr.css +616 -0
- codeagent/web/static/18-agent-mgr.js +204 -0
- codeagent/web/static/24-health-dashboard.js +68 -0
- codeagent/web/static/99-brand-polish.css +152 -0
- codeagent/web/static/body.html +824 -0
- codeagent/web/static/setup/setup_form.html +171 -0
- codeagent/web/static/setup/setup_head.html +100 -0
- codeagent/web/static/setup/setup_tail.html +264 -0
- codeagent/web/static/setup/setup_test.html +38 -0
- codeagent/web/web_login.html +265 -0
- codeagent/web/webui.html +35 -0
- tagword_codeagent-1.1.6.dist-info/METADATA +273 -0
- tagword_codeagent-1.1.6.dist-info/RECORD +173 -0
- tagword_codeagent-1.1.6.dist-info/WHEEL +5 -0
- tagword_codeagent-1.1.6.dist-info/entry_points.txt +3 -0
- tagword_codeagent-1.1.6.dist-info/licenses/LICENSE +21 -0
- 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
|