Current section

Files

Jump to
cmdc lib cmdc backend.ex
Raw

lib/cmdc/backend.ex

defmodule CMDC.Backend do
@moduledoc """
CMDC.Backend behaviour — 文件 / 状态 / 远程存储的统一访问层。
让工具层(read_file / write_file / edit_file / ls / glob / grep / execute 等)
与底层执行环境完全解耦,支持任意 backend 实现(ETS / 本地文件 / Docker /
Modal / E2B / Postgres 等)。
## 设计
- **10 个核心 callback**
- 文件操作:`ls/1` `read/3` `write/2` `edit/4` `grep/3` `glob/2`
- 二进制传输:`upload_files/1` `download_files/1`
- 沙盒扩展(可选):`execute/2` `id/0`(由 `CMDC.Sandbox` 子 behaviour 引入)
- **统一 Result struct**:所有 callback 返回标准 Result struct(详见各 callback 文档),
不再用 `{:ok, val} | {:error, reason}` 双形态,便于 pattern match 与
序列化(Postgres / JSON)。
- **标准错误码 atom**`:file_not_found / :permission_denied / :is_directory /
:invalid_path`(4 基础 + 允许 backend 扩展 string)。
- **可路由 (Composite)**:`CMDC.Backend.Composite` 支持按路径前缀路由到不同子
backend,让一个会话同时挂载 sandbox + PG memory + ETS history。
## 与 `CMDC.Sandbox` 关系
历史 `CMDC.Sandbox` 已经定义了类似但更简单的 8 callback(read_file/write_file/...)。
未来版本会将 `CMDC.Sandbox` 重构为 `extends CMDC.Backend`,只新增 `execute/2`
`id/0`,消除两套接口重叠。
## 与 `CMDC.Memory` 关系
现有 `CMDC.Memory` 是「语义记忆存储」behaviour(store/search/similarity_search),
与本模块**用途完全不同**,命名上独立,不冲突。
## Quick Start
backend = CMDC.Backend.State.new(:my_table)
%CMDC.Backend.Results.WriteResult{path: "/hello.txt"} =
CMDC.Backend.write(backend, "/hello.txt", "world")
%CMDC.Backend.Results.ReadResult{file_data: %{content: "world"}} =
CMDC.Backend.read(backend, "/hello.txt")
## 实现指南
实现 backend 时只需 `use CMDC.Backend`(引入默认 `read/3` 等 dispatcher)+
实现 callback。最少要实现 `ls / read / write` 三件套,其他可逐步补齐。
defmodule MyBackend do
use CMDC.Backend
@impl true
def read(backend, path, opts), do: ...
# 其他 callback ...
end
## Backend Factory
支持运行期注入:传递工厂 `{module, opts}``lambda runtime -> backend.t()`
"""
alias CMDC.Backend.Results
alias CMDC.Backend.Results.{
EditResult,
GlobResult,
GrepResult,
LsResult,
ReadResult,
WriteResult
}
# ==========================================================================
# 类型定义
# ==========================================================================
@typedoc """
Backend 实例 —— 通常是 struct,由 `Backend.State.new/1` 等构造器返回。
"""
@type t :: struct()
@typedoc "Backend 工厂 — 接受 runtime(context)返回 backend 实例。"
@type factory :: t() | (term() -> t())
@typedoc "路径 — 必须以 `/` 开头的绝对路径。"
@type path :: String.t()
@typedoc "callback 通用选项。"
@type opts :: keyword()
# ==========================================================================
# Behaviour 回调(10 个)
# ==========================================================================
@doc """
列出指定目录下的直接子项(非递归)。
返回 `%LsResult{entries: [%FileInfo{}]}`,目录条目以 `/` 结尾、
`:is_dir``true`
"""
@callback ls(t(), path()) :: LsResult.t()
@doc """
读取文件内容,支持按行分页。
## 选项
- `:offset` — 起始行(0-indexed,默认 0)
- `:limit` — 最大行数(默认 2000)
"""
@callback read(t(), path(), opts()) :: ReadResult.t()
@doc """
创建新文件。文件已存在时返回 `:invalid_path` 错误(用 `edit/4` 修改既有文件)。
"""
@callback write(t(), path(), content :: String.t()) :: WriteResult.t()
@doc """
精确字符串替换。
## 选项
- `:replace_all``true` 替换全部、`false` 要求唯一匹配(默认 `false`
"""
@callback edit(t(), path(), old_string :: String.t(), new_string :: String.t(), opts()) ::
EditResult.t()
@doc """
搜索文件内容(literal substring,不是 regex)。
## 选项
- `:path` — 搜索目录(默认 cwd)
- `:glob` — 文件名过滤
"""
@callback grep(t(), pattern :: String.t(), opts()) :: GrepResult.t()
@doc """
glob 文件匹配。
## 选项
- `:path` — 基础目录(默认 `/`
"""
@callback glob(t(), pattern :: String.t(), opts()) :: GlobResult.t()
@doc """
批量上传二进制文件。
返回与输入等长的 `[%FileUploadResponse{}]`,支持 partial success。
"""
@callback upload_files(t(), files :: [{path(), binary()}]) ::
[Results.FileUploadResponse.t()]
@doc """
批量下载二进制文件。
返回与输入等长的 `[%FileDownloadResponse{}]`,支持 partial success。
"""
@callback download_files(t(), paths :: [path()]) ::
[Results.FileDownloadResponse.t()]
# 可选 callback — 仅 Sandbox 子 behaviour 必须实现
@optional_callbacks []
# ==========================================================================
# Dispatch helpers(统一入口)
# ==========================================================================
@doc "委派到 backend.ls/1。"
@spec ls(t(), path()) :: LsResult.t()
def ls(%module{} = backend, path), do: module.ls(backend, path)
@doc "委派到 backend.read/3,opts 可选。"
@spec read(t(), path(), opts()) :: ReadResult.t()
def read(%module{} = backend, path, opts \\ []), do: module.read(backend, path, opts)
@doc "委派到 backend.write/2。"
@spec write(t(), path(), String.t()) :: WriteResult.t()
def write(%module{} = backend, path, content), do: module.write(backend, path, content)
@doc "委派到 backend.edit/4,opts 可选。"
@spec edit(t(), path(), String.t(), String.t(), opts()) :: EditResult.t()
def edit(%module{} = backend, path, old, new, opts \\ []) do
module.edit(backend, path, old, new, opts)
end
@doc "委派到 backend.grep/3,opts 可选。"
@spec grep(t(), String.t(), opts()) :: GrepResult.t()
def grep(%module{} = backend, pattern, opts \\ []), do: module.grep(backend, pattern, opts)
@doc "委派到 backend.glob/3,opts 可选(含 :path)。"
@spec glob(t(), String.t(), opts()) :: GlobResult.t()
def glob(%module{} = backend, pattern, opts \\ []), do: module.glob(backend, pattern, opts)
@doc "委派到 backend.upload_files/2。"
@spec upload_files(t(), [{path(), binary()}]) :: [Results.FileUploadResponse.t()]
def upload_files(%module{} = backend, files), do: module.upload_files(backend, files)
@doc "委派到 backend.download_files/2。"
@spec download_files(t(), [path()]) :: [Results.FileDownloadResponse.t()]
def download_files(%module{} = backend, paths), do: module.download_files(backend, paths)
@doc """
解析 backend factory — 接受 struct 或函数。
- struct → 直接返回
- 1-arity function → 用 `runtime` 调用并返回结果
"""
@spec resolve(factory(), term()) :: t()
def resolve(backend, _runtime) when is_struct(backend), do: backend
def resolve(fun, runtime) when is_function(fun, 1), do: fun.(runtime)
end