diff --git a/README.md b/README.md index 9a343b6..6151e10 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # PicGen Console -一个面向 OpenAI 兼容图像生成 / 编辑接口的本地工作台,当前版本 **0.1.60**。它把 +一个面向 OpenAI 兼容图像生成 / 编辑接口的本地工作台,当前版本 **0.1.61**。它把 `/v1/images/generations`、`/v1/images/edits` 与 `/v1/responses`(含 `image_generation` 工具) 包装成统一可观测的代理,前端是一套零依赖的 Web 控制台。 @@ -14,7 +14,9 @@  -## 0.1.60 主要特性 +## 0.1.61 主要特性 + +- **正常页面刷新不再误限流**:业务 API 仍受每分钟总配额保护,5 秒 burst 只约束写请求;生成完成后的统计刷新、工作区重载和作品列表读取不会互相挤占短时写入配额。 - **多候选兼容参数路径**:上游若以 `tools[0].n` 等嵌套参数路径拒绝候选数量,自动去掉 `n` 并并发补齐候选;结构化识别只针对候选数字段,不会误重试内容审核错误。 @@ -62,7 +64,7 @@ - `RequestIdMiddleware` — 生成或透传 `X-Request-ID` - `SecurityHeadersMiddleware` — `X-Content-Type-Options / X-Frame-Options / Referrer-Policy / Permissions-Policy / Cache-Control` - `BodySizeLimitMiddleware` — 拒绝超大请求(默认 64 MB) - - `RateLimitMiddleware` — 滑动窗口限流(默认 120 req/min + 20 burst/5s) + - `RateLimitMiddleware` — 滑动窗口限流(默认业务 API 120 req/min + 写请求 20 burst/5s) - `ProxyAuthMiddleware` — 可选 Bearer / `X-Proxy-Token` 鉴权 - `CORSMiddleware` — 配置化跨域 - **原子化落盘**:图片与 sidecar JSON 用临时文件 + rename 写入,崩溃不留半截文件;核心生成、 @@ -113,10 +115,10 @@ PICGEN_LOG_FORMAT=json \ ### Docker ```bash -docker build -t minorli/picgen:0.1.60 . +docker build -t minorli/picgen:0.1.61 . docker run --rm -p 8000:8000 \ -v picgen-data:/app/data \ - minorli/picgen:0.1.60 + minorli/picgen:0.1.61 ``` 或: @@ -131,10 +133,10 @@ docker compose up -d ./scripts/docker-build-push.sh ``` -默认会构建并推送 `minorli/picgen:0.1.60`。也可以覆盖: +默认会构建并推送 `minorli/picgen:0.1.61`。也可以覆盖: ```bash -IMAGE=minorli/picgen VERSION=0.1.60 PLATFORM=linux/amd64 ./scripts/docker-build-push.sh +IMAGE=minorli/picgen VERSION=0.1.61 PLATFORM=linux/amd64 ./scripts/docker-build-push.sh ``` 镜像不会包含 `.env`、本地用户库或历史图片。容器内置 `HEALTHCHECK` 探测 `/api/health`,以非 root @@ -247,7 +249,7 @@ Bug 反馈和找回密码申请会先写入本地认证库,再优先发送到 ## 图像通道 -PicGen 0.1.60 把四类图像操作统一提交给 `/api/image-jobs`,实际通道由服务端决定: +PicGen 0.1.61 把四类图像操作统一提交给 `/api/image-jobs`,实际通道由服务端决定: | 用户操作 | 默认接口 | 默认模型 | | --- | --- | --- | diff --git a/docker-compose.yml b/docker-compose.yml index e84978d..b355c5e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ services: picgen: - image: minorli/picgen:0.1.60 + image: minorli/picgen:0.1.61 build: context: . ports: diff --git a/pyproject.toml b/pyproject.toml index 2739a4b..c19879d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "picgen" -version = "0.1.60" +version = "0.1.61" description = "Enterprise-grade local web console for OpenAI-compatible image generation and editing APIs." readme = "README.md" requires-python = ">=3.12" diff --git a/scripts/docker-build-push.sh b/scripts/docker-build-push.sh index d4b77b9..a1cffde 100755 --- a/scripts/docker-build-push.sh +++ b/scripts/docker-build-push.sh @@ -2,7 +2,7 @@ set -euo pipefail IMAGE="${IMAGE:-minorli/picgen}" -VERSION="${VERSION:-0.1.60}" +VERSION="${VERSION:-0.1.61}" PLATFORM="${PLATFORM:-linux/amd64}" docker buildx build \ diff --git a/src/picgen/middleware.py b/src/picgen/middleware.py index d93871b..89bd276 100644 --- a/src/picgen/middleware.py +++ b/src/picgen/middleware.py @@ -243,6 +243,7 @@ async def dispatch(self, request: Request, call_next: RequestHandler) -> Respons if request.url.path in {"/api/health", "/api/ready", "/api/config"}: return await call_next(request) client_key = self._client_key(request) + uses_burst_limit = request.method.upper() not in {"GET", "HEAD", "OPTIONS"} now = time.monotonic() async with self._lock: if now - self._last_sweep >= 60.0: @@ -252,7 +253,9 @@ async def dispatch(self, request: Request, call_next: RequestHandler) -> Respons burst_bucket = self._burst_buckets.setdefault(client_key, deque()) self._evict(minute_bucket, now - 60.0) self._evict(burst_bucket, now - 5.0) - if len(minute_bucket) >= self.per_minute or (self.burst > 0 and len(burst_bucket) >= self.burst): + if len(minute_bucket) >= self.per_minute or ( + uses_burst_limit and self.burst > 0 and len(burst_bucket) >= self.burst + ): retry_after = 5 if len(minute_bucket) >= self.per_minute and minute_bucket: # The minute window is what's exhausted — tell the client @@ -275,7 +278,8 @@ async def dispatch(self, request: Request, call_next: RequestHandler) -> Respons extra_headers={"Retry-After": str(retry_after)}, ) minute_bucket.append(now) - burst_bucket.append(now) + if uses_burst_limit: + burst_bucket.append(now) return await call_next(request) diff --git a/static/app.js b/static/app.js index 05e369f..ac24a7d 100644 --- a/static/app.js +++ b/static/app.js @@ -1,11 +1,11 @@ -import { calculateLogoPlacementScore, chooseLogoPlacement } from "./logo-placement.mjs?v=0.1.60" +import { calculateLogoPlacementScore, chooseLogoPlacement } from "./logo-placement.mjs?v=0.1.61" import { DEFAULT_RESPONSES_MODEL, RESPONSES_MODEL_STORAGE_VERSION, RESPONSES_REASONING_STORAGE_VERSION, migrateStoredResponsesReasoningSettings, migrateStoredResponsesSettings, -} from "./responses-settings.mjs?v=0.1.60" +} from "./responses-settings.mjs?v=0.1.61" const RESPONSES_REASONING_EFFORTS = new Set(["low", "medium", "high", "xhigh", "max", "ultra"]) const DEFAULT_RESPONSES_REASONING_EFFORT = "xhigh" diff --git a/static/index.html b/static/index.html index 7e91a03..7c41af5 100644 --- a/static/index.html +++ b/static/index.html @@ -5,7 +5,7 @@