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,98 @@
1
+ """Web UI: plugin toggles and which config/*.md files feed the system prompt."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import logging
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from seed.core.config_plane import CONFIG_FILENAMES, config_dir
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ PLUGINS_FILENAME = "codeagent.plugins.json"
14
+
15
+
16
+ def _defaults() -> dict[str, Any]:
17
+ return {
18
+ "config_md_enabled": list(CONFIG_FILENAMES),
19
+ "plugins": {},
20
+ }
21
+
22
+
23
+ def _plugins_path(base: Path | None = None) -> Path:
24
+ cfg = config_dir() if base is None else base.resolve() / "config"
25
+ return cfg / PLUGINS_FILENAME
26
+
27
+
28
+ def load_plugins(base: Path | None = None) -> dict[str, Any]:
29
+ d = _defaults()
30
+ path = _plugins_path(base)
31
+ if not path.is_file():
32
+ return d
33
+ try:
34
+ raw = json.loads(path.read_text(encoding="utf-8"))
35
+ except (json.JSONDecodeError, OSError) as e:
36
+ logger.warning("ignore bad %s: %s", path, e)
37
+ return d
38
+ if not isinstance(raw, dict):
39
+ return d
40
+ if isinstance(raw.get("config_md_enabled"), list):
41
+ sel = [
42
+ x
43
+ for x in raw["config_md_enabled"]
44
+ if isinstance(x, str) and x in CONFIG_FILENAMES
45
+ ]
46
+ if sel:
47
+ d["config_md_enabled"] = sel
48
+ pl = raw.get("plugins")
49
+ if isinstance(pl, dict):
50
+ d["plugins"] = pl
51
+ return d
52
+
53
+
54
+ def save_plugins(data: dict[str, Any], base: Path | None = None) -> None:
55
+ path = _plugins_path(base)
56
+ path.parent.mkdir(parents=True, exist_ok=True)
57
+ path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
58
+
59
+ def get_system_prompt_filenames(base: Path | None = None) -> list[str]:
60
+ data = load_plugins(base) if base is not None else load_plugins()
61
+ enabled = data.get("config_md_enabled") or list(CONFIG_FILENAMES)
62
+ if not enabled:
63
+ return list(CONFIG_FILENAMES)
64
+ s = set(enabled)
65
+ ordered = [f for f in CONFIG_FILENAMES if f in s]
66
+ if not ordered:
67
+ return list(CONFIG_FILENAMES)
68
+ return ordered
69
+
70
+
71
+ def get_tool_exclude_prefixes() -> tuple[str, ...]:
72
+ return ()
73
+
74
+
75
+ def plugins_public_view() -> dict[str, Any]:
76
+ d = load_plugins()
77
+ en = set(d.get("config_md_enabled") or CONFIG_FILENAMES)
78
+ return {
79
+ "config_filenames": list(CONFIG_FILENAMES),
80
+ "config_md": [{"name": f, "enabled": f in en} for f in CONFIG_FILENAMES],
81
+ "plugins": d.get("plugins", _defaults()["plugins"]),
82
+ }
83
+
84
+
85
+ def save_plugins_from_ui(body: dict[str, Any]) -> dict[str, Any]:
86
+ current = load_plugins()
87
+ enabled = body.get("config_md_enabled")
88
+ if enabled is not None:
89
+ if not isinstance(enabled, list):
90
+ raise ValueError("config_md_enabled must be a list of filenames")
91
+ sel = [x for x in enabled if isinstance(x, str) and x in CONFIG_FILENAMES]
92
+ if not sel:
93
+ sel = list(CONFIG_FILENAMES)
94
+ current["config_md_enabled"] = sel
95
+ pl = body.get("plugins")
96
+ if isinstance(pl, dict):
97
+ current["plugins"] = pl
98
+ save_plugins(current)
@@ -0,0 +1,330 @@
1
+ """MiniMax text-to-speech (HTTP T2A) for Web UI bubble playback."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import os
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+ from typing import Optional, Tuple
10
+
11
+ import requests
12
+
13
+ from seed.integrations.mcp_config import MINIMAX_MCP_SERVER_ID, get_server_config
14
+
15
+ from codeagent.core.tts_voices import (
16
+ default_tts_model,
17
+ default_tts_voice_id,
18
+ normalize_tts_model,
19
+ normalize_tts_voice_id,
20
+ )
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+ _QUOTA_STATUS_CODES = frozenset({1002, 1008, 2056})
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class TtsSynthOutcome:
29
+ audio: Optional[bytes] = None
30
+ mime: str = "audio/mpeg"
31
+ error: str = ""
32
+ status_code: Optional[int] = None
33
+
34
+ @property
35
+ def ok(self) -> bool:
36
+ return bool(self.audio)
37
+
38
+
39
+ def _base_resp_status_code(base_resp: object) -> tuple[int, str]:
40
+ if not isinstance(base_resp, dict):
41
+ return 0, ""
42
+ raw_code = base_resp.get("status_code", 0)
43
+ try:
44
+ code = int(raw_code)
45
+ except (TypeError, ValueError):
46
+ code = 0
47
+ msg = str(base_resp.get("status_msg") or "").strip()
48
+ return code, msg
49
+
50
+
51
+ def _decode_t2a_audio(raw_audio: object, *, timeout: int) -> tuple[Optional[bytes], str]:
52
+ if raw_audio is None:
53
+ return None, ""
54
+ audio_str = str(raw_audio).strip()
55
+ if not audio_str:
56
+ return None, ""
57
+ if audio_str.startswith("http://") or audio_str.startswith("https://"):
58
+ try:
59
+ r = requests.get(audio_str, timeout=max(10, timeout))
60
+ r.raise_for_status()
61
+ mime = r.headers.get("Content-Type") or "audio/mpeg"
62
+ return r.content, str(mime).split(";")[0].strip() or "audio/mpeg"
63
+ except Exception:
64
+ logger.debug("MiniMax TTS audio URL download failed", exc_info=True)
65
+ return None, ""
66
+ try:
67
+ audio_bytes = bytes.fromhex(audio_str)
68
+ except ValueError:
69
+ logger.debug("MiniMax TTS invalid hex audio", exc_info=True)
70
+ return None, ""
71
+ if not audio_bytes:
72
+ return None, ""
73
+ return audio_bytes, "audio/mpeg"
74
+
75
+
76
+ def synthesize_minimax_speech_with_detail(
77
+ text: str,
78
+ base: Optional[Path] = None,
79
+ *,
80
+ voice_id: Optional[str] = None,
81
+ model: Optional[str] = None,
82
+ ) -> TtsSynthOutcome:
83
+ """Return structured TTS result with MiniMax error details when synthesis fails."""
84
+ key, host = get_minimax_tts_credentials(base)
85
+ if not key:
86
+ return TtsSynthOutcome(error="MiniMax TTS not configured")
87
+
88
+ raw = (text or "").strip()
89
+ if not raw:
90
+ return TtsSynthOutcome(error="text required")
91
+ raw = raw[: _max_tts_chars()]
92
+
93
+ model_id = normalize_tts_model(model)
94
+ voice = normalize_tts_voice_id(voice_id)
95
+
96
+ payload = {
97
+ "model": model_id,
98
+ "text": raw,
99
+ "stream": False,
100
+ "output_format": "hex",
101
+ "language_boost": "auto",
102
+ "voice_setting": {
103
+ "voice_id": voice,
104
+ "speed": 1,
105
+ "vol": 1,
106
+ "pitch": 0,
107
+ },
108
+ "audio_setting": {
109
+ "sample_rate": 32000,
110
+ "bitrate": 128000,
111
+ "format": "mp3",
112
+ "channel": 1,
113
+ },
114
+ }
115
+
116
+ try:
117
+ timeout = int(os.environ.get("CODEAGENT_TTS_TIMEOUT_SEC", "120") or 120)
118
+ except ValueError:
119
+ timeout = 120
120
+
121
+ try:
122
+ resp = requests.post(
123
+ _t2a_url(host),
124
+ json=payload,
125
+ headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"},
126
+ timeout=max(10, timeout),
127
+ )
128
+ resp.raise_for_status()
129
+ body = resp.json()
130
+ except requests.HTTPError as e:
131
+ detail = ""
132
+ try:
133
+ err_body = e.response.json() if e.response is not None else {}
134
+ br = err_body.get("base_resp") if isinstance(err_body, dict) else None
135
+ code, msg = _base_resp_status_code(br)
136
+ detail = msg or str(e)
137
+ if code:
138
+ return TtsSynthOutcome(error=detail or f"MiniMax TTS HTTP error ({code})", status_code=code)
139
+ except Exception:
140
+ detail = str(e)
141
+ logger.warning("MiniMax TTS HTTP error: %s", detail or e)
142
+ return TtsSynthOutcome(error=detail or "MiniMax TTS request failed")
143
+ except Exception as e:
144
+ logger.warning("MiniMax TTS request failed: %s", e, exc_info=True)
145
+ return TtsSynthOutcome(error=str(e) or "MiniMax TTS request failed")
146
+
147
+ if not isinstance(body, dict):
148
+ return TtsSynthOutcome(error="MiniMax TTS returned invalid JSON")
149
+
150
+ code, msg = _base_resp_status_code(body.get("base_resp"))
151
+ if code != 0:
152
+ logger.info("MiniMax TTS base_resp error: %s", body.get("base_resp"))
153
+ return TtsSynthOutcome(
154
+ error=_humanize_tts_error(code, msg, key=key),
155
+ status_code=code,
156
+ )
157
+
158
+ data = body.get("data")
159
+ if not isinstance(data, dict):
160
+ return TtsSynthOutcome(error="MiniMax TTS returned no data")
161
+
162
+ status = data.get("status")
163
+ if status is not None:
164
+ try:
165
+ if int(status) != 2:
166
+ return TtsSynthOutcome(
167
+ error=f"MiniMax TTS incomplete (status={status})",
168
+ status_code=code,
169
+ )
170
+ except (TypeError, ValueError):
171
+ pass
172
+
173
+ audio_bytes, mime = _decode_t2a_audio(data.get("audio"), timeout=timeout)
174
+ if not audio_bytes:
175
+ return TtsSynthOutcome(error=msg or "MiniMax TTS returned no audio", status_code=code)
176
+ return TtsSynthOutcome(audio=audio_bytes, mime=mime or "audio/mpeg")
177
+
178
+
179
+ def synthesize_minimax_speech(
180
+ text: str,
181
+ base: Optional[Path] = None,
182
+ *,
183
+ voice_id: Optional[str] = None,
184
+ model: Optional[str] = None,
185
+ ) -> Optional[Tuple[bytes, str]]:
186
+ """Return (mp3 bytes, mime) or None when MiniMax TTS is not configured."""
187
+ out = synthesize_minimax_speech_with_detail(
188
+ text,
189
+ base,
190
+ voice_id=voice_id,
191
+ model=model,
192
+ )
193
+ if not out.ok or out.audio is None:
194
+ return None
195
+ return out.audio, out.mime
196
+
197
+
198
+ def tts_http_status_for_outcome(out: TtsSynthOutcome) -> int:
199
+ code = out.status_code
200
+ if code in _QUOTA_STATUS_CODES:
201
+ return 429
202
+ if code in {1004, 2049}:
203
+ return 401
204
+ if code in {2013, 1042}:
205
+ return 422
206
+ return 502
207
+
208
+
209
+ def _max_tts_chars() -> int:
210
+ try:
211
+ return max(1, min(int(os.environ.get("CODEAGENT_TTS_MAX_CHARS", "2000") or 2000), 10000))
212
+ except ValueError:
213
+ return 2000
214
+
215
+
216
+ def _tts_api_key_from_env() -> str:
217
+ from codeagent.core import env as ca_env
218
+
219
+ return ca_env.pick_nonempty("CODEAGENT_TTS_API_KEY")
220
+
221
+
222
+ def _tts_api_key_from_mcp(base: Optional[Path] = None) -> tuple[str, str]:
223
+ cfg = get_server_config(MINIMAX_MCP_SERVER_ID, base)
224
+ if not cfg:
225
+ return "", ""
226
+ env = cfg.env or {}
227
+ key = str(env.get("MINIMAX_TTS_API_KEY") or "").strip()
228
+ if not key:
229
+ return "", ""
230
+ host = str(env.get("MINIMAX_TTS_API_HOST") or env.get("MINIMAX_API_HOST") or "https://api.minimaxi.com")
231
+ return key, host.strip().rstrip("/")
232
+
233
+
234
+ def _humanize_tts_error(code: Optional[int], msg: str, *, key: str = "") -> str:
235
+ base = (msg or "").strip() or (f"MiniMax TTS error (status_code={code})" if code else "MiniMax TTS failed")
236
+ if code != 2056:
237
+ return base
238
+ if "0/0" in base or "Token Plan" in base:
239
+ hint = (
240
+ "Token Plan Key(sk-cp-…)通常可用于聊天,但 TTS(Speech)有独立的每日字符额度;"
241
+ "当前返回 0/0 表示套餐未分配朗读额度,并非「已用完」。"
242
+ "请在 MiniMax 开放平台「账户管理 → 接口密钥」创建按量 API Key,"
243
+ "填到配置页「朗读 API Key(开放平台按量,可选)」后重试。"
244
+ )
245
+ if key.startswith("sk-cp-"):
246
+ return f"{base}。{hint}"
247
+ return f"{base}。{hint}"
248
+ return base
249
+
250
+
251
+ def minimax_tts_configured(base: Optional[Path] = None) -> bool:
252
+ key, _host = get_minimax_tts_credentials(base)
253
+ return bool(key)
254
+
255
+
256
+ def get_minimax_tts_credentials(base: Optional[Path] = None) -> Tuple[str, str]:
257
+ """TTS credentials: dedicated pay-as-you-go key first, then MCP / LLM preset."""
258
+ tts_key = _tts_api_key_from_env()
259
+ if tts_key:
260
+ host = _host_from_preset_base_url(
261
+ os.environ.get("CODEAGENT_TTS_API_HOST", "https://api.minimaxi.com")
262
+ )
263
+ return tts_key, host
264
+
265
+ mcp_tts_key, mcp_tts_host = _tts_api_key_from_mcp(base)
266
+ if mcp_tts_key:
267
+ return mcp_tts_key, mcp_tts_host
268
+
269
+ cfg = get_server_config(MINIMAX_MCP_SERVER_ID, base)
270
+ if cfg:
271
+ env = cfg.env or {}
272
+ key = str(env.get("MINIMAX_API_KEY") or "").strip()
273
+ if key:
274
+ host = str(env.get("MINIMAX_API_HOST") or "https://api.minimaxi.com").strip().rstrip("/")
275
+ return key, host
276
+ return _minimax_credentials_from_llm_presets(base)
277
+
278
+
279
+ def _host_from_preset_base_url(base_url: str) -> str:
280
+ raw = (base_url or "").strip().rstrip("/")
281
+ if not raw:
282
+ return "https://api.minimaxi.com"
283
+ if raw.endswith("/v1/t2a_v2"):
284
+ return raw[: -len("/v1/t2a_v2")]
285
+ if raw.endswith("/v1"):
286
+ return raw[: -len("/v1")]
287
+ return raw
288
+
289
+
290
+ def _minimax_credentials_from_llm_presets(base: Optional[Path] = None) -> Tuple[str, str]:
291
+ """Use MiniMax LLM preset api_key (e.g. MiniMax-M2.7 chat) for T2A — same platform key."""
292
+ try:
293
+ from seed.core.llm_presets import get_default_preset_id, load_presets
294
+ from seed_model_providers import resolve_provider_for_preset
295
+ except Exception:
296
+ return "", "https://api.minimaxi.com"
297
+
298
+ default_id = get_default_preset_id().strip()
299
+ ranked: list[tuple[int, str, str, str]] = []
300
+ for p in load_presets():
301
+ if resolve_provider_for_preset(p) != "minimax":
302
+ continue
303
+ key = str(p.get("api_key") or "").strip()
304
+ if not key:
305
+ continue
306
+ pid = str(p.get("id") or "").strip()
307
+ host = _host_from_preset_base_url(str(p.get("base_url") or ""))
308
+ use_type = str(p.get("use_type") or "").strip().lower()
309
+ if pid and pid == default_id:
310
+ rank = 0
311
+ elif use_type in ("", "chat") or "M2" in str(p.get("model") or ""):
312
+ rank = 1
313
+ else:
314
+ rank = 2
315
+ ranked.append((rank, pid, key, host))
316
+
317
+ if not ranked:
318
+ return "", "https://api.minimaxi.com"
319
+ ranked.sort(key=lambda x: (x[0], x[1]))
320
+ _rank, _pid, key, host = ranked[0]
321
+ return key, host
322
+
323
+
324
+ def _t2a_url(host: str) -> str:
325
+ base = (host or "").strip().rstrip("/")
326
+ if base.endswith("/v1/t2a_v2"):
327
+ return base
328
+ if base.endswith("/v1"):
329
+ return f"{base}/t2a_v2"
330
+ return f"{base}/v1/t2a_v2"
@@ -0,0 +1,247 @@
1
+ """Team configuration — parse .codeagent/config/team.json into typed data objects.
2
+
3
+ Usage::
4
+ config = TeamConfig.from_file(".codeagent/config/team.json")
5
+ lead = config.lead
6
+ for member in config.members:
7
+ print(member.id, member.name)
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import logging
14
+ from dataclasses import dataclass, field
15
+ from pathlib import Path
16
+ from typing import Any, Dict, List, Optional
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ # Default team tools auto-injected into Lead
21
+ TEAM_TOOLS = ["call_agent", "dispatch", "parallel"]
22
+
23
+
24
+ @dataclass
25
+ class AgentConfig:
26
+ """Configuration for a single agent (lead or member)."""
27
+
28
+ id: str
29
+ name: str
30
+ persona: str
31
+ tools: List[str] = field(default_factory=list)
32
+ metadata: Dict[str, Any] = field(default_factory=dict)
33
+
34
+ @classmethod
35
+ def from_dict(cls, d: dict) -> AgentConfig:
36
+ """Create from parsed JSON dict."""
37
+ return cls(
38
+ id=d["id"],
39
+ name=d.get("name", d["id"]),
40
+ persona=d.get("persona", ""),
41
+ tools=d.get("tools", []),
42
+ metadata=d.get("metadata", {}),
43
+ )
44
+
45
+
46
+ @dataclass
47
+ class TeamConfig:
48
+ """Complete team configuration parsed from team.json."""
49
+
50
+ version: str = "1.0"
51
+ lead: Optional[AgentConfig] = None
52
+ members: List[AgentConfig] = field(default_factory=list)
53
+ metadata: Dict[str, Any] = field(default_factory=dict)
54
+
55
+ @classmethod
56
+ def from_file(cls, path: Path | str) -> TeamConfig:
57
+ """Parse a team.json file into a TeamConfig instance.
58
+
59
+ Args:
60
+ path: Path to team.json file.
61
+
62
+ Returns:
63
+ TeamConfig instance.
64
+
65
+ Raises:
66
+ FileNotFoundError: If the file doesn't exist.
67
+ ValueError: If the file is invalid JSON or missing required fields.
68
+ """
69
+ path = Path(path)
70
+ if not path.is_file():
71
+ raise FileNotFoundError(f"Team config not found: {path}")
72
+
73
+ try:
74
+ data = json.loads(path.read_text(encoding="utf-8"))
75
+ except json.JSONDecodeError as e:
76
+ raise ValueError(f"Invalid JSON in team config: {e}")
77
+
78
+ return cls.from_dict(data, config_dir=path.parent)
79
+
80
+ @classmethod
81
+ def from_dict(cls, data: dict, config_dir: Optional[Path] = None) -> TeamConfig:
82
+ """Create from parsed JSON dict, with optional base dir for persona paths."""
83
+ version = data.get("version", "1.0")
84
+ metadata = data.get("metadata", {})
85
+
86
+ lead_data = data.get("lead")
87
+ if not lead_data:
88
+ raise ValueError("team.json: 'lead' is required")
89
+
90
+ lead = AgentConfig.from_dict(lead_data)
91
+ # Auto-inject team tools to Lead
92
+ for tool in TEAM_TOOLS:
93
+ if tool not in lead.tools:
94
+ lead.tools.append(tool)
95
+
96
+ # Resolve lead persona path
97
+ lead.persona = _resolve_path(lead.persona, config_dir)
98
+
99
+ members = []
100
+ for m in data.get("members", []):
101
+ agent = AgentConfig.from_dict(m)
102
+ agent.persona = _resolve_path(agent.persona, config_dir)
103
+ members.append(agent)
104
+
105
+ config = cls(version=version, lead=lead, members=members, metadata=metadata)
106
+ config._validate()
107
+ return config
108
+
109
+ def _validate(self) -> None:
110
+ """Validate config for required fields and consistency."""
111
+ errors: List[str] = []
112
+
113
+ if not self.lead:
114
+ errors.append("Lead agent is required")
115
+
116
+ # Check for duplicate IDs
117
+ ids = [self.lead.id] if self.lead else []
118
+ for m in self.members:
119
+ if m.id in ids:
120
+ errors.append(f"Duplicate agent id: {m.id!r}")
121
+ ids.append(m.id)
122
+
123
+ # Check persona files exist (warn, not error — they'll use defaults)
124
+ all_agents = [self.lead] + self.members if self.lead else self.members
125
+ for agent in all_agents:
126
+ if agent.persona:
127
+ p = Path(agent.persona)
128
+ if not p.is_file():
129
+ logger.warning(f"Persona file not found for '{agent.id}': {agent.persona} — will use defaults")
130
+
131
+ if errors:
132
+ raise ValueError("Invalid team config:\n" + "\n".join(errors))
133
+
134
+ def as_dict(self) -> dict:
135
+ """Serialize back to dict (for debugging / display)."""
136
+ return {
137
+ "version": self.version,
138
+ "lead": {
139
+ "id": self.lead.id,
140
+ "name": self.lead.name,
141
+ "persona": self.lead.persona,
142
+ "tools": self.lead.tools,
143
+ } if self.lead else None,
144
+ "members": [
145
+ {
146
+ "id": m.id,
147
+ "name": m.name,
148
+ "persona": m.persona,
149
+ "tools": m.tools,
150
+ }
151
+ for m in self.members
152
+ ],
153
+ "metadata": self.metadata,
154
+ }
155
+
156
+ def to_json(self, indent: int = 2) -> str:
157
+ """Serialize to JSON string."""
158
+ return json.dumps(self.as_dict(), indent=indent, ensure_ascii=False)
159
+
160
+
161
+ def _resolve_path(persona_path: str, config_dir: Optional[Path]) -> str:
162
+ """Resolve a persona path relative to config dir if it's a relative path."""
163
+ if not persona_path or not config_dir:
164
+ return persona_path
165
+ p = Path(persona_path)
166
+ if not p.is_absolute():
167
+ resolved = (config_dir / p).resolve()
168
+ return str(resolved)
169
+ return persona_path
170
+
171
+
172
+ def create_default_team_config(config_dir: Path) -> TeamConfig:
173
+ """Create a default team.json with standard 8-member roles.
174
+
175
+ Args:
176
+ config_dir: The .codeagent/config directory path.
177
+
178
+ Returns:
179
+ A TeamConfig with default roles.
180
+ """
181
+ # Persona paths relative to config dir
182
+ def persona(name: str) -> str:
183
+ return str(config_dir / ".." / "personas" / f"{name}.md")
184
+
185
+ data = {
186
+ "version": "1.0",
187
+ "lead": {
188
+ "id": "lead",
189
+ "name": "Lead Developer",
190
+ "persona": persona("lead"),
191
+ "tools": [
192
+ "file_read", "file_write", "file_edit",
193
+ "bash", "git", "grep", "glob",
194
+ ],
195
+ },
196
+ "members": [
197
+ {
198
+ "id": "pm",
199
+ "name": "产品经理",
200
+ "persona": persona("pm"),
201
+ "tools": ["file_read", "file_write", "web_search"],
202
+ },
203
+ {
204
+ "id": "ux_designer",
205
+ "name": "UX 设计师",
206
+ "persona": persona("ux-designer"),
207
+ "tools": ["file_read", "file_write", "browser_screenshot"],
208
+ },
209
+ {
210
+ "id": "frontend_dev",
211
+ "name": "前端工程师",
212
+ "persona": persona("frontend"),
213
+ "tools": ["file_read", "file_write", "bash", "browser_screenshot"],
214
+ },
215
+ {
216
+ "id": "backend_dev",
217
+ "name": "后端工程师",
218
+ "persona": persona("backend"),
219
+ "tools": ["file_read", "file_write", "bash", "db"],
220
+ },
221
+ {
222
+ "id": "test_dev",
223
+ "name": "测试工程师",
224
+ "persona": persona("test"),
225
+ "tools": ["file_read", "bash", "test_run"],
226
+ },
227
+ {
228
+ "id": "reviewer",
229
+ "name": "代码审查员",
230
+ "persona": persona("reviewer"),
231
+ "tools": ["file_read", "code_check", "git"],
232
+ },
233
+ {
234
+ "id": "doc_writer",
235
+ "name": "文档工程师",
236
+ "persona": persona("doc-writer"),
237
+ "tools": ["file_read", "file_write", "bash"],
238
+ },
239
+ {
240
+ "id": "data_analyst",
241
+ "name": "数据分析师",
242
+ "persona": persona("data-analyst"),
243
+ "tools": ["file_read", "bash", "db"],
244
+ },
245
+ ],
246
+ }
247
+ return TeamConfig.from_dict(data, config_dir=config_dir)