# BlendJob 开发者指南 ## 命名与边界 通用 module 命名为 `blendjob`。这个名字表达的是 Blender 插件与独立 Job 进程之间的桥接,不绑定 AI、模型框架或 SnapForm。 - `JobClient`:运行在 Blender Python 中,通过 HTTP 提交、查询和取消 Job - `JobServer`:对 FastAPI 的通用封装,负责 Job 注册、单任务调度、状态与取消 - `JobContext`:单个 Job 的运行上下文,提供工作目录、进度、取消与 Resource - `ModalJobOperatorBase`:可直接继承的 Blender Modal Operator,内置提交线程、轮询、进度、取消和终态处理 - `Resource`:由 Job Server 持有、跨多个 Job 复用的状态对象 - `SnapForm Job Server`:SnapForm 创建并注册专属 Job 后得到的服务实例;代码变量使用 `server` 或 `snapform_server` `blendjob` 不知道模型、输出文件或 Blender Operator。插件专属代码负责注册 Job 和 Resource,并把参数交给自己的执行逻辑。 ## 最小接入 服务端只创建 Server 并注册业务函数: ```python server = JobServer("Example", jobs_directory="D:/example/jobs") @server.job("generate-depth") def generate_depth(context, parameters): output = context.directory / "depth.npz" run_depth(parameters["input"], output) context.update(1.0, "Depth is ready", output=str(output)) ``` Blender 端直接继承一个基类。普通任务只需要声明 Controller、Job Type 和怎么传值: ```python class GenerateDepth(ModalJobOperatorBase): bl_idname = "example.generate_depth" bl_label = "Generate Depth" controller_factory = staticmethod(service_controller) job_type = "generate-depth" def job_parameters(self, context): return {"input": context.scene.source_path} ``` 这个 Operator 已经具备异步提交、Server 生成 Job ID 与目录、HTTP 轮询、进度、Esc 取消、错误报告和 Timer 清理。只有需要把产物导回 Blender 时,才覆盖 `on_job_succeeded(context, job, status)`;产物目录是 `job.directory`。 ## Resource Job 表示一次有终态的操作;Resource 表示在 Service 生命周期内被多个 Job 共用的状态。Resource 可以按需实现 `snapshot()`、`clear()` 和 `close()`: ```python models = server.add_resource("models", ModelManager(models_directory)) @server.job("generate-depth") def generate_depth(context, parameters): models = context.resource("models") session = models.get(parameters["model"], parameters["device"]) ``` `JobServer` 在同一把调度锁下阻止运行中清理 Resource,关闭时按注册逆序调用 `close()`。Resource Snapshot 自动进入 `/health` 的 `resources` 字段。 ## 服务端使用 开发者先创建 `JobServer`,再用装饰器注册 Job 类型: ```python from blendjob import JobCancelled, JobServer server = JobServer("Example Job Server", jobs_directory="D:/example/jobs") @server.job("generate-depth") def generate_depth(context, parameters): input_path = parameters["input"] output_path = context.directory / "depth.npz" context.update(0.1, "Loading image") context.check_cancelled() run_model(input_path, output_path) context.update( 1.0, "Depth is ready", state="succeeded", output=output_path, ) ``` 处理函数签名固定为 `handler(context, parameters)`。`parameters` 必须可由 JSON 表示。客户端提交的 `JobRequest` 不包含 ID;`JobServer` 接受请求时自动生成 opaque Job ID,并创建 `//`。处理函数通过 `context.job_id` 与 `context.directory` 获取它们。客户端不创建 Job 目录,也不应把输出目录作为业务参数传入。Job API 只保存内存状态和少量结果元数据,不创建 `status.json`。 `context.update()` 支持以下通用字段: - `progress`:`0.0–1.0` - `message`:面向界面的当前步骤 - `state`:通常自动推导,也可显式设置 `queued`、`running`、`succeeded`、`failed`、`cancelling` 或 `cancelled` - `error`:失败信息 - 其它关键字:作为插件自定义状态字段原样返回 长耗时流程应在推理前后和循环内调用 `context.check_cancelled()`。底层库不可中断时,取消会在它返回后的最近检查点生效。 ## Blender 端底层 API `JobClient` 只依赖 Python 标准库,适合直接放入 Blender 插件: ```python from blendjob import JobClient, JobRequest, ServiceController client = JobClient("127.0.0.1", port) request = JobRequest.create( "generate-depth", input="D:/images/source.png", ) submitted = client.submit_request(request) job_id = submitted["job_id"] job_directory = submitted["directory"] status = client.status(job_id) if status["state"] == "running": update_blender_progress(status["progress"], status["message"]) ``` 专属 Service 进程由 `ServiceController` 管理。插件只提供启动命令、工作目录和日志路径,端口、Instance ID、进程句柄、连接验证以及 `start()`、`ensure()`、`enable()`、`restart()`、`stop()`、`detach()` 都由 Controller 负责: ```python controller = ServiceController( lambda port, instance_id: [ str(environment_python), "server.py", "--port", str(port), "--instance-id", instance_id, "--parent-pid", str(blender_pid), ], cwd_factory=lambda: storage_root, log_path_factory=lambda: storage_root / "service.log", ) connection = controller.ensure() submitted = connection.client.submit_request(request) controller.stop() ``` 对应 HTTP API: - `GET /health`:查询服务身份、忙闲状态和插件扩展状态 - `POST /jobs`:提交 Job - `GET /jobs/{job_id}`:轮询进度与终态 - `DELETE /jobs/{job_id}`:请求取消 - `GET /resources`:查询全部 Resource - `GET /resources/{resource_name}`:查询一个 Resource - `POST /resources/{resource_name}/clear`:空闲时清理 Resource - `POST /shutdown`:关闭当前专属服务 Service 只监听 `127.0.0.1`,专属实例由启动它的 `ServiceController` 保存随机端口与 Instance ID。这个本地专属进程协议不额外使用 Token 鉴权。 `ModalJobOperatorBase` 默认使用 Blender Window Manager 的进度接口。插件若有自己的状态栏,可覆盖 `update_job_ui()` 与 `redraw_job_ui()`;这些属于界面定制,不是接入 Job 所必需的代码。运行态使用通用 `ModalJobState`,只包含 Controller、Job ID、目录、进度和取消状态。插件特有的完成参数可由 `create_job_data()` 返回,并通过 `job.data` 读取,不应加入通用状态类。 ## 生命周期建议 每个 Blender 实例为每个插件启动一个专属 Job Server。随机端口与 Instance ID 由 `ServiceController` 生成并只保存在内存中,不写公共发现文件,也不扫描其它本地服务。 启动服务时传入 Blender 的 PID。服务同时监听显式 `/shutdown` 和父进程存活状态,因此正常禁用插件时可立即停止,Blender 异常退出后也能自行退出。这个方案允许同一机器出现多个服务进程,换取明确的所有权和插件隔离。 模型与 Environment 可以继续放在插件自己的共享 Storage Root 中;共享磁盘缓存不代表共享服务进程。若多个 Blender 可能同时写同一个模型目录,插件仍需对下载或更新操作单独加文件锁。 ## 在其它插件复用 当前实现位于 `src/snapform/blendjob/`,没有 SnapForm 依赖。复用时复制整个 `blendjob` package,并在插件专属入口中完成以下工作: 1. 创建带 `jobs_directory` 的 `JobServer` 2. 用 `@server.job()` 注册插件自己的处理函数 3. 用 `server.add_resource()` 注册需要跨 Job 复用的状态 4. 用安装环境中的 Uvicorn 运行 `server.create_app(instance_id)` 5. Blender 端用 `ServiceController` 管理启动、停止、重启和连接检查 6. Blender Operator 继承 `ModalJobOperatorBase`,声明 `controller_factory`、`job_type` 与 `job_parameters()` 7. 需要导入产物时覆盖 `on_job_succeeded()` 后续若多个项目稳定复用,可把 `blendjob` 发布为独立 Python package;现阶段保持源码内嵌可以避免为 Blender 扩展增加安装依赖。