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,342 @@
|
|
|
1
|
+
"""Attachment subsystem — file storage, user message building, directory scan."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import fnmatch
|
|
6
|
+
import mimetypes
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
from dataclasses import asdict, dataclass, field
|
|
10
|
+
from datetime import datetime, timezone
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Optional
|
|
13
|
+
|
|
14
|
+
from codeagent.core import env as ca_env
|
|
15
|
+
|
|
16
|
+
ATTACHMENT_TAG_RE = re.compile(
|
|
17
|
+
r"\[attachment:([^\s\]]+)(?:\s+([^\]]+))?\]",
|
|
18
|
+
re.IGNORECASE,
|
|
19
|
+
)
|
|
20
|
+
IMAGE_DIR_TAG_RE = re.compile(
|
|
21
|
+
r"\[image_dir:([^\s\]]+)(?:\s+max=(\d+))?\]",
|
|
22
|
+
re.IGNORECASE,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
_DEFAULT_IMAGE_GLOBS = ("*.png", "*.jpg", "*.jpeg", "*.webp", "*.gif", "*.bmp")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class AttachmentMeta:
|
|
30
|
+
id: str
|
|
31
|
+
kind: str
|
|
32
|
+
mime: str
|
|
33
|
+
filename: str
|
|
34
|
+
size_bytes: int
|
|
35
|
+
path: str = ""
|
|
36
|
+
extracted_text: Optional[str] = None
|
|
37
|
+
|
|
38
|
+
def to_dict(self) -> dict[str, Any]:
|
|
39
|
+
return {k: v for k, v in asdict(self).items() if v is not None}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _max_count() -> int:
|
|
43
|
+
return ca_env.pick_int(8, "CODEAGENT_ATTACHMENTS_MAX_COUNT")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _max_bytes() -> int:
|
|
47
|
+
return ca_env.pick_int(20 * 1024 * 1024, "CODEAGENT_ATTACHMENTS_MAX_BYTES")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _dir_max_files() -> int:
|
|
51
|
+
return ca_env.pick_int(32, "CODEAGENT_ATTACHMENTS_DIR_MAX_FILES")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _allowed_mime_prefixes() -> tuple[str, ...]:
|
|
55
|
+
raw = ca_env.pick_default(
|
|
56
|
+
"image/,video/,audio/,application/pdf,text/",
|
|
57
|
+
"CODEAGENT_ATTACHMENTS_ALLOWED_MIME",
|
|
58
|
+
)
|
|
59
|
+
return tuple(p.strip() for p in raw.split(",") if p.strip())
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def mime_allowed(mime: str, filename: str = "") -> bool:
|
|
63
|
+
m = (mime or "").lower().split(";")[0].strip()
|
|
64
|
+
if not m and filename:
|
|
65
|
+
guessed, _ = mimetypes.guess_type(filename)
|
|
66
|
+
m = (guessed or "").lower()
|
|
67
|
+
if m in ("image/svg+xml", "text/html"):
|
|
68
|
+
return False
|
|
69
|
+
for prefix in _allowed_mime_prefixes():
|
|
70
|
+
if prefix.endswith("/") and m.startswith(prefix):
|
|
71
|
+
return True
|
|
72
|
+
if m == prefix:
|
|
73
|
+
return True
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _max_bytes_for_kind(kind: str) -> int:
|
|
78
|
+
k = (kind or "").lower()
|
|
79
|
+
if k in ("video", "audio"):
|
|
80
|
+
return ca_env.pick_int(100 * 1024 * 1024, "CODEAGENT_MEDIA_MAX_BYTES")
|
|
81
|
+
return _max_bytes()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _kind_for_mime(mime: str, filename: str) -> str:
|
|
85
|
+
m = (mime or "").lower()
|
|
86
|
+
if m.startswith("image/"):
|
|
87
|
+
return "image"
|
|
88
|
+
if m.startswith("video/"):
|
|
89
|
+
return "video"
|
|
90
|
+
if m.startswith("audio/"):
|
|
91
|
+
return "audio"
|
|
92
|
+
if m == "application/pdf" or filename.lower().endswith(".pdf"):
|
|
93
|
+
return "document"
|
|
94
|
+
return "document"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def extract_document_text(raw: bytes, mime: str, filename: str = "") -> str:
|
|
98
|
+
m = (mime or "").lower()
|
|
99
|
+
name = (filename or "").lower()
|
|
100
|
+
if m.startswith("text/") or name.endswith((".txt", ".md", ".markdown")):
|
|
101
|
+
return raw.decode("utf-8", errors="replace").strip()
|
|
102
|
+
if m == "application/pdf" or name.endswith(".pdf"):
|
|
103
|
+
try:
|
|
104
|
+
from pypdf import PdfReader
|
|
105
|
+
import io
|
|
106
|
+
|
|
107
|
+
reader = PdfReader(io.BytesIO(raw))
|
|
108
|
+
parts = []
|
|
109
|
+
for page in reader.pages:
|
|
110
|
+
t = page.extract_text() or ""
|
|
111
|
+
if t.strip():
|
|
112
|
+
parts.append(t.strip())
|
|
113
|
+
text = "\n\n".join(parts).strip()
|
|
114
|
+
if not text:
|
|
115
|
+
return "[PDF 无文本层;请转为图片后使用 vision_analyze]"
|
|
116
|
+
return text
|
|
117
|
+
except ImportError:
|
|
118
|
+
return "[PDF 解析需要 pip install pypdf]"
|
|
119
|
+
except Exception as e:
|
|
120
|
+
return f"[PDF 提取失败: {e}]"
|
|
121
|
+
return ""
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def save_attachment(
|
|
125
|
+
*,
|
|
126
|
+
agent_id: str,
|
|
127
|
+
session_id: str,
|
|
128
|
+
raw_bytes: bytes,
|
|
129
|
+
filename: str,
|
|
130
|
+
mime: str = "",
|
|
131
|
+
) -> AttachmentMeta:
|
|
132
|
+
kind_guess = _kind_for_mime(mime, filename)
|
|
133
|
+
limit = _max_bytes_for_kind(kind_guess)
|
|
134
|
+
if len(raw_bytes) > limit:
|
|
135
|
+
raise ValueError(f"attachment exceeds {limit} bytes")
|
|
136
|
+
if not mime_allowed(mime, filename):
|
|
137
|
+
raise ValueError(f"mime not allowed: {mime or filename}")
|
|
138
|
+
from seed.core.media_store import save_session_media
|
|
139
|
+
|
|
140
|
+
aid, path = save_session_media(
|
|
141
|
+
agent_id=agent_id,
|
|
142
|
+
session_id=session_id,
|
|
143
|
+
raw_bytes=raw_bytes,
|
|
144
|
+
filename=filename,
|
|
145
|
+
mime=mime,
|
|
146
|
+
)
|
|
147
|
+
kind = _kind_for_mime(mime, filename)
|
|
148
|
+
extracted = None
|
|
149
|
+
if kind == "document":
|
|
150
|
+
extracted = extract_document_text(raw_bytes, mime, filename) or None
|
|
151
|
+
return AttachmentMeta(
|
|
152
|
+
id=aid,
|
|
153
|
+
kind=kind,
|
|
154
|
+
mime=(mime or mimetypes.guess_type(filename)[0] or "application/octet-stream"),
|
|
155
|
+
filename=filename or aid,
|
|
156
|
+
size_bytes=len(raw_bytes),
|
|
157
|
+
path=str(path),
|
|
158
|
+
extracted_text=extracted,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def resolve_attachment_path(agent_id: str, session_id: str, attachment_id: str) -> Optional[Path]:
|
|
163
|
+
from seed.core.media_store import resolve_session_media_path
|
|
164
|
+
|
|
165
|
+
p = resolve_session_media_path(agent_id, session_id, attachment_id)
|
|
166
|
+
return p
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def load_attachment_meta(agent_id: str, session_id: str, attachment_id: str) -> Optional[AttachmentMeta]:
|
|
170
|
+
p = resolve_attachment_path(agent_id, session_id, attachment_id)
|
|
171
|
+
if not p or not p.is_file():
|
|
172
|
+
return None
|
|
173
|
+
mime, _ = mimetypes.guess_type(str(p))
|
|
174
|
+
kind = _kind_for_mime(mime or "", p.name)
|
|
175
|
+
return AttachmentMeta(
|
|
176
|
+
id=attachment_id,
|
|
177
|
+
kind=kind,
|
|
178
|
+
mime=mime or "application/octet-stream",
|
|
179
|
+
filename=p.name.split("_", 1)[-1] if "_" in p.name else p.name,
|
|
180
|
+
size_bytes=p.stat().st_size,
|
|
181
|
+
path=str(p.resolve()),
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def build_user_message(
|
|
186
|
+
text: str,
|
|
187
|
+
attachments: list[AttachmentMeta],
|
|
188
|
+
*,
|
|
189
|
+
image_dir_tag: str = "",
|
|
190
|
+
) -> dict[str, Any]:
|
|
191
|
+
lines: list[str] = []
|
|
192
|
+
t = (text or "").strip()
|
|
193
|
+
if t:
|
|
194
|
+
lines.append(t)
|
|
195
|
+
elif attachments:
|
|
196
|
+
lines.append("[附件]")
|
|
197
|
+
for a in attachments:
|
|
198
|
+
if a.kind == "document" and a.extracted_text:
|
|
199
|
+
lines.append(f"[文档: {a.filename}]\n{a.extracted_text}")
|
|
200
|
+
else:
|
|
201
|
+
lines.append(f"[attachment:{a.id} {a.filename}]")
|
|
202
|
+
if image_dir_tag.strip():
|
|
203
|
+
lines.append(image_dir_tag.strip())
|
|
204
|
+
msg: dict[str, Any] = {
|
|
205
|
+
"role": "user",
|
|
206
|
+
"content": "\n".join(lines),
|
|
207
|
+
"ts": datetime.now(timezone.utc).isoformat(),
|
|
208
|
+
}
|
|
209
|
+
if attachments:
|
|
210
|
+
msg["attachments"] = [a.to_dict() for a in attachments]
|
|
211
|
+
return msg
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def content_text_for_skills(content: Any) -> str:
|
|
215
|
+
if content is None:
|
|
216
|
+
return ""
|
|
217
|
+
if isinstance(content, str):
|
|
218
|
+
text = content
|
|
219
|
+
elif isinstance(content, list):
|
|
220
|
+
parts = []
|
|
221
|
+
for item in content:
|
|
222
|
+
if isinstance(item, dict) and item.get("type") == "text":
|
|
223
|
+
parts.append(str(item.get("text") or ""))
|
|
224
|
+
text = "".join(parts)
|
|
225
|
+
else:
|
|
226
|
+
text = str(content)
|
|
227
|
+
text = ATTACHMENT_TAG_RE.sub("", text)
|
|
228
|
+
text = IMAGE_DIR_TAG_RE.sub("", text)
|
|
229
|
+
return text.strip()
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def parse_image_dir_tags(text: str) -> list[tuple[str, int]]:
|
|
233
|
+
out: list[tuple[str, int]] = []
|
|
234
|
+
for m in IMAGE_DIR_TAG_RE.finditer(text or ""):
|
|
235
|
+
rel = m.group(1).strip()
|
|
236
|
+
try:
|
|
237
|
+
mx = int(m.group(2)) if m.group(2) else _dir_max_files()
|
|
238
|
+
except ValueError:
|
|
239
|
+
mx = _dir_max_files()
|
|
240
|
+
out.append((rel, min(mx, _dir_max_files())))
|
|
241
|
+
return out
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def scan_image_directory(
|
|
245
|
+
workspace_root: Path,
|
|
246
|
+
rel_path: str,
|
|
247
|
+
*,
|
|
248
|
+
pattern: Optional[str] = None,
|
|
249
|
+
max_files: Optional[int] = None,
|
|
250
|
+
) -> tuple[list[Path], bool]:
|
|
251
|
+
"""Return (image paths, truncated). Paths resolved under workspace_root."""
|
|
252
|
+
root = workspace_root.resolve()
|
|
253
|
+
rel = (rel_path or "").strip().replace("\\", "/").lstrip("/")
|
|
254
|
+
if ".." in rel.split("/"):
|
|
255
|
+
raise ValueError("path traversal not allowed")
|
|
256
|
+
target = (root / rel).resolve()
|
|
257
|
+
if not str(target).startswith(str(root)):
|
|
258
|
+
raise ValueError("path outside workspace")
|
|
259
|
+
if not target.is_dir():
|
|
260
|
+
raise ValueError(f"not a directory: {rel_path}")
|
|
261
|
+
|
|
262
|
+
globs = _parse_globs(pattern)
|
|
263
|
+
limit = max_files if max_files is not None else _dir_max_files()
|
|
264
|
+
found: list[Path] = []
|
|
265
|
+
truncated = False
|
|
266
|
+
for dirpath, _, filenames in os.walk(target):
|
|
267
|
+
for name in sorted(filenames):
|
|
268
|
+
if not _matches_globs(name, globs):
|
|
269
|
+
continue
|
|
270
|
+
p = Path(dirpath) / name
|
|
271
|
+
if not p.is_file():
|
|
272
|
+
continue
|
|
273
|
+
found.append(p.resolve())
|
|
274
|
+
if len(found) >= limit:
|
|
275
|
+
truncated = True
|
|
276
|
+
return found, truncated
|
|
277
|
+
return found, truncated
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _parse_globs(pattern: Optional[str]) -> tuple[str, ...]:
|
|
281
|
+
if pattern and pattern.strip():
|
|
282
|
+
return tuple(g.strip() for g in pattern.split(",") if g.strip())
|
|
283
|
+
raw = ca_env.pick_default(
|
|
284
|
+
",".join(_DEFAULT_IMAGE_GLOBS),
|
|
285
|
+
"CODEAGENT_IMAGE_DIR_ALLOWED_GLOB",
|
|
286
|
+
)
|
|
287
|
+
return tuple(g.strip() for g in raw.split(",") if g.strip())
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _matches_globs(name: str, globs: tuple[str, ...]) -> bool:
|
|
291
|
+
low = name.lower()
|
|
292
|
+
for g in globs:
|
|
293
|
+
if fnmatch.fnmatch(low, g.lower()):
|
|
294
|
+
return True
|
|
295
|
+
return False
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def message_has_image_attachments(msg: dict[str, Any]) -> bool:
|
|
299
|
+
atts = msg.get("attachments")
|
|
300
|
+
if isinstance(atts, list):
|
|
301
|
+
for a in atts:
|
|
302
|
+
if isinstance(a, dict) and a.get("kind") == "image":
|
|
303
|
+
return True
|
|
304
|
+
if IMAGE_DIR_TAG_RE.search(str(msg.get("content") or "")):
|
|
305
|
+
return True
|
|
306
|
+
return False
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def message_has_video_attachments(msg: dict[str, Any]) -> bool:
|
|
310
|
+
atts = msg.get("attachments")
|
|
311
|
+
if isinstance(atts, list):
|
|
312
|
+
for a in atts:
|
|
313
|
+
if isinstance(a, dict) and a.get("kind") == "video":
|
|
314
|
+
return True
|
|
315
|
+
return False
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def message_has_audio_attachments(msg: dict[str, Any]) -> bool:
|
|
319
|
+
atts = msg.get("attachments")
|
|
320
|
+
if isinstance(atts, list):
|
|
321
|
+
for a in atts:
|
|
322
|
+
if isinstance(a, dict) and a.get("kind") == "audio":
|
|
323
|
+
return True
|
|
324
|
+
return False
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def collect_attachment_ids_from_message(msg: dict[str, Any]) -> list[str]:
|
|
328
|
+
ids: list[str] = []
|
|
329
|
+
atts = msg.get("attachments")
|
|
330
|
+
if isinstance(atts, list):
|
|
331
|
+
for a in atts:
|
|
332
|
+
if isinstance(a, dict) and a.get("id"):
|
|
333
|
+
ids.append(str(a["id"]))
|
|
334
|
+
for m in ATTACHMENT_TAG_RE.finditer(str(msg.get("content") or "")):
|
|
335
|
+
ids.append(m.group(1))
|
|
336
|
+
seen: set[str] = set()
|
|
337
|
+
out: list[str] = []
|
|
338
|
+
for i in ids:
|
|
339
|
+
if i not in seen:
|
|
340
|
+
seen.add(i)
|
|
341
|
+
out.append(i)
|
|
342
|
+
return out
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Audio transcription preset resolution for audio_transcribe tool."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
from codeagent.core import env as ca_env
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def list_audio_presets() -> list[dict[str, Any]]:
|
|
11
|
+
from seed.core.llm_presets import load_presets
|
|
12
|
+
|
|
13
|
+
return [p for p in load_presets() if preset_supports_audio(p)]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def preset_supports_audio(preset: dict[str, Any] | None) -> bool:
|
|
17
|
+
if not isinstance(preset, dict):
|
|
18
|
+
return False
|
|
19
|
+
return preset.get("supports_audio") is True
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def preset_supports_audio_id(preset_id: str) -> bool:
|
|
23
|
+
from seed.core.llm_presets import load_presets
|
|
24
|
+
|
|
25
|
+
pid = (preset_id or "").strip()
|
|
26
|
+
for p in load_presets():
|
|
27
|
+
if str(p.get("id") or "").strip() == pid:
|
|
28
|
+
return preset_supports_audio(p)
|
|
29
|
+
return False
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def resolve_audio_preset(preset_id: Optional[str] = None) -> dict[str, Any]:
|
|
33
|
+
from seed.core.llm_presets import load_presets
|
|
34
|
+
|
|
35
|
+
pid = (preset_id or "").strip()
|
|
36
|
+
if not pid:
|
|
37
|
+
pid = ca_env.pick_default("", "CODEAGENT_AUDIO_PRESET_ID").strip()
|
|
38
|
+
if not pid:
|
|
39
|
+
presets = list_audio_presets()
|
|
40
|
+
if len(presets) == 1:
|
|
41
|
+
return dict(presets[0])
|
|
42
|
+
raise ValueError(
|
|
43
|
+
"audio preset required: set CODEAGENT_AUDIO_PRESET_ID or Web UI audio_llm_id"
|
|
44
|
+
)
|
|
45
|
+
for p in load_presets():
|
|
46
|
+
if str(p.get("id") or "").strip() == pid:
|
|
47
|
+
if preset_supports_audio(p):
|
|
48
|
+
return dict(p)
|
|
49
|
+
raise ValueError(f"preset {pid} does not support audio transcription")
|
|
50
|
+
raise ValueError(f"audio preset not found: {pid}")
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""Code Agent process bootstrap (product home → config → Seed bridge)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from codeagent.core.env import apply_default_product_home
|
|
11
|
+
from codeagent.core.seed_bridge import bridge_codeagent_env_to_seed
|
|
12
|
+
from seed.integrations.env_config import apply_seed_env_from_config
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
_CODEAGENT_BOOTSTRAP_MD = """\
|
|
17
|
+
# CodeAgent 首次启动引导
|
|
18
|
+
|
|
19
|
+
首次使用请按以下顺序完成初始化。
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 1. 环境变量
|
|
24
|
+
|
|
25
|
+
复制模板并填写 LLM 信息:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
cp config/env.example config/env
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
最低必填项(编辑 `config/env`):
|
|
32
|
+
|
|
33
|
+
| 变量 | 说明 |
|
|
34
|
+
|------|------|
|
|
35
|
+
| `SEED_LLM_BASEURL` | LLM API 地址(OpenAI 格式) |
|
|
36
|
+
| `SEED_LLM_MODEL` | 模型名称 |
|
|
37
|
+
| `SEED_LLM_API_KEY` | API 密钥 |
|
|
38
|
+
|
|
39
|
+
系统环境变量优先级 > env 文件。
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 2. Agent 人格配置
|
|
44
|
+
|
|
45
|
+
Agent 行为由 `agents/default/persona/` 下的 Markdown 文件定义:
|
|
46
|
+
|
|
47
|
+
| 文件 | 作用 |
|
|
48
|
+
|------|------|
|
|
49
|
+
| `identity.md` | 角色定位 |
|
|
50
|
+
| `soul.md` | 核心价值观与行为准则 |
|
|
51
|
+
| `agent.md` | 工作流程与项目管理规范 |
|
|
52
|
+
| `skills.md` | 核心工作流 |
|
|
53
|
+
| `tools.md` | 工具选用规则 |
|
|
54
|
+
| `user.md` | 用户偏好 |
|
|
55
|
+
|
|
56
|
+
按需修改即可调整 Agent 风格。
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## 3. 首次验证
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# Web UI(推荐)
|
|
64
|
+
codeagent web
|
|
65
|
+
|
|
66
|
+
# 或 CLI 模式
|
|
67
|
+
codeagent chat
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
确认 LLM 能正常响应、会话日志写入正常。
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## 4. (可选)Git 版本管理
|
|
75
|
+
|
|
76
|
+
对配置进行版本控制,方便回溯:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
cd ~/.codeagent
|
|
80
|
+
git init
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
创建 `.gitignore` 忽略运行数据:
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
agents/default/sessions/
|
|
87
|
+
agents/default/projects-data/
|
|
88
|
+
agents/default/memory/
|
|
89
|
+
mcp-minimax-out/
|
|
90
|
+
config/env
|
|
91
|
+
config/seed.env
|
|
92
|
+
config/webui.token
|
|
93
|
+
config/codeagent.webui.token
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
首次提交:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
git add .gitignore agents/default/persona/ agents/default/skills/ config/
|
|
100
|
+
git commit -m "chore: init codeagent config"
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## 目录结构
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
~/.codeagent/
|
|
109
|
+
├── config/ # 全局配置(env、cron、MCP、LSP、hooks)
|
|
110
|
+
├── agents/default/
|
|
111
|
+
│ ├── persona/ # Agent 人格定义(修改这里)
|
|
112
|
+
│ ├── skills/ # 自定义技能
|
|
113
|
+
│ ├── sessions/ # 对话历史(自动生成)
|
|
114
|
+
│ ├── memory/ # 长期记忆(自动生成)
|
|
115
|
+
│ └── projects-data/ # 项目记忆(自动生成)
|
|
116
|
+
├── mcp-minimax-out/ # MCP 工具缓存
|
|
117
|
+
└── bootstrap.md # ← 本文件
|
|
118
|
+
```
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _ensure_codeagent_bootstrap_md(home: Path) -> None:
|
|
123
|
+
"""Overwrite Seed's generic bootstrap.md with CodeAgent's own version."""
|
|
124
|
+
dest = home / "config" / "bootstrap.md"
|
|
125
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
126
|
+
# Always overwrite: CodeAgent's bootstrap is the authoritative one.
|
|
127
|
+
dest.write_text(_CODEAGENT_BOOTSTRAP_MD, encoding="utf-8")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def bootstrap_codeagent_runtime(base: Optional[Path] = None) -> Path:
|
|
131
|
+
"""
|
|
132
|
+
1. Default data root ``~/.codeagent`` (or ``CODEAGENT_HOME``)
|
|
133
|
+
2. Set ``SEED_PROJECT_ROOT`` so Seed / seed-tools use the same tree
|
|
134
|
+
3. Load ``config/env`` (or legacy ``config/seed.env``) + ``config/codeagent.env``
|
|
135
|
+
4. Bridge ``CODEAGENT_*`` → ``SEED_*`` for kernel env keys
|
|
136
|
+
5. Write CodeAgent's bootstrap.md (overrides Seed's generic version)
|
|
137
|
+
"""
|
|
138
|
+
if base is not None:
|
|
139
|
+
home = base.resolve()
|
|
140
|
+
os.environ["CODEAGENT_HOME"] = str(home)
|
|
141
|
+
os.environ["SEED_PROJECT_ROOT"] = str(home)
|
|
142
|
+
home.mkdir(parents=True, exist_ok=True)
|
|
143
|
+
(home / "config").mkdir(parents=True, exist_ok=True)
|
|
144
|
+
else:
|
|
145
|
+
home = apply_default_product_home()
|
|
146
|
+
|
|
147
|
+
apply_seed_env_from_config(None)
|
|
148
|
+
bridge_codeagent_env_to_seed()
|
|
149
|
+
|
|
150
|
+
from codeagent.runtime.compact_prompt import default_summarizer_prompt_path
|
|
151
|
+
|
|
152
|
+
if default_summarizer_prompt_path().is_file():
|
|
153
|
+
os.environ.setdefault(
|
|
154
|
+
"CODEAGENT_CONTEXT_COMPACT_SUMMARIZER_PROMPT_FILE",
|
|
155
|
+
str(default_summarizer_prompt_path()),
|
|
156
|
+
)
|
|
157
|
+
os.environ.setdefault("CODEAGENT_CONTEXT_COMPACT_MID_LOOP", "1")
|
|
158
|
+
os.environ.setdefault("CODEAGENT_CONTEXT_COMPACT_KEEP_USER_ROUNDS", "2")
|
|
159
|
+
bridge_codeagent_env_to_seed()
|
|
160
|
+
|
|
161
|
+
_ensure_codeagent_bootstrap_md(home)
|
|
162
|
+
|
|
163
|
+
# ── Team config: load if exists ──
|
|
164
|
+
try:
|
|
165
|
+
team_cfg_path = home / "config" / "team.json"
|
|
166
|
+
if team_cfg_path.is_file():
|
|
167
|
+
from codeagent.core.team_manager import TeamManager
|
|
168
|
+
tm = TeamManager()
|
|
169
|
+
if tm.init_from_config(team_cfg_path):
|
|
170
|
+
logger.info(f"Team mode enabled: {len(tm.config.members)} member agents configured")
|
|
171
|
+
else:
|
|
172
|
+
logger.info("Single-agent mode (team config invalid or empty)")
|
|
173
|
+
else:
|
|
174
|
+
logger.info("Single-agent mode (no team.json found)")
|
|
175
|
+
except Exception:
|
|
176
|
+
logger.exception("Team config init failed — continuing in single-agent mode")
|
|
177
|
+
|
|
178
|
+
return home
|