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
codeagent/core/paths.py
ADDED
|
@@ -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
|