coderfleet 0.1.0__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.
- coderfleet/__init__.py +1 -0
- coderfleet/__main__.py +4 -0
- coderfleet/cli.py +212 -0
- coderfleet/compose.py +176 -0
- coderfleet/config.py +69 -0
- coderfleet/config_cmds.py +243 -0
- coderfleet/data/Dockerfile +92 -0
- coderfleet/data/__init__.py +0 -0
- coderfleet/data/accounts.conf.example +26 -0
- coderfleet/data/config.conf.example +31 -0
- coderfleet/data/entrypoint.sh +56 -0
- coderfleet/data/projects.conf.example +17 -0
- coderfleet/data/scripts/coderfleet_usage_status.py +138 -0
- coderfleet/docker_ops.py +385 -0
- coderfleet/init_wizard.py +227 -0
- coderfleet/login_cmd.py +168 -0
- coderfleet/server/__init__.py +0 -0
- coderfleet/server/docker_mgr.py +45 -0
- coderfleet/server/main.py +546 -0
- coderfleet/server/models.py +285 -0
- coderfleet/server/scheduler.py +1219 -0
- coderfleet/server/static/css/main.css +2906 -0
- coderfleet/server/static/index.html +378 -0
- coderfleet/server/static/js/accounts.js +85 -0
- coderfleet/server/static/js/app.js +28 -0
- coderfleet/server/static/js/chat.js +743 -0
- coderfleet/server/static/js/log.js +145 -0
- coderfleet/server/static/js/nav.js +46 -0
- coderfleet/server/static/js/projects.js +298 -0
- coderfleet/server/static/js/renderer.js +586 -0
- coderfleet/server/static/js/state.js +76 -0
- coderfleet/server/static/js/submit.js +200 -0
- coderfleet/server/static/js/tasks.js +92 -0
- coderfleet/server/static/js/terminal.js +347 -0
- coderfleet/server/static/js/utils.js +147 -0
- coderfleet/server/static/vendor/marked.min.js +6 -0
- coderfleet/server/static/vendor/xterm/addon-fit.js +2 -0
- coderfleet/server/static/vendor/xterm/xterm.css +218 -0
- coderfleet/server/static/vendor/xterm/xterm.js +2 -0
- coderfleet/server/terminal.py +129 -0
- coderfleet/task_cmds.py +311 -0
- coderfleet-0.1.0.dist-info/METADATA +492 -0
- coderfleet-0.1.0.dist-info/RECORD +45 -0
- coderfleet-0.1.0.dist-info/WHEEL +4 -0
- coderfleet-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""
|
|
2
|
+
config_cmds.py — coderfleet account / project 子命令
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import re
|
|
7
|
+
import subprocess
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
|
|
13
|
+
from coderfleet.config import ensure_workspace, parse_conf, remove_conf_entry, write_conf_line
|
|
14
|
+
|
|
15
|
+
_NAME_RE = re.compile(r"^[a-zA-Z0-9-]+$")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _container_name(project_name: str, acc_type: str) -> str:
|
|
19
|
+
return f"{acc_type}-{project_name}"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _is_running(container: str) -> bool:
|
|
23
|
+
r = subprocess.run(
|
|
24
|
+
["docker", "inspect", container, "--format", "{{.State.Running}}"],
|
|
25
|
+
capture_output=True, text=True,
|
|
26
|
+
)
|
|
27
|
+
return r.stdout.strip() == "true"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# ── account ───────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@click.group("account")
|
|
34
|
+
def account_group() -> None:
|
|
35
|
+
"""Account management commands."""
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@account_group.command("add")
|
|
40
|
+
@click.argument("name")
|
|
41
|
+
@click.argument("typearg", metavar="TYPE=codex|claude")
|
|
42
|
+
@click.option("--auth", default="login", type=click.Choice(["login", "env"]),
|
|
43
|
+
show_default=True, help="Authentication method")
|
|
44
|
+
@click.option("--env-file", "env_file", default=None,
|
|
45
|
+
help="Env file path (for --auth env, default: accounts/<name>/env)")
|
|
46
|
+
@click.option("--proxy", default="relay", type=click.Choice(["relay", "off"]),
|
|
47
|
+
show_default=True, help="Proxy mode")
|
|
48
|
+
@click.pass_context
|
|
49
|
+
def cmd_account_add(
|
|
50
|
+
ctx: click.Context,
|
|
51
|
+
name: str,
|
|
52
|
+
typearg: str,
|
|
53
|
+
auth: str,
|
|
54
|
+
env_file: Optional[str],
|
|
55
|
+
proxy: str,
|
|
56
|
+
) -> None:
|
|
57
|
+
"""Add a new account. TYPE=codex|claude (or just codex|claude)."""
|
|
58
|
+
ws: Path = ctx.obj["workspace"]
|
|
59
|
+
|
|
60
|
+
if not _NAME_RE.match(name):
|
|
61
|
+
raise click.ClickException("NAME 只能包含字母、数字、连字符")
|
|
62
|
+
|
|
63
|
+
acc_type = typearg[5:] if typearg.startswith("TYPE=") else typearg
|
|
64
|
+
if acc_type not in ("codex", "claude"):
|
|
65
|
+
raise click.ClickException("TYPE 不合法,只支持 TYPE=codex 或 TYPE=claude")
|
|
66
|
+
|
|
67
|
+
if auth == "env":
|
|
68
|
+
if acc_type != "claude":
|
|
69
|
+
raise click.ClickException("AUTH=env 目前只支持 TYPE=claude")
|
|
70
|
+
if not env_file:
|
|
71
|
+
env_file = f"./accounts/{name}/env"
|
|
72
|
+
|
|
73
|
+
accounts_conf = ws / "accounts.conf"
|
|
74
|
+
for r in parse_conf(accounts_conf):
|
|
75
|
+
if r.get("NAME") == name:
|
|
76
|
+
raise click.ClickException(f"账号 '{name}' 已存在,若要修改请先 remove 再 add")
|
|
77
|
+
|
|
78
|
+
tokens: dict[str, str] = {"NAME": name, "TYPE": acc_type, "AUTH": auth}
|
|
79
|
+
if auth == "env" and env_file:
|
|
80
|
+
tokens["ENV_FILE"] = env_file
|
|
81
|
+
tokens["PROXY"] = proxy
|
|
82
|
+
|
|
83
|
+
ensure_workspace(ws)
|
|
84
|
+
write_conf_line(accounts_conf, tokens)
|
|
85
|
+
(ws / "accounts" / name).mkdir(parents=True, exist_ok=True)
|
|
86
|
+
|
|
87
|
+
click.secho(f"✓ 账号 '{name}' 已添加(类型:{acc_type},认证:{auth},代理:{proxy})", fg="green")
|
|
88
|
+
if auth == "env":
|
|
89
|
+
click.secho(f" 请在 {env_file} 中配置 ANTHROPIC_API_KEY 等环境变量", fg="yellow")
|
|
90
|
+
click.secho(f" 接下来:coderfleet project add <项目名> {name} <项目路径>", dim=True)
|
|
91
|
+
click.secho(" 执行 coderfleet apply 使配置生效", fg="yellow")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@account_group.command("remove")
|
|
95
|
+
@click.argument("name")
|
|
96
|
+
@click.pass_context
|
|
97
|
+
def cmd_account_remove(ctx: click.Context, name: str) -> None:
|
|
98
|
+
"""Remove an account (stops associated containers)."""
|
|
99
|
+
ws: Path = ctx.obj["workspace"]
|
|
100
|
+
accounts_conf = ws / "accounts.conf"
|
|
101
|
+
projects_conf = ws / "projects.conf"
|
|
102
|
+
|
|
103
|
+
accounts = {r["NAME"]: r for r in parse_conf(accounts_conf) if "NAME" in r}
|
|
104
|
+
if name not in accounts:
|
|
105
|
+
raise click.ClickException(f"账号 '{name}' 不存在")
|
|
106
|
+
|
|
107
|
+
acc_type = accounts[name].get("TYPE", "")
|
|
108
|
+
if acc_type:
|
|
109
|
+
for p in parse_conf(projects_conf):
|
|
110
|
+
if p.get("ACCOUNT") != name:
|
|
111
|
+
continue
|
|
112
|
+
pname = p.get("NAME", "")
|
|
113
|
+
ctr = _container_name(pname, acc_type)
|
|
114
|
+
if _is_running(ctr):
|
|
115
|
+
click.echo(f" 停止容器 {ctr}...")
|
|
116
|
+
subprocess.run(["docker", "stop", ctr], capture_output=True)
|
|
117
|
+
subprocess.run(["docker", "rm", ctr], capture_output=True)
|
|
118
|
+
|
|
119
|
+
remove_conf_entry(accounts_conf, "NAME", name)
|
|
120
|
+
click.secho(f"✓ 账号 '{name}' 已从配置中移除", fg="green")
|
|
121
|
+
click.secho(f" 认证数据保留在 accounts/{name}/ 如需清除:rm -rf accounts/{name}", fg="yellow")
|
|
122
|
+
click.secho(" 执行 coderfleet apply 重新生成配置", fg="yellow")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@account_group.command("list")
|
|
126
|
+
@click.pass_context
|
|
127
|
+
def cmd_account_list(ctx: click.Context) -> None:
|
|
128
|
+
"""List all accounts."""
|
|
129
|
+
ws: Path = ctx.obj["workspace"]
|
|
130
|
+
accounts = parse_conf(ws / "accounts.conf")
|
|
131
|
+
projects = parse_conf(ws / "projects.conf")
|
|
132
|
+
|
|
133
|
+
click.echo()
|
|
134
|
+
click.echo(" ── 账号列表 " + "─" * 58)
|
|
135
|
+
click.echo(f" {'名称':<20} {'类型':<8} {'认证':<8} {'代理':<8} 状态")
|
|
136
|
+
click.echo(" " + "─" * 70)
|
|
137
|
+
|
|
138
|
+
if not accounts:
|
|
139
|
+
click.secho(" 暂无账号,使用 coderfleet account add 添加", fg="yellow")
|
|
140
|
+
click.echo()
|
|
141
|
+
return
|
|
142
|
+
|
|
143
|
+
for rec in accounts:
|
|
144
|
+
name = rec.get("NAME", "")
|
|
145
|
+
acc_type = rec.get("TYPE", "")
|
|
146
|
+
auth = rec.get("AUTH", "login")
|
|
147
|
+
proxy = rec.get("PROXY", "relay")
|
|
148
|
+
|
|
149
|
+
any_running = any(
|
|
150
|
+
_is_running(_container_name(p.get("NAME", ""), acc_type))
|
|
151
|
+
for p in projects
|
|
152
|
+
if p.get("ACCOUNT") == name
|
|
153
|
+
)
|
|
154
|
+
status = click.style("● 运行中", fg="green") if any_running else click.style("○ 已停止", fg="yellow")
|
|
155
|
+
click.echo(f" {name:<20} {acc_type:<8} {auth:<8} {proxy:<8} {status}")
|
|
156
|
+
|
|
157
|
+
click.echo()
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# ── project ───────────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@click.group("project")
|
|
164
|
+
def project_group() -> None:
|
|
165
|
+
"""Project management commands."""
|
|
166
|
+
pass
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@project_group.command("add")
|
|
170
|
+
@click.argument("name")
|
|
171
|
+
@click.argument("account")
|
|
172
|
+
@click.argument("path")
|
|
173
|
+
@click.pass_context
|
|
174
|
+
def cmd_project_add(
|
|
175
|
+
ctx: click.Context,
|
|
176
|
+
name: str,
|
|
177
|
+
account: str,
|
|
178
|
+
path: str,
|
|
179
|
+
) -> None:
|
|
180
|
+
"""Add a new project."""
|
|
181
|
+
ws: Path = ctx.obj["workspace"]
|
|
182
|
+
|
|
183
|
+
if not _NAME_RE.match(name):
|
|
184
|
+
raise click.ClickException("项目名称只能包含字母、数字、连字符")
|
|
185
|
+
|
|
186
|
+
accounts_conf = ws / "accounts.conf"
|
|
187
|
+
accounts = {r["NAME"] for r in parse_conf(accounts_conf) if "NAME" in r}
|
|
188
|
+
if account not in accounts:
|
|
189
|
+
raise click.ClickException(f"账号 '{account}' 不存在")
|
|
190
|
+
|
|
191
|
+
projects_conf = ws / "projects.conf"
|
|
192
|
+
for p in parse_conf(projects_conf):
|
|
193
|
+
if p.get("NAME") == name:
|
|
194
|
+
raise click.ClickException(f"项目 '{name}' 已存在,若要修改请先 remove 再 add")
|
|
195
|
+
|
|
196
|
+
ensure_workspace(ws)
|
|
197
|
+
write_conf_line(projects_conf, {"NAME": name, "ACCOUNT": account, "PATH": path})
|
|
198
|
+
|
|
199
|
+
expanded = str(Path(path).expanduser())
|
|
200
|
+
click.secho(f"✓ 项目 '{name}' 已添加(账号:{account} 路径:{expanded})", fg="green")
|
|
201
|
+
click.secho(" 执行 coderfleet apply 使配置生效", fg="yellow")
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@project_group.command("remove")
|
|
205
|
+
@click.argument("name")
|
|
206
|
+
@click.pass_context
|
|
207
|
+
def cmd_project_remove(ctx: click.Context, name: str) -> None:
|
|
208
|
+
"""Remove a project."""
|
|
209
|
+
ws: Path = ctx.obj["workspace"]
|
|
210
|
+
projects_conf = ws / "projects.conf"
|
|
211
|
+
|
|
212
|
+
if not any(p.get("NAME") == name for p in parse_conf(projects_conf)):
|
|
213
|
+
raise click.ClickException(f"项目 '{name}' 不存在")
|
|
214
|
+
|
|
215
|
+
remove_conf_entry(projects_conf, "NAME", name)
|
|
216
|
+
click.secho(f"✓ 项目 '{name}' 已移除", fg="green")
|
|
217
|
+
click.secho(" 执行 coderfleet apply 重新生成配置", fg="yellow")
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
@project_group.command("list")
|
|
221
|
+
@click.pass_context
|
|
222
|
+
def cmd_project_list(ctx: click.Context) -> None:
|
|
223
|
+
"""List all projects."""
|
|
224
|
+
ws: Path = ctx.obj["workspace"]
|
|
225
|
+
projects = parse_conf(ws / "projects.conf")
|
|
226
|
+
|
|
227
|
+
click.echo()
|
|
228
|
+
click.echo(" ── 项目列表 " + "─" * 62)
|
|
229
|
+
click.echo(f" {'名称':<20} {'账号':<20} 路径")
|
|
230
|
+
click.echo(" " + "─" * 74)
|
|
231
|
+
|
|
232
|
+
if not projects:
|
|
233
|
+
click.secho(" 暂无项目,使用 coderfleet project add 添加", fg="yellow")
|
|
234
|
+
click.echo()
|
|
235
|
+
return
|
|
236
|
+
|
|
237
|
+
for p in projects:
|
|
238
|
+
name = p.get("NAME", "")
|
|
239
|
+
account = p.get("ACCOUNT", "")
|
|
240
|
+
path = p.get("PATH", "")
|
|
241
|
+
click.echo(f" {name:<20} {account:<20} {path}")
|
|
242
|
+
|
|
243
|
+
click.echo()
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# ============================================================
|
|
2
|
+
# CoderFleet — 统一工作容器镜像
|
|
3
|
+
# 包含:Python 3.12 / Node.js 20 / Codex CLI / Claude Code
|
|
4
|
+
# 平台:linux/amd64(Apple Silicon 通过 Docker 模拟运行)
|
|
5
|
+
# ============================================================
|
|
6
|
+
|
|
7
|
+
FROM --platform=linux/amd64 ubuntu:24.04
|
|
8
|
+
|
|
9
|
+
# 避免交互式安装询问
|
|
10
|
+
ENV DEBIAN_FRONTEND=noninteractive
|
|
11
|
+
ENV TZ=Asia/Shanghai
|
|
12
|
+
|
|
13
|
+
# ── 基础系统工具 ──────────────────────────────────────────
|
|
14
|
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
15
|
+
# 网络工具
|
|
16
|
+
curl wget ca-certificates netcat-openbsd \
|
|
17
|
+
# 开发基础
|
|
18
|
+
git build-essential pkg-config \
|
|
19
|
+
# Python 依赖
|
|
20
|
+
libssl-dev libffi-dev zlib1g-dev libbz2-dev \
|
|
21
|
+
libreadline-dev libsqlite3-dev liblzma-dev \
|
|
22
|
+
# 系统工具
|
|
23
|
+
bash-completion less vim nano \
|
|
24
|
+
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
|
25
|
+
|
|
26
|
+
# ── Node.js 20 (via NodeSource) ───────────────────────────
|
|
27
|
+
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
|
28
|
+
&& apt-get install -y nodejs \
|
|
29
|
+
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
|
30
|
+
|
|
31
|
+
# 验证 Node 版本并安装常用全局包
|
|
32
|
+
RUN node --version && npm --version \
|
|
33
|
+
&& npm install -g npm@latest
|
|
34
|
+
|
|
35
|
+
# ── Python 3.12 (via deadsnakes PPA) ─────────────────────
|
|
36
|
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
37
|
+
software-properties-common \
|
|
38
|
+
&& add-apt-repository ppa:deadsnakes/ppa \
|
|
39
|
+
&& apt-get update && apt-get install -y --no-install-recommends \
|
|
40
|
+
python3.12 python3.12-dev python3.12-venv python3-pip \
|
|
41
|
+
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
|
42
|
+
|
|
43
|
+
# 设置 python3.12 为默认 python3
|
|
44
|
+
RUN update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.12 1 \
|
|
45
|
+
&& update-alternatives --install /usr/bin/python python /usr/bin/python3.12 1
|
|
46
|
+
|
|
47
|
+
# 安装常用 Python 工具
|
|
48
|
+
# Ubuntu 24.04 实行 PEP 668,系统 Python 需加 --break-system-packages
|
|
49
|
+
RUN pip3 install --no-cache-dir --break-system-packages \
|
|
50
|
+
uv ruff black mypy pexpect
|
|
51
|
+
|
|
52
|
+
# ── Codex CLI ─────────────────────────────────────────────
|
|
53
|
+
RUN npm install -g @openai/codex
|
|
54
|
+
|
|
55
|
+
# ── Claude Code CLI ───────────────────────────────────────
|
|
56
|
+
RUN npm install -g @anthropic-ai/claude-code
|
|
57
|
+
|
|
58
|
+
# ── Rust 1.93.0 ───────────────────────────────────────────
|
|
59
|
+
ENV RUSTUP_HOME=/usr/local/rustup \
|
|
60
|
+
CARGO_HOME=/usr/local/cargo \
|
|
61
|
+
PATH=/usr/local/cargo/bin:$PATH \
|
|
62
|
+
RUSTUP_DIST_SERVER=https://rsproxy.cn \
|
|
63
|
+
RUSTUP_UPDATE_ROOT=https://rsproxy.cn/rustup
|
|
64
|
+
RUN curl --proto '=https' --tlsv1.2 -sSf https://rsproxy.cn/rustup-init.sh | sh -s -- -y --no-modify-path --default-toolchain 1.93.0 \
|
|
65
|
+
&& chmod -R a+w $RUSTUP_HOME $CARGO_HOME \
|
|
66
|
+
&& rustc --version
|
|
67
|
+
|
|
68
|
+
# ── 创建非特权用户 byclaw ─────────────────────────────────
|
|
69
|
+
RUN groupadd -r byclaw && useradd -r -g byclaw -m -s /bin/bash byclaw
|
|
70
|
+
|
|
71
|
+
# ── 工作目录和环境变量 ────────────────────────────────────
|
|
72
|
+
RUN mkdir -p /workspace && chown byclaw:byclaw /workspace
|
|
73
|
+
WORKDIR /workspace
|
|
74
|
+
|
|
75
|
+
# 这两个目录由 CoderFleet 按账号挂载,容器内路径固定
|
|
76
|
+
# Codex 认证:/home/byclaw/.codex 由 CODEX_HOME 控制
|
|
77
|
+
# Claude 认证:/home/byclaw/.claude 由 CLAUDE_CONFIG_DIR 控制
|
|
78
|
+
ENV CODEX_HOME=/home/byclaw/.codex
|
|
79
|
+
ENV CLAUDE_CONFIG_DIR=/home/byclaw/.claude
|
|
80
|
+
|
|
81
|
+
# ── 启动脚本 ──────────────────────────────────────────────
|
|
82
|
+
COPY entrypoint.sh /entrypoint.sh
|
|
83
|
+
RUN chmod +x /entrypoint.sh
|
|
84
|
+
|
|
85
|
+
COPY scripts/coderfleet_usage_status.py /usr/local/bin/coderfleet-usage-status
|
|
86
|
+
RUN chmod +x /usr/local/bin/coderfleet-usage-status
|
|
87
|
+
|
|
88
|
+
# ── 切换为非特权用户 ──────────────────────────────────────
|
|
89
|
+
USER byclaw
|
|
90
|
+
|
|
91
|
+
ENTRYPOINT ["/entrypoint.sh"]
|
|
92
|
+
CMD ["sleep", "infinity"]
|
|
File without changes
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# ============================================================
|
|
2
|
+
# accounts.conf — CoderFleet 账号配置
|
|
3
|
+
#
|
|
4
|
+
# 字段说明:
|
|
5
|
+
# NAME=<名称> 必填,只允许字母/数字/连字符
|
|
6
|
+
# TYPE=codex|claude 必填,使用哪个 AI CLI
|
|
7
|
+
# AUTH=login|env 可选,默认 login
|
|
8
|
+
# ENV_FILE=<路径> AUTH=env 时可选;省略则默认 ./accounts/<名称>/env
|
|
9
|
+
# PROXY=relay|off 可选,默认 relay;off 表示该账号下项目不注入代理
|
|
10
|
+
#
|
|
11
|
+
# 项目路径请配置在 projects.conf。
|
|
12
|
+
# 修改后执行 coderfleet apply 生效。
|
|
13
|
+
# ============================================================
|
|
14
|
+
|
|
15
|
+
# Codex 账号示例
|
|
16
|
+
# NAME=codex-alice TYPE=codex
|
|
17
|
+
|
|
18
|
+
# Claude Code 账号示例
|
|
19
|
+
# NAME=claude-alice TYPE=claude
|
|
20
|
+
|
|
21
|
+
# Claude Code 环境变量认证示例(API key 会优先于订阅登录态)
|
|
22
|
+
# NAME=claude-api TYPE=claude AUTH=env
|
|
23
|
+
# NAME=claude-api TYPE=claude AUTH=env ENV_FILE=./accounts/claude-api/env
|
|
24
|
+
|
|
25
|
+
# 不走代理的账号示例(该账号关联的项目都会直连)
|
|
26
|
+
# NAME=claude-local TYPE=claude PROXY=off
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# ============================================================
|
|
2
|
+
# config.conf — CoderFleet 全局配置
|
|
3
|
+
# 修改后执行 coderfleet apply 生效
|
|
4
|
+
# ============================================================
|
|
5
|
+
|
|
6
|
+
# ── 镜像配置 ──────────────────────────────────────────────
|
|
7
|
+
IMAGE_NAME=coderfleet
|
|
8
|
+
IMAGE_TAG=latest
|
|
9
|
+
|
|
10
|
+
# 构建平台(Mac M 系列芯片固定用 linux/amd64,由 Docker Rosetta 模拟运行)
|
|
11
|
+
BUILD_PLATFORM=linux/arm64
|
|
12
|
+
|
|
13
|
+
# ── 代理配置 ──────────────────────────────────────────────
|
|
14
|
+
# 宿主机代理地址(容器通过 host.docker.internal 访问宿主机)
|
|
15
|
+
PROXY_HOST=host.docker.internal
|
|
16
|
+
|
|
17
|
+
# 宿主机代理端口
|
|
18
|
+
# 混合端口(同时支持 HTTP/SOCKS5,如 v2ray/sing-box 的混合入站):两个填同一个值
|
|
19
|
+
# 分开端口(如 Clash:HTTP=7890 SOCKS5=7891):分别填写
|
|
20
|
+
PROXY_HTTP_PORT=10808
|
|
21
|
+
PROXY_SOCKS5_PORT=10808
|
|
22
|
+
|
|
23
|
+
# ── 内部网络配置 ──────────────────────────────────────────
|
|
24
|
+
INTERNAL_SUBNET=172.21.0.0/16
|
|
25
|
+
RELAY_IP=172.21.0.2
|
|
26
|
+
|
|
27
|
+
# 代理中继对内网监听的端口(容器内 HTTP_PROXY 指向此端口)
|
|
28
|
+
RELAY_LISTEN_PORT=10808
|
|
29
|
+
|
|
30
|
+
# 代理中继镜像
|
|
31
|
+
RELAY_IMAGE=gogost/gost:3
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ============================================================
|
|
3
|
+
# CoderFleet — 容器启动脚本
|
|
4
|
+
# 负责:代理验证、认证目录初始化、启动保活进程
|
|
5
|
+
# ============================================================
|
|
6
|
+
|
|
7
|
+
set -e
|
|
8
|
+
|
|
9
|
+
# ── 打印启动信息 ─────────────────────────────────────────
|
|
10
|
+
echo "========================================"
|
|
11
|
+
echo " CoderFleet Container"
|
|
12
|
+
echo " 账号:${CODERFLEET_ACCOUNT_NAME:-unknown}"
|
|
13
|
+
echo " 类型:${CODERFLEET_ACCOUNT_TYPE:-unknown}"
|
|
14
|
+
echo " 认证:${CODERFLEET_ACCOUNT_AUTH:-login}"
|
|
15
|
+
echo "========================================"
|
|
16
|
+
|
|
17
|
+
# ── 等待代理中继就绪 ──────────────────────────────────────
|
|
18
|
+
if [ "${CODERFLEET_ACCOUNT_PROXY:-relay}" = "off" ]; then
|
|
19
|
+
echo "账号代理:关闭(不等待代理中继)"
|
|
20
|
+
else
|
|
21
|
+
PROXY_HOST="${CODERFLEET_RELAY_IP:-172.20.0.2}"
|
|
22
|
+
PROXY_PORT="${CODERFLEET_RELAY_PORT:-7890}"
|
|
23
|
+
MAX_WAIT=30
|
|
24
|
+
waited=0
|
|
25
|
+
|
|
26
|
+
echo "等待代理中继 ${PROXY_HOST}:${PROXY_PORT}..."
|
|
27
|
+
while ! nc -z "$PROXY_HOST" "$PROXY_PORT" 2>/dev/null; do
|
|
28
|
+
if [ "$waited" -ge "$MAX_WAIT" ]; then
|
|
29
|
+
echo "警告:代理中继未就绪,继续启动(网络可能不可用)"
|
|
30
|
+
break
|
|
31
|
+
fi
|
|
32
|
+
sleep 1
|
|
33
|
+
waited=$((waited + 1))
|
|
34
|
+
done
|
|
35
|
+
[ "$waited" -lt "$MAX_WAIT" ] && echo "代理中继已就绪"
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
# ── 初始化认证目录 ────────────────────────────────────────
|
|
39
|
+
ACCOUNT_TYPE="${CODERFLEET_ACCOUNT_TYPE:-codex}"
|
|
40
|
+
case "$ACCOUNT_TYPE" in
|
|
41
|
+
codex)
|
|
42
|
+
mkdir -p "$CODEX_HOME"
|
|
43
|
+
echo "Codex 认证目录:$CODEX_HOME"
|
|
44
|
+
;;
|
|
45
|
+
claude)
|
|
46
|
+
mkdir -p "$CLAUDE_CONFIG_DIR"
|
|
47
|
+
echo "Claude 认证目录:$CLAUDE_CONFIG_DIR"
|
|
48
|
+
if [ "${CODERFLEET_ACCOUNT_AUTH:-login}" = "env" ]; then
|
|
49
|
+
echo "Claude 环境变量认证:已启用"
|
|
50
|
+
fi
|
|
51
|
+
;;
|
|
52
|
+
esac
|
|
53
|
+
|
|
54
|
+
# ── 执行传入的命令(默认 sleep infinity)─────────────────
|
|
55
|
+
echo "容器启动完成,执行:$*"
|
|
56
|
+
exec "$@"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# ============================================================
|
|
2
|
+
# projects.conf — CoderFleet 项目配置
|
|
3
|
+
#
|
|
4
|
+
# 字段说明:
|
|
5
|
+
# NAME=<名称> 必填,只允许字母/数字/连字符
|
|
6
|
+
# ACCOUNT=<账号名> 必填,使用 accounts.conf 中的账号
|
|
7
|
+
# PATH=<项目路径> 必填,挂载进该账号容器的目录(支持 ~)
|
|
8
|
+
#
|
|
9
|
+
# 修改后执行 coderfleet apply 生效。
|
|
10
|
+
# ============================================================
|
|
11
|
+
|
|
12
|
+
# 示例:
|
|
13
|
+
# NAME=my-app ACCOUNT=codex-alice PATH=~/projects/my-app
|
|
14
|
+
# NAME=local-app ACCOUNT=claude-local PATH=~/projects/local-app
|
|
15
|
+
|
|
16
|
+
NAME=byclaw-code-agent ACCOUNT=codex-zhchxiao123 PATH=/home/demo/demo-byclaw
|
|
17
|
+
NAME=timelog ACCOUNT=claude-fajikmomi PATH=/home/demo/demo-timelog
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
ANSI_RE = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
|
|
13
|
+
STATUS_LINE_RE = re.compile(
|
|
14
|
+
r"(5h|five|weekly|week|limit|left|remaining|usage|quota|reset|used|剩余|用量|限制|重置)",
|
|
15
|
+
re.IGNORECASE,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def strip_ansi(text: str) -> str:
|
|
20
|
+
return ANSI_RE.sub("", text)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def unique_lines(lines: list[str]) -> list[str]:
|
|
24
|
+
seen: set[str] = set()
|
|
25
|
+
result: list[str] = []
|
|
26
|
+
for line in lines:
|
|
27
|
+
if line not in seen:
|
|
28
|
+
seen.add(line)
|
|
29
|
+
result.append(line)
|
|
30
|
+
return result
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def clean_status_line(line: str) -> str:
|
|
34
|
+
cleaned = line.strip().strip("│").strip()
|
|
35
|
+
cleaned = re.sub(r"\[[█░▒▓#=\-\s]+\]\s*", "", cleaned)
|
|
36
|
+
cleaned = re.sub(r"\s{2,}", " ", cleaned)
|
|
37
|
+
cleaned = re.sub(r"^([A-Za-z0-9 ]+ limit:)\s+", r"\1 ", cleaned)
|
|
38
|
+
return cleaned
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def extract_codex_usage_summary(text: str) -> str:
|
|
42
|
+
cleaned_text = strip_ansi(text)
|
|
43
|
+
matched = [
|
|
44
|
+
clean_status_line(line)
|
|
45
|
+
for line in cleaned_text.splitlines()
|
|
46
|
+
if re.search(r"\b(5h|Weekly) limit:\s+", line, re.IGNORECASE)
|
|
47
|
+
]
|
|
48
|
+
matched = [line for line in matched if line]
|
|
49
|
+
return "\n".join(unique_lines(matched))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def check_codex_login() -> bool:
|
|
53
|
+
try:
|
|
54
|
+
result = subprocess.run(
|
|
55
|
+
["codex", "login", "status"],
|
|
56
|
+
stdout=subprocess.PIPE,
|
|
57
|
+
stderr=subprocess.STDOUT,
|
|
58
|
+
text=True,
|
|
59
|
+
timeout=20,
|
|
60
|
+
check=False,
|
|
61
|
+
)
|
|
62
|
+
return result.returncode == 0
|
|
63
|
+
except (OSError, subprocess.SubprocessError):
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def read_available(child: Any, pexpect_module: Any, size: int = 20000, timeout: int = 3) -> str:
|
|
68
|
+
try:
|
|
69
|
+
return child.read_nonblocking(size=size, timeout=timeout)
|
|
70
|
+
except pexpect_module.TIMEOUT:
|
|
71
|
+
return ""
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def collect_codex_status() -> str:
|
|
75
|
+
if shutil.which("codex") is None:
|
|
76
|
+
return "Codex CLI 不存在,跳过用量检查"
|
|
77
|
+
if not check_codex_login():
|
|
78
|
+
return "Codex 未登录或凭证不可用,跳过用量检查"
|
|
79
|
+
|
|
80
|
+
import pexpect
|
|
81
|
+
|
|
82
|
+
child = pexpect.spawn(
|
|
83
|
+
"codex",
|
|
84
|
+
encoding="utf-8",
|
|
85
|
+
timeout=25,
|
|
86
|
+
dimensions=(40, 160),
|
|
87
|
+
)
|
|
88
|
+
buf = ""
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
time.sleep(2)
|
|
92
|
+
buf += read_available(child, pexpect, timeout=1)
|
|
93
|
+
|
|
94
|
+
child.sendline("/status")
|
|
95
|
+
time.sleep(4)
|
|
96
|
+
buf += read_available(child, pexpect)
|
|
97
|
+
|
|
98
|
+
child.sendline("/exit")
|
|
99
|
+
try:
|
|
100
|
+
child.expect(pexpect.EOF, timeout=8)
|
|
101
|
+
buf += child.before or ""
|
|
102
|
+
except pexpect.TIMEOUT:
|
|
103
|
+
child.sendcontrol("c")
|
|
104
|
+
child.close(force=True)
|
|
105
|
+
finally:
|
|
106
|
+
if child.isalive():
|
|
107
|
+
child.close(force=True)
|
|
108
|
+
|
|
109
|
+
summary = extract_codex_usage_summary(buf)
|
|
110
|
+
if summary:
|
|
111
|
+
return summary
|
|
112
|
+
|
|
113
|
+
text = strip_ansi(buf)
|
|
114
|
+
matched = [
|
|
115
|
+
clean_status_line(line)
|
|
116
|
+
for line in text.splitlines()
|
|
117
|
+
if line.strip() and STATUS_LINE_RE.search(line)
|
|
118
|
+
]
|
|
119
|
+
matched = [line for line in matched if line]
|
|
120
|
+
return "\n".join(unique_lines(matched)) if matched else text.strip()
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def main(argv: list[str]) -> int:
|
|
124
|
+
cli = argv[1] if len(argv) > 1 else "codex"
|
|
125
|
+
if cli != "codex":
|
|
126
|
+
print(f"{cli} 暂未支持自动用量检查")
|
|
127
|
+
return 0
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
output = collect_codex_status()
|
|
131
|
+
except Exception as err:
|
|
132
|
+
output = f"Codex 用量检查失败:{err}"
|
|
133
|
+
print(output or "未获取到 Codex 用量信息")
|
|
134
|
+
return 0
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
if __name__ == "__main__":
|
|
138
|
+
raise SystemExit(main(sys.argv))
|