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,285 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, Field
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TaskStatus(str, Enum):
|
|
13
|
+
scheduled = "scheduled"
|
|
14
|
+
pending = "pending"
|
|
15
|
+
running = "running"
|
|
16
|
+
done = "done"
|
|
17
|
+
failed = "failed"
|
|
18
|
+
killed = "killed"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ConversationStatus(str, Enum):
|
|
22
|
+
active = "active"
|
|
23
|
+
archived = "archived"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class AccountType(str, Enum):
|
|
27
|
+
codex = "codex"
|
|
28
|
+
claude = "claude"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class AccountAuth(str, Enum):
|
|
32
|
+
login = "login"
|
|
33
|
+
env = "env"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class AccountProxy(str, Enum):
|
|
37
|
+
relay = "relay"
|
|
38
|
+
off = "off"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# ── 账号 ──────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
class Account(BaseModel):
|
|
44
|
+
name: str
|
|
45
|
+
type: AccountType
|
|
46
|
+
auth: AccountAuth = AccountAuth.login
|
|
47
|
+
env_file: str = ""
|
|
48
|
+
proxy: AccountProxy = AccountProxy.relay
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class Project(BaseModel):
|
|
52
|
+
name: str
|
|
53
|
+
account: str
|
|
54
|
+
path: str
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def mount_path(self) -> str:
|
|
58
|
+
return "/workspace"
|
|
59
|
+
|
|
60
|
+
def container_name(self, account_type: AccountType) -> str:
|
|
61
|
+
return f"{account_type.value}-{self.name}"
|
|
62
|
+
|
|
63
|
+
def service_name(self, account_type: AccountType) -> str:
|
|
64
|
+
return f"{account_type.value}-project-{self.name}"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ── 任务 ──────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
class Conversation(BaseModel):
|
|
70
|
+
id: str
|
|
71
|
+
name: str
|
|
72
|
+
account: str
|
|
73
|
+
type: AccountType
|
|
74
|
+
project: str
|
|
75
|
+
project_name: str = ""
|
|
76
|
+
native_session_id: str = ""
|
|
77
|
+
status: ConversationStatus = ConversationStatus.active
|
|
78
|
+
created: str = Field(default_factory=lambda: datetime.now().isoformat(timespec="seconds"))
|
|
79
|
+
updated: str = Field(default_factory=lambda: datetime.now().isoformat(timespec="seconds"))
|
|
80
|
+
last_task_id: str = ""
|
|
81
|
+
|
|
82
|
+
def save(self, conversations_dir: Path) -> None:
|
|
83
|
+
conversations_dir.mkdir(parents=True, exist_ok=True)
|
|
84
|
+
path = conversations_dir / f"{self.id}.json"
|
|
85
|
+
path.write_text(
|
|
86
|
+
json.dumps(self.model_dump(), ensure_ascii=False, indent=2),
|
|
87
|
+
encoding="utf-8",
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
@classmethod
|
|
91
|
+
def load(cls, path: Path) -> "Conversation":
|
|
92
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
93
|
+
return cls(**data)
|
|
94
|
+
|
|
95
|
+
@classmethod
|
|
96
|
+
def load_all(cls, conversations_dir: Path) -> list["Conversation"]:
|
|
97
|
+
if not conversations_dir.exists():
|
|
98
|
+
return []
|
|
99
|
+
conversations = []
|
|
100
|
+
for p in sorted(conversations_dir.glob("*.json"), key=lambda x: x.stat().st_mtime, reverse=True):
|
|
101
|
+
try:
|
|
102
|
+
conversations.append(cls.load(p))
|
|
103
|
+
except Exception:
|
|
104
|
+
pass
|
|
105
|
+
return conversations
|
|
106
|
+
|
|
107
|
+
def touch(
|
|
108
|
+
self,
|
|
109
|
+
conversations_dir: Path,
|
|
110
|
+
native_session_id: Optional[str] = None,
|
|
111
|
+
last_task_id: Optional[str] = None,
|
|
112
|
+
) -> None:
|
|
113
|
+
if native_session_id:
|
|
114
|
+
self.native_session_id = native_session_id
|
|
115
|
+
if last_task_id:
|
|
116
|
+
self.last_task_id = last_task_id
|
|
117
|
+
self.updated = datetime.now().isoformat(timespec="seconds")
|
|
118
|
+
self.save(conversations_dir)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class Task(BaseModel):
|
|
122
|
+
id: str
|
|
123
|
+
status: TaskStatus
|
|
124
|
+
account: str
|
|
125
|
+
type: AccountType
|
|
126
|
+
prompt: str
|
|
127
|
+
project: str
|
|
128
|
+
project_name: str = ""
|
|
129
|
+
conversation_id: str = ""
|
|
130
|
+
native_session_id: str = ""
|
|
131
|
+
pid: str = ""
|
|
132
|
+
created: str = Field(default_factory=lambda: datetime.now().isoformat(timespec="seconds"))
|
|
133
|
+
finished: Optional[str] = None
|
|
134
|
+
archived: bool = False
|
|
135
|
+
auto: bool = False
|
|
136
|
+
images: list[str] = Field(default_factory=list)
|
|
137
|
+
execute_at: Optional[str] = None
|
|
138
|
+
|
|
139
|
+
# ── 持久化 ────────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
def save(self, tasks_dir: Path) -> None:
|
|
142
|
+
tasks_dir.mkdir(parents=True, exist_ok=True)
|
|
143
|
+
path = tasks_dir / f"{self.id}.json"
|
|
144
|
+
path.write_text(
|
|
145
|
+
json.dumps(self.model_dump(), ensure_ascii=False, indent=2),
|
|
146
|
+
encoding="utf-8",
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
@classmethod
|
|
150
|
+
def load(cls, path: Path) -> "Task":
|
|
151
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
152
|
+
return cls(**data)
|
|
153
|
+
|
|
154
|
+
@classmethod
|
|
155
|
+
def load_all(cls, tasks_dir: Path) -> list["Task"]:
|
|
156
|
+
if not tasks_dir.exists():
|
|
157
|
+
return []
|
|
158
|
+
tasks = []
|
|
159
|
+
for p in sorted(tasks_dir.glob("*.json"), key=lambda x: x.stat().st_mtime, reverse=True):
|
|
160
|
+
try:
|
|
161
|
+
tasks.append(cls.load(p))
|
|
162
|
+
except Exception:
|
|
163
|
+
pass
|
|
164
|
+
return tasks
|
|
165
|
+
|
|
166
|
+
def update_status(self, status: TaskStatus, tasks_dir: Path) -> None:
|
|
167
|
+
self.status = status
|
|
168
|
+
if status in (TaskStatus.done, TaskStatus.failed, TaskStatus.killed):
|
|
169
|
+
self.finished = datetime.now().isoformat(timespec="seconds")
|
|
170
|
+
self.save(tasks_dir)
|
|
171
|
+
|
|
172
|
+
@property
|
|
173
|
+
def log_path_str(self) -> str:
|
|
174
|
+
return f"tasks/{self.id}.log"
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
# ── HTTP 请求/响应结构 ─────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
class TaskCreateRequest(BaseModel):
|
|
180
|
+
prompt: str
|
|
181
|
+
account: Optional[str] = None # 指定账号名
|
|
182
|
+
project: Optional[str] = None # 按项目路径匹配
|
|
183
|
+
project_name: Optional[str] = None # 按项目配置名匹配
|
|
184
|
+
type: Optional[AccountType] = None
|
|
185
|
+
auto: bool = False # 全自动模式(--full-auto / --dangerously-skip-permissions)
|
|
186
|
+
conversation_id: Optional[str] = None
|
|
187
|
+
conversation_name: Optional[str] = None
|
|
188
|
+
images: list[str] = [] # 容器内图片路径列表(--image 参数)
|
|
189
|
+
execute_at: Optional[str] = None # 定时执行时间
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class TaskResponse(BaseModel):
|
|
193
|
+
id: str
|
|
194
|
+
status: TaskStatus
|
|
195
|
+
account: str
|
|
196
|
+
type: AccountType
|
|
197
|
+
prompt: str
|
|
198
|
+
project: str
|
|
199
|
+
project_name: str = ""
|
|
200
|
+
conversation_id: str = ""
|
|
201
|
+
native_session_id: str = ""
|
|
202
|
+
created: str
|
|
203
|
+
finished: Optional[str] = None
|
|
204
|
+
log_path: str
|
|
205
|
+
archived: bool = False
|
|
206
|
+
auto: bool = False
|
|
207
|
+
images: list[str] = []
|
|
208
|
+
execute_at: Optional[str] = None
|
|
209
|
+
|
|
210
|
+
@classmethod
|
|
211
|
+
def from_task(cls, t: Task) -> "TaskResponse":
|
|
212
|
+
return cls(
|
|
213
|
+
id = t.id,
|
|
214
|
+
status = t.status,
|
|
215
|
+
account = t.account,
|
|
216
|
+
type = t.type,
|
|
217
|
+
prompt = t.prompt,
|
|
218
|
+
project = t.project,
|
|
219
|
+
project_name = t.project_name,
|
|
220
|
+
conversation_id = t.conversation_id,
|
|
221
|
+
native_session_id = t.native_session_id,
|
|
222
|
+
created = t.created,
|
|
223
|
+
finished = t.finished,
|
|
224
|
+
log_path = t.log_path_str,
|
|
225
|
+
archived = t.archived,
|
|
226
|
+
auto = getattr(t, "auto", False),
|
|
227
|
+
images = getattr(t, "images", []),
|
|
228
|
+
execute_at = getattr(t, "execute_at", None),
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class ConversationResponse(BaseModel):
|
|
233
|
+
id: str
|
|
234
|
+
name: str
|
|
235
|
+
account: str
|
|
236
|
+
type: AccountType
|
|
237
|
+
project: str
|
|
238
|
+
project_name: str = ""
|
|
239
|
+
native_session_id: str
|
|
240
|
+
status: ConversationStatus
|
|
241
|
+
created: str
|
|
242
|
+
updated: str
|
|
243
|
+
last_task_id: str
|
|
244
|
+
|
|
245
|
+
@classmethod
|
|
246
|
+
def from_conversation(cls, c: Conversation) -> "ConversationResponse":
|
|
247
|
+
return cls(
|
|
248
|
+
id = c.id,
|
|
249
|
+
name = c.name,
|
|
250
|
+
account = c.account,
|
|
251
|
+
type = c.type,
|
|
252
|
+
project = c.project,
|
|
253
|
+
project_name = c.project_name,
|
|
254
|
+
native_session_id = c.native_session_id,
|
|
255
|
+
status = c.status,
|
|
256
|
+
created = c.created,
|
|
257
|
+
updated = c.updated,
|
|
258
|
+
last_task_id = c.last_task_id,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
class ProjectResponse(BaseModel):
|
|
263
|
+
name: str
|
|
264
|
+
account: str
|
|
265
|
+
path: str
|
|
266
|
+
|
|
267
|
+
@classmethod
|
|
268
|
+
def from_project(cls, p: Project) -> "ProjectResponse":
|
|
269
|
+
return cls(name=p.name, account=p.account, path=p.path)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
class AccountResponse(BaseModel):
|
|
273
|
+
name: str
|
|
274
|
+
type: AccountType
|
|
275
|
+
auth: AccountAuth
|
|
276
|
+
env_file: str = ""
|
|
277
|
+
proxy: AccountProxy
|
|
278
|
+
projects: list[str]
|
|
279
|
+
running: bool # 容器是否在线
|
|
280
|
+
busy: bool # 是否有 running 任务占用
|
|
281
|
+
container: str
|
|
282
|
+
running_task_id: str = ""
|
|
283
|
+
running_task_prompt: str = ""
|
|
284
|
+
task_done_count: int = 0
|
|
285
|
+
task_failed_count: int = 0
|