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,238 @@
1
+ """CodeAgent filesystem layout — re-exports kernel paths + CodeAgent default persona files."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import contextlib
6
+ import logging
7
+ from pathlib import Path
8
+
9
+ from codeagent.core.env import product_home
10
+ from seed.core.config_plane import project_root as _kernel_project_root
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ def codeagent_home() -> Path:
16
+ """Product data root (``~/.codeagent``). Same as ``project_root()`` after bootstrap."""
17
+ return product_home()
18
+ from seed.core.paths import (
19
+ agent_archive_dir,
20
+ agent_daily_dir,
21
+ agent_home,
22
+ agent_id_default,
23
+ agent_memory_dir,
24
+ agent_persona_dir,
25
+ agent_persona_memory_path,
26
+ agent_project_archive_dir,
27
+ agent_project_daily_dir,
28
+ agent_project_data_dir,
29
+ agent_project_data_subdir,
30
+ agent_projects_data_dir,
31
+ agent_projects_registry_dir,
32
+ agent_skills_dir,
33
+ ensure_agent_dirs,
34
+ )
35
+
36
+
37
+ # ---------------------------------------------------------------------------
38
+ # Project-level working directories (.codeagent/{agent_id}/)
39
+ # ---------------------------------------------------------------------------
40
+
41
+ _PROJECT_SUBDIRS = ("docs", "plans", "scripts", "sessions", "cache", "tmp")
42
+
43
+
44
+ def _codeagent_dir(root: Path | None = None) -> Path:
45
+ """Return the ``.codeagent/`` directory under the given root (default: cwd)."""
46
+ base = Path(root).resolve() if root is not None else Path.cwd().resolve()
47
+ return base / ".codeagent"
48
+
49
+
50
+ def _agent_work_dir(agent_id: str, root: Path | None = None) -> Path:
51
+ """Return the agent work directory ``.codeagent/{agent_id}/``."""
52
+ return _codeagent_dir(root) / agent_id
53
+
54
+
55
+ def ensure_project_dirs(
56
+ agent_id: str | None = None,
57
+ root: Path | None = None,
58
+ ) -> Path:
59
+ """Create ``.codeagent/{agent_id}/*`` working directories under *root*.
60
+
61
+ Creates the full structure including global ``rules.md``, per-agent
62
+ ``rules.md`` and ``state.md``, and sub-directories for docs, plans,
63
+ scripts, sessions, cache, and tmp.
64
+
65
+ Also appends ``.codeagent/`` to the project root ``.gitignore`` if not
66
+ already present.
67
+
68
+ Returns the ``.codeagent/`` path.
69
+ """
70
+ from seed.core.paths import agent_id_default
71
+
72
+ base = Path(root).resolve() if root is not None else Path.cwd().resolve()
73
+ aid = (agent_id or "").strip() or agent_id_default()
74
+
75
+ ca_root = _codeagent_dir(base)
76
+ ca_root.mkdir(parents=True, exist_ok=True)
77
+
78
+ # Global rules placeholder
79
+ (ca_root / "rules.md").touch(exist_ok=True)
80
+
81
+ # Per-agent dirs
82
+ agent_dir = ca_root / aid
83
+ for sub in _PROJECT_SUBDIRS:
84
+ (agent_dir / sub).mkdir(parents=True, exist_ok=True)
85
+ (agent_dir / "rules.md").touch(exist_ok=True)
86
+ (agent_dir / "state.md").touch(exist_ok=True)
87
+
88
+ # Ensure project root .gitignore has .codeagent/ entry
89
+ _append_gitignore_entry(base)
90
+
91
+ return ca_root
92
+
93
+
94
+ def read_state_file(agent_id: str, root: Path | None = None) -> str:
95
+ """Read ``.codeagent/{agent_id}/state.md``, return empty string if missing."""
96
+ p = _agent_work_dir(agent_id, root) / "state.md"
97
+ if p.is_file():
98
+ return p.read_text(encoding="utf-8").strip()
99
+ return ""
100
+
101
+
102
+ def write_state_file(
103
+ content: str,
104
+ agent_id: str,
105
+ root: Path | None = None,
106
+ ) -> None:
107
+ """Write ``.codeagent/{agent_id}/state.md`` with the given content."""
108
+ p = _agent_work_dir(agent_id, root) / "state.md"
109
+ p.parent.mkdir(parents=True, exist_ok=True)
110
+ p.write_text(content.strip(), encoding="utf-8")
111
+
112
+
113
+ def read_global_rules(root: Path | None = None) -> str:
114
+ """Read ``.codeagent/rules.md``, return empty string if missing."""
115
+ p = _codeagent_dir(root) / "rules.md"
116
+ if p.is_file():
117
+ return p.read_text(encoding="utf-8").strip()
118
+ return ""
119
+
120
+
121
+ def read_agent_rules(agent_id: str, root: Path | None = None) -> str:
122
+ """Read ``.codeagent/{agent_id}/rules.md``, return empty string if missing."""
123
+ p = _agent_work_dir(agent_id, root) / "rules.md"
124
+ if p.is_file():
125
+ return p.read_text(encoding="utf-8").strip()
126
+ return ""
127
+
128
+
129
+ def _append_gitignore_entry(root: Path) -> None:
130
+ """Append ``.codeagent/`` to project root ``.gitignore`` if not already present."""
131
+ gi = root / ".gitignore"
132
+ line = ".codeagent/"
133
+ if gi.is_file():
134
+ content = gi.read_text(encoding="utf-8")
135
+ if line not in content:
136
+ gi.write_text(content.rstrip() + "\n" + line + "\n", encoding="utf-8")
137
+ else:
138
+ gi.write_text(line + "\n", encoding="utf-8")
139
+
140
+ # Default persona Markdown filenames (same as CONFIG_FILENAMES in config_plane).
141
+ _DEFAULT_PERSONA_FILENAMES = [
142
+ "agent.md",
143
+ "identity.md",
144
+ "soul.md",
145
+ "tools.md",
146
+ "skills.md",
147
+ "user.md",
148
+ ]
149
+ # memory.md is appended separately in fresh_system_prompt, not in CONFIG_FILENAMES.
150
+ _PERSONA_INIT_FILENAMES = (*_DEFAULT_PERSONA_FILENAMES, "memory.md")
151
+
152
+ _BUNDLED_PERSONA_DIR = Path(__file__).resolve().parent.parent / "persona_defaults"
153
+
154
+
155
+ def _bundled_persona_defaults_dir() -> Path | None:
156
+ """Return the shipped default persona template directory inside the package."""
157
+ if _BUNDLED_PERSONA_DIR.is_dir() and any(_BUNDLED_PERSONA_DIR.glob("*.md")):
158
+ return _BUNDLED_PERSONA_DIR
159
+ return None
160
+
161
+
162
+ def _read_bundled_persona_file(fname: str) -> str | None:
163
+ """Read one bundled persona markdown file (wheel-safe via importlib.resources)."""
164
+ try:
165
+ from importlib.resources import files as pkg_files
166
+
167
+ ref = pkg_files("codeagent") / "persona_defaults" / fname
168
+ if ref.is_file():
169
+ return ref.read_text(encoding="utf-8")
170
+ except Exception:
171
+ pass
172
+ bundled = _bundled_persona_defaults_dir()
173
+ if bundled is None:
174
+ return None
175
+ src = bundled / fname
176
+ if not src.is_file():
177
+ return None
178
+ try:
179
+ return src.read_text(encoding="utf-8")
180
+ except OSError:
181
+ return None
182
+
183
+
184
+ def _ensure_default_persona_files(persona_dir: Path) -> None:
185
+ """Create default persona *.md from bundled template if they do not exist."""
186
+ if _bundled_persona_defaults_dir() is None:
187
+ logger.warning(
188
+ "Bundled persona defaults missing; skipping persona init for %s",
189
+ persona_dir,
190
+ )
191
+ return
192
+ for fname in _PERSONA_INIT_FILENAMES:
193
+ p = persona_dir / fname
194
+ if p.is_file():
195
+ continue
196
+ body = _read_bundled_persona_file(fname)
197
+ if not body:
198
+ logger.warning("Bundled persona file missing: %s", fname)
199
+ continue
200
+ with contextlib.suppress(OSError):
201
+ p.write_text(body, encoding="utf-8")
202
+
203
+
204
+ def ensure_agent_scaffold(agent_id: str, base: Path | None = None) -> Path:
205
+ """Ensure agent dirs exist, persona files, and project working directories."""
206
+ root = Path(base).resolve() if base is not None else _kernel_project_root()
207
+ aid = (agent_id or "").strip() or agent_id_default()
208
+ home = ensure_agent_dirs(aid, base=root)
209
+ _ensure_default_persona_files(home / "persona")
210
+ ensure_project_dirs(aid, root=root)
211
+ return home
212
+
213
+
214
+ __all__ = [
215
+ "codeagent_home",
216
+ "agent_archive_dir",
217
+ "agent_daily_dir",
218
+ "agent_home",
219
+ "agent_id_default",
220
+ "agent_memory_dir",
221
+ "agent_persona_dir",
222
+ "agent_persona_memory_path",
223
+ "agent_project_archive_dir",
224
+ "agent_project_daily_dir",
225
+ "agent_project_data_dir",
226
+ "agent_project_data_subdir",
227
+ "agent_projects_data_dir",
228
+ "agent_projects_registry_dir",
229
+ "agent_skills_dir",
230
+ "ensure_agent_dirs",
231
+ "ensure_agent_scaffold",
232
+ # Project-level working directories
233
+ "ensure_project_dirs",
234
+ "read_state_file",
235
+ "write_state_file",
236
+ "read_global_rules",
237
+ "read_agent_rules",
238
+ ]
@@ -0,0 +1,107 @@
1
+ """
2
+ DeepSeek 模型定价表 & 费用计算。
3
+
4
+ 定价数据来源:https://api-docs.deepseek.com/zh-cn/quick_start/pricing
5
+ 当前 deepseek-v4-pro 模型 2.5 折,优惠期至 2026-05-31 23:59 (UTC+8)。
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any
11
+
12
+ # ── 定价表(元/百万 tokens) ─────────────────────────────────
13
+ # 模型名统一映射到标准化名称
14
+ # deepseek-v4-flash = 之前 deepseek-chat(非思考) / deepseek-reasoner(思考)
15
+ # deepseek-v4-pro = 新旗舰模型
16
+
17
+ PRICING_TABLE: dict[str, dict[str, float]] = {
18
+ "deepseek-v4-flash": {
19
+ "input_cache_hit": 0.02,
20
+ "input_cache_miss": 1.0,
21
+ "output": 2.0,
22
+ },
23
+ "deepseek-v4-pro": {
24
+ "input_cache_hit": 0.025,
25
+ "input_cache_miss": 3.0,
26
+ "output": 6.0,
27
+ },
28
+ }
29
+
30
+ # 别名映射(旧模型名 → 标准化名)
31
+ _MODEL_ALIASES: dict[str, str] = {
32
+ "deepseek-chat": "deepseek-v4-flash",
33
+ "deepseek-reasoner": "deepseek-v4-flash",
34
+ "deepseek-v4-flash": "deepseek-v4-flash",
35
+ "deepseek-v4-pro": "deepseek-v4-pro",
36
+ }
37
+
38
+
39
+ def normalize_model(model: str) -> str | None:
40
+ """将模型名标准化为定价表中的 key。未知模型返回 None。"""
41
+ key = model.strip().lower()
42
+ return _MODEL_ALIASES.get(key)
43
+
44
+
45
+ def calculate_cost(model: str, usage: dict[str, Any]) -> dict[str, Any]:
46
+ """根据模型和 usage 计算费用(人民币元)。
47
+
48
+ Args:
49
+ model: 模型名(自动匹配别名)
50
+ usage: DeepSeek API 返回的 usage 对象,包含:
51
+ - prompt_cache_hit_tokens (int): 缓存命中的输入 tokens
52
+ - prompt_cache_miss_tokens (int): 缓存未命中的输入 tokens
53
+ - completion_tokens (int): 输出 tokens
54
+ - total_tokens (int): 总计
55
+
56
+ Returns:
57
+ {
58
+ "total_cost": float, # 总费用(元)
59
+ "cache_hit_cost": float, # 缓存命中费用
60
+ "cache_miss_cost": float, # 缓存未命中费用
61
+ "output_cost": float, # 输出费用
62
+ "currency": "CNY",
63
+ "model": str, # 标准化模型名
64
+ }
65
+ 如果模型不在定价表中或 usage 为空,所有费用返回 0。
66
+ """
67
+ std_model = normalize_model(model)
68
+ if not std_model or not usage:
69
+ return {
70
+ "total_cost": 0.0,
71
+ "cache_hit_cost": 0.0,
72
+ "cache_miss_cost": 0.0,
73
+ "output_cost": 0.0,
74
+ "currency": "CNY",
75
+ "model": model,
76
+ }
77
+
78
+ prices = PRICING_TABLE[std_model]
79
+
80
+ cache_hit = int(usage.get("prompt_cache_hit_tokens", 0) or 0)
81
+ cache_miss = int(usage.get("prompt_cache_miss_tokens", 0) or 0)
82
+ output = int(usage.get("completion_tokens", 0) or 0)
83
+
84
+ cache_hit_cost = cache_hit * prices["input_cache_hit"] / 1_000_000
85
+ cache_miss_cost = cache_miss * prices["input_cache_miss"] / 1_000_000
86
+ output_cost = output * prices["output"] / 1_000_000
87
+ total = cache_hit_cost + cache_miss_cost + output_cost
88
+
89
+ return {
90
+ "total_cost": round(total, 6),
91
+ "cache_hit_cost": round(cache_hit_cost, 6),
92
+ "cache_miss_cost": round(cache_miss_cost, 6),
93
+ "output_cost": round(output_cost, 6),
94
+ "currency": "CNY",
95
+ "model": std_model,
96
+ }
97
+
98
+
99
+ def format_cost(cost: float, currency: str = "CNY") -> str:
100
+ """将费用格式化为可读字符串。"""
101
+ if cost <= 0:
102
+ return "¥0"
103
+ if cost < 0.01:
104
+ return f"¥{cost:.4f}"
105
+ if cost < 1:
106
+ return f"¥{cost:.3f}"
107
+ return f"¥{cost:.2f}"
@@ -0,0 +1,258 @@
1
+ """Cross-platform helpers to find and stop processes listening on a TCP port."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import re
7
+ import signal
8
+ import subprocess
9
+ import time
10
+ from shutil import which
11
+ from typing import Callable, Optional, Set
12
+
13
+
14
+ def _win_no_window_kwargs() -> dict:
15
+ if os.name != "nt":
16
+ return {}
17
+ si = subprocess.STARTUPINFO() # type: ignore[attr-defined]
18
+ si.dwFlags |= subprocess.STARTF_USESHOWWINDOW # type: ignore[attr-defined]
19
+ si.wShowWindow = 0
20
+ return {"startupinfo": si, "creationflags": 0x08000000}
21
+
22
+
23
+ def pids_listening_on_port(port: int) -> Set[int]:
24
+ """Return PIDs with a TCP listener on *port* (best effort)."""
25
+ if os.name == "nt":
26
+ return _pids_listening_on_port_windows(port)
27
+ return _pids_listening_on_port_posix(port)
28
+
29
+
30
+ def kill_pid(pid: int) -> bool:
31
+ if pid <= 0 or pid == os.getpid():
32
+ return False
33
+ if os.name == "nt":
34
+ return _kill_pid_windows(pid)
35
+ try:
36
+ os.kill(pid, signal.SIGTERM)
37
+ return True
38
+ except (ProcessLookupError, PermissionError, OSError):
39
+ return False
40
+
41
+
42
+ def force_kill_pid(pid: int) -> bool:
43
+ if pid <= 0 or pid == os.getpid():
44
+ return False
45
+ if os.name == "nt":
46
+ return _kill_pid_windows(pid)
47
+ try:
48
+ os.kill(pid, signal.SIGKILL)
49
+ return True
50
+ except (ProcessLookupError, PermissionError, OSError):
51
+ return False
52
+
53
+
54
+ def wait_port_released(port: int, timeout_sec: float) -> bool:
55
+ deadline = time.time() + max(0.1, float(timeout_sec))
56
+ while time.time() < deadline:
57
+ if not pids_listening_on_port(port):
58
+ return True
59
+ time.sleep(0.15)
60
+ return not pids_listening_on_port(port)
61
+
62
+
63
+ def stop_listeners_on_port(
64
+ port: int,
65
+ *,
66
+ timeout_sec: float = 5.0,
67
+ exclude_pid: Optional[int] = None,
68
+ log: Optional[Callable[[str], None]] = None,
69
+ ) -> Set[int]:
70
+ """
71
+ SIGTERM listeners on *port*, wait for release, then SIGKILL stragglers.
72
+ Returns PIDs that were targeted (excluding *exclude_pid*).
73
+ """
74
+ pids = pids_listening_on_port(port)
75
+ if exclude_pid is not None:
76
+ pids.discard(int(exclude_pid))
77
+ if not pids:
78
+ return set()
79
+
80
+ def _say(msg: str) -> None:
81
+ if log:
82
+ log(msg)
83
+
84
+ _say(f"Stopping listener(s) on port {port}: {sorted(pids)}")
85
+ for pid in sorted(pids):
86
+ kill_pid(pid)
87
+
88
+ if wait_port_released(port, timeout_sec):
89
+ return pids
90
+
91
+ remaining = pids_listening_on_port(port)
92
+ if exclude_pid is not None:
93
+ remaining.discard(int(exclude_pid))
94
+ for pid in sorted(remaining):
95
+ _say(f"Force-stopping pid {pid} on port {port}")
96
+ force_kill_pid(pid)
97
+ wait_port_released(port, min(2.0, timeout_sec))
98
+ return pids
99
+
100
+
101
+ def _pids_listening_on_port_windows(port: int) -> Set[int]:
102
+ try:
103
+ cp = subprocess.run(
104
+ ["netstat", "-ano"],
105
+ check=False,
106
+ capture_output=True,
107
+ text=True,
108
+ encoding="utf-8",
109
+ errors="replace",
110
+ **_win_no_window_kwargs(),
111
+ )
112
+ except Exception:
113
+ return set()
114
+ pids: Set[int] = set()
115
+ needle = f":{int(port)}"
116
+ for line in (cp.stdout or "").splitlines():
117
+ line = line.strip()
118
+ if not line or "LISTENING" not in line.upper() or needle not in line:
119
+ continue
120
+ parts = line.split()
121
+ try:
122
+ pid = int(parts[-1])
123
+ except (ValueError, IndexError):
124
+ continue
125
+ if pid > 0:
126
+ pids.add(pid)
127
+ return pids
128
+
129
+
130
+ def _kill_pid_windows(pid: int) -> bool:
131
+ try:
132
+ cp = subprocess.run(
133
+ ["taskkill", "/PID", str(int(pid)), "/F"],
134
+ check=False,
135
+ capture_output=True,
136
+ text=True,
137
+ encoding="utf-8",
138
+ errors="replace",
139
+ **_win_no_window_kwargs(),
140
+ )
141
+ return cp.returncode == 0
142
+ except Exception:
143
+ return False
144
+
145
+
146
+ def _pids_listening_on_port_posix(port: int) -> Set[int]:
147
+ p = int(port)
148
+ pids = _pids_via_lsof(p)
149
+ if pids:
150
+ return {x for x in pids if _pid_listens_on_port(x, p)}
151
+ pids = _pids_via_ss(p)
152
+ if pids:
153
+ return pids
154
+ return _pids_via_fuser(p)
155
+
156
+
157
+ def _pids_via_lsof(port: int) -> Set[int]:
158
+ if not which("lsof"):
159
+ return set()
160
+ try:
161
+ cp = subprocess.run(
162
+ ["lsof", "-nP", f"-iTCP:{port}", "-sTCP:LISTEN", "-t"],
163
+ check=False,
164
+ capture_output=True,
165
+ text=True,
166
+ encoding="utf-8",
167
+ errors="replace",
168
+ timeout=10,
169
+ )
170
+ except Exception:
171
+ return set()
172
+ out: Set[int] = set()
173
+ for line in (cp.stdout or "").splitlines():
174
+ line = line.strip()
175
+ if not line:
176
+ continue
177
+ try:
178
+ out.add(int(line))
179
+ except ValueError:
180
+ continue
181
+ return out
182
+
183
+
184
+ _SS_PID_RE = re.compile(r"pid=(\d+)", re.I)
185
+
186
+
187
+ def _pids_via_ss(port: int) -> Set[int]:
188
+ if not which("ss"):
189
+ return set()
190
+ try:
191
+ cp = subprocess.run(
192
+ ["ss", "-lptn", f"sport = :{port}"],
193
+ check=False,
194
+ capture_output=True,
195
+ text=True,
196
+ encoding="utf-8",
197
+ errors="replace",
198
+ timeout=10,
199
+ )
200
+ except Exception:
201
+ return set()
202
+ pids: Set[int] = set()
203
+ for line in (cp.stdout or "").splitlines():
204
+ for m in _SS_PID_RE.finditer(line):
205
+ try:
206
+ pids.add(int(m.group(1)))
207
+ except ValueError:
208
+ continue
209
+ return pids
210
+
211
+
212
+ def _pids_via_fuser(port: int) -> Set[int]:
213
+ if not which("fuser"):
214
+ return set()
215
+ try:
216
+ cp = subprocess.run(
217
+ ["fuser", "-n", "tcp", str(port)],
218
+ check=False,
219
+ capture_output=True,
220
+ text=True,
221
+ encoding="utf-8",
222
+ errors="replace",
223
+ timeout=10,
224
+ )
225
+ except Exception:
226
+ return set()
227
+ pids: Set[int] = set()
228
+ spec = f"{port}/tcp"
229
+ for part in (cp.stdout or "").split():
230
+ part = part.strip()
231
+ if part.endswith(spec):
232
+ part = part[: -len(spec)]
233
+ part = part.rstrip(":").strip()
234
+ if not part:
235
+ continue
236
+ try:
237
+ pids.add(int(part))
238
+ except ValueError:
239
+ continue
240
+ return pids
241
+
242
+
243
+ def _pid_listens_on_port(pid: int, port: int) -> bool:
244
+ if not which("lsof"):
245
+ return True
246
+ try:
247
+ cp = subprocess.run(
248
+ ["lsof", "-nP", f"-iTCP:{port}", "-sTCP:LISTEN", "-a", "-p", str(pid), "-t"],
249
+ check=False,
250
+ capture_output=True,
251
+ text=True,
252
+ encoding="utf-8",
253
+ errors="replace",
254
+ timeout=5,
255
+ )
256
+ except Exception:
257
+ return True
258
+ return bool((cp.stdout or "").strip())
@@ -0,0 +1,21 @@
1
+ """Bridge ``CODEAGENT_*`` process env into ``SEED_*`` for the Seed kernel."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+
7
+
8
+ def bridge_codeagent_env_to_seed() -> None:
9
+ """
10
+ For each ``CODEAGENT_<SUFFIX>`` set in ``os.environ``, copy to ``SEED_<SUFFIX>``
11
+ when the latter is not already defined.
12
+
13
+ Call after loading ``config/env`` (or legacy ``config/seed.env`` / ``codeagent.env``) and before
14
+ invoking Seed / seed-tools. Does not remove ``CODEAGENT_*`` keys.
15
+ """
16
+ for key, value in list(os.environ.items()):
17
+ if not key.startswith("CODEAGENT_"):
18
+ continue
19
+ seed_key = "SEED_" + key[len("CODEAGENT_") :]
20
+ if seed_key not in os.environ:
21
+ os.environ[seed_key] = value