Initial commit

This commit is contained in:
2026-08-19 19:34:30 +08:00
commit beba8744b4
285 changed files with 74293 additions and 0 deletions
+183
View File
@@ -0,0 +1,183 @@
from __future__ import annotations
import json
import re
import secrets
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from app.settings import Settings
TASK_ID = re.compile(r"^cad_[a-z0-9]{12}$")
CONVERSATION_ID = re.compile(r"^conv_[a-z0-9]{12}$")
def now_iso() -> str:
return datetime.now(timezone.utc).isoformat()
def new_id(prefix: str) -> str:
return f"{prefix}_{secrets.token_hex(6)}"
def safe_task_id(task_id: str) -> str:
value = str(task_id or "").strip()
if not TASK_ID.fullmatch(value):
raise ValueError("Invalid task id")
return value
def safe_conversation_id(conversation_id: str) -> str:
value = str(conversation_id or "").strip()
if not CONVERSATION_ID.fullmatch(value):
raise ValueError("Invalid conversation id")
return value
def safe_relative_path(value: str) -> str:
path = Path(str(value or ""))
if not value or path.is_absolute() or ".." in path.parts:
raise ValueError("Invalid artifact path")
return path.as_posix()
def write_json(path: Path, payload: Any) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
def read_json(path: Path, fallback: Any = None) -> Any:
if not path.is_file():
return fallback
return json.loads(path.read_text(encoding="utf-8"))
class WorkspaceStore:
def __init__(self, settings: Settings) -> None:
self.settings = settings
self.settings.task_root.mkdir(parents=True, exist_ok=True)
self.settings.conversation_root.mkdir(parents=True, exist_ok=True)
def task_dir(self, task_id: str) -> Path:
return self.settings.task_root / safe_task_id(task_id)
def task_path(self, task_id: str) -> Path:
return self.task_dir(task_id) / "task.json"
def conversation_dir(self, conversation_id: str) -> Path:
return self.settings.conversation_root / safe_conversation_id(conversation_id)
def conversation_path(self, conversation_id: str) -> Path:
return self.conversation_dir(conversation_id) / "conversation.json"
def ensure_conversation(
self,
conversation_id: str | None,
current_task_id: str | None = None,
attachments: list[dict[str, Any]] | None = None,
) -> dict[str, Any]:
cid = safe_conversation_id(conversation_id) if conversation_id else new_id("conv")
path = self.conversation_path(cid)
current = read_json(path)
if current:
changed = False
if current_task_id:
current["current_task_id"] = safe_task_id(current_task_id)
changed = True
if attachments is not None:
current["attachments"] = attachments
changed = True
if changed:
current["updated_at"] = now_iso()
write_json(path, current)
return current
record = {
"schema_version": "1.0",
"conversation_id": cid,
"created_at": now_iso(),
"updated_at": now_iso(),
"current_task_id": safe_task_id(current_task_id) if current_task_id else "",
"messages": [],
"attachments": attachments or [],
}
write_json(path, record)
return record
def read_conversation(self, conversation_id: str) -> dict[str, Any] | None:
return read_json(self.conversation_path(conversation_id))
def append_conversation_message(self, conversation_id: str, message: dict[str, Any], current_task_id: str | None = None) -> dict[str, Any]:
record = self.ensure_conversation(conversation_id, current_task_id)
known = {str(item.get("id")) for item in record["messages"]}
if str(message.get("id")) not in known:
record["messages"].append(message)
if current_task_id:
record["current_task_id"] = safe_task_id(current_task_id)
record["updated_at"] = now_iso()
write_json(self.conversation_path(record["conversation_id"]), record)
return record
def write_upload(self, task_id: str, filename: str, data: bytes) -> tuple[str, Path]:
safe_name = re.sub(r"[^a-zA-Z0-9._-]+", "_", Path(filename).name).strip("._") or "attachment"
relative = Path("uploads") / f"upload_{secrets.token_hex(6)}_{safe_name}"
target = self.artifact_path(task_id, relative.as_posix())
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(data)
return relative.as_posix(), target
def ensure_task(self, task_id: str | None, request: str) -> dict[str, Any]:
tid = safe_task_id(task_id) if task_id else new_id("cad")
path = self.task_path(tid)
current = read_json(path)
if current:
return current
task_dir = self.task_dir(tid)
(task_dir / "revisions").mkdir(parents=True, exist_ok=True)
record = {
"schema_version": "1.0",
"task_id": tid,
"request": request,
"created_at": now_iso(),
"updated_at": now_iso(),
"current_revision": "",
"revisions": [],
}
write_json(path, record)
return record
def next_revision(self, task_id: str) -> tuple[str, Path]:
task = self.ensure_task(task_id, "")
revision_id = f"rev_{len(task['revisions']) + 1:03d}"
revision_dir = self.task_dir(task_id) / "revisions" / revision_id
revision_dir.mkdir(parents=True, exist_ok=False)
return revision_id, revision_dir
def update_task(self, task_id: str, revision: dict[str, Any]) -> dict[str, Any]:
task = self.ensure_task(task_id, "")
task["revisions"].append(revision)
if revision.get("status") == "success":
task["current_revision"] = revision["revision_id"]
task["updated_at"] = now_iso()
write_json(self.task_path(task_id), task)
return task
def read_task(self, task_id: str) -> dict[str, Any] | None:
return read_json(self.task_path(task_id))
def current_cdsl_path(self, task_id: str) -> Path | None:
task = self.read_task(task_id)
revision_id = str((task or {}).get("current_revision") or "")
if not revision_id:
return None
candidate = self.task_dir(task_id) / "revisions" / revision_id / "model.cdsl.json"
return candidate if candidate.is_file() else None
def artifact_path(self, task_id: str, relative_path: str) -> Path:
safe = safe_relative_path(relative_path)
root = self.task_dir(task_id).resolve()
target = (root / safe).resolve()
if root != target and root not in target.parents:
raise ValueError("Artifact path escapes task directory")
return target