feat(web): 有 gpt_image 权限的用户生图默认 GPT 生图(bump 0.58.16)

- /v1/image_models 的 is_default 改为 _default_image_variant:档位里有
  gpt_image(pro/admin)→ 默认它,否则仍过滤后第一个(seedream)
- _resolve_image_model 空串时按同一 helper 解析该用户默认,下拉默认选中
  与实际起 run 挂的图像工具保持一致;用户仍可下拉切回豆包 Seedream
- 无 user_id 内部路径(定时任务/messenger)行为不变

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
caoqianming 2026-07-13 10:26:31 +08:00
parent 61137611a8
commit 4d7075aeb8
5 changed files with 37 additions and 10 deletions

View File

@ -5,6 +5,10 @@
> 所以不是每个版本号都有条目。条目格式 `## <版本> — <日期>`,新条目加在最上面。 > 所以不是每个版本号都有条目。条目格式 `## <版本> — <日期>`,新条目加在最上面。
> 工程口径的完整记录见 `PROGRESS.md` / git log。 > 工程口径的完整记录见 `PROGRESS.md` / git log。
## 0.58.16 — 2026-07-13
- Pro 档用户的图像生成默认改为「GPT 生图」(顶栏下拉自动选中并实际生效);想用豆包 Seedream(支持改图、指定尺寸)随时在下拉切回即可。
## 0.58.15 — 2026-07-13 ## 0.58.15 — 2026-07-13
- 画流程图 / 架构图前会先让你点选出图方式:结构清晰的矢量图(文字准、可编辑、零成本)还是图像生成的视觉版(有质感,但文字标签易乱码、不可后期改字)。你点名了工具或本次对话已选过,则不再重复问。 - 画流程图 / 架构图前会先让你点选出图方式:结构清晰的矢量图(文字准、可编辑、零成本)还是图像生成的视觉版(有质感,但文字标签易乱码、不可后期改字)。你点名了工具或本次对话已选过,则不再重复问。

View File

@ -23,6 +23,7 @@
### 2026-07 ### 2026-07
- **07-13 / 0.58.16**:**有 gpt_image 权限的用户生图默认 GPT 生图**:此前 `/v1/image_models``is_default` 恒标"档位过滤后第一个"(yaml 文件名排序豆包在前 → 人人默认 seedream)。改法:`web/app.py` 新增 `_default_image_variant(allowed)`——档位集合里有 `gpt_image`(pro 档 / admin)→ 默认它,否则仍第一个(seedream);`/v1/image_models` 的 `is_default``_resolve_image_model` 的空串解析(POST messages 时客户端没显式选 image_model)共用该 helper,保证下拉默认选中项和实际起 run 挂的图像工具永远一致。用户可在顶栏下拉自行切回豆包 Seedream(选中值随消息显式发送,不受默认影响)。前端零改动(chat.js 本就选 `is_default` 项);无 user_id 的内部路径(定时任务/messenger)行为不变(空串 → agent_builder yaml 第一个兜底)。
- **07-13 / 0.58.15**:**结构图 mermaid vs 生图:改为 ask_user 让用户明确点选**:用户反馈"画流程图/架构图时模型直奔 mermaid,想用生图(gpt image)没入口"。根因两处:①岔路引导只写在 imagegen SKILL.md 里,而 mermaid 路径根本不触发 load_skill,模型全程看不到岔路口;②SKILL.md 岔路段把"画个流程图/做个架构图"硬性划给 mermaid。改法(口径:结构图选型交给用户点选,不做信号猜测):`core/agent_builder.py` 新增 `_MEDIA_DIAGRAM_FORK_SEG`(挂 seedream 或 gpt_image 任一 variant 时追加进媒体段)——用户要流程图/架构图/技术路线/时序图时先 `ask_user` 二选一(mermaid 矢量:文字准可编辑零成本 vs 生图视觉版:有质感但中文标签易乱码/位图不可改字),免问例外=用户已点名工具/本对话已选过路线/paper·proposal skill 管线内部要求 mermaid;SKILL.md 岔路段同口径重写,并补"生图画结构图取舍"段(乱码/不可改字提前说清,prompt 按风格化示意少文字装配,想两全建议 mermaid 出结构+生图出底图),反模式条同步;SKILL_LIST.md imagegen 岔路段同步。纯 prompt 调整。 - **07-13 / 0.58.15**:**结构图 mermaid vs 生图:改为 ask_user 让用户明确点选**:用户反馈"画流程图/架构图时模型直奔 mermaid,想用生图(gpt image)没入口"。根因两处:①岔路引导只写在 imagegen SKILL.md 里,而 mermaid 路径根本不触发 load_skill,模型全程看不到岔路口;②SKILL.md 岔路段把"画个流程图/做个架构图"硬性划给 mermaid。改法(口径:结构图选型交给用户点选,不做信号猜测):`core/agent_builder.py` 新增 `_MEDIA_DIAGRAM_FORK_SEG`(挂 seedream 或 gpt_image 任一 variant 时追加进媒体段)——用户要流程图/架构图/技术路线/时序图时先 `ask_user` 二选一(mermaid 矢量:文字准可编辑零成本 vs 生图视觉版:有质感但中文标签易乱码/位图不可改字),免问例外=用户已点名工具/本对话已选过路线/paper·proposal skill 管线内部要求 mermaid;SKILL.md 岔路段同口径重写,并补"生图画结构图取舍"段(乱码/不可改字提前说清,prompt 按风格化示意少文字装配,想两全建议 mermaid 出结构+生图出底图),反模式条同步;SKILL_LIST.md imagegen 岔路段同步。纯 prompt 调整。
- **07-13 / 0.58.14**:**toolfail 聚集加时间分布,活跃/已安静一眼分**(0.58.11 的可观测性补强):此前聚集只有全窗口累计 count+last_at——修掉的问题旧记录仍挂满 7 天窗口,和"还在发生"的没视觉区别,只能人肉盯"最近时间"是否前进。`core/toolfail.py` 聚合时加 `daily`(从 now 往回按 24h 分桶,旧→新,非日历日)/ `count_24h`(=尾桶)/ `first_at`,排序改"近 24h 活跃在前(count_24h 降序),安静的按 count 降序沉底";兼补 naive timestamp 的 tzinfo 兜底。admin 页表格加"近24h"(>0 红)和"趋势"列(unicode block mini 条形图,悬浮看逐桶数值),近24h=0 的行整体淡化;告警邮件每条带"近24h N 次/已安静"。API 只加字段不删,向后兼容。冒烟:伪造行 patch session_scope 全绿(分桶/排序/naive ts/邮件格式)。修复部署后次日看板:对应行变灰+趋势尾格归零 = 修好;仍红 = 没修好。 - **07-13 / 0.58.14**:**toolfail 聚集加时间分布,活跃/已安静一眼分**(0.58.11 的可观测性补强):此前聚集只有全窗口累计 count+last_at——修掉的问题旧记录仍挂满 7 天窗口,和"还在发生"的没视觉区别,只能人肉盯"最近时间"是否前进。`core/toolfail.py` 聚合时加 `daily`(从 now 往回按 24h 分桶,旧→新,非日历日)/ `count_24h`(=尾桶)/ `first_at`,排序改"近 24h 活跃在前(count_24h 降序),安静的按 count 降序沉底";兼补 naive timestamp 的 tzinfo 兜底。admin 页表格加"近24h"(>0 红)和"趋势"列(unicode block mini 条形图,悬浮看逐桶数值),近24h=0 的行整体淡化;告警邮件每条带"近24h N 次/已安静"。API 只加字段不删,向后兼容。冒烟:伪造行 patch session_scope 全绿(分桶/排序/naive ts/邮件格式)。修复部署后次日看板:对应行变灰+趋势尾格归零 = 修好;仍红 = 没修好。
- **07-13 / 0.58.13**:**look_at_image 引导口径改"慢、谨慎调"**:tool description 原写 "Cheap (usually < ¥0.01)"、system prompt 媒体段写"每次很便宜"——都在鼓励随手调,而实际痛点是慢(一次几十秒)。两处(`tools/look_at_image.py` description + `core/agent_builder.py` `_MEDIA_LOOK_SEG`)改为强调 SLOW / 谨慎调用 / 只在确实需要图内容时调 / 想问的一次在 `question` 里问全、绝不对同一张图反复看;"何时不调"加"图内容对当前任务可有可无"。纯 prompt 调整,无行为代码变化。 - **07-13 / 0.58.13**:**look_at_image 引导口径改"慢、谨慎调"**:tool description 原写 "Cheap (usually < ¥0.01)"、system prompt 媒体段写"每次很便宜"——都在鼓励随手调,而实际痛点是慢(一次几十秒)。两处(`tools/look_at_image.py` description + `core/agent_builder.py` `_MEDIA_LOOK_SEG`)改为强调 SLOW / 谨慎调用 / 只在确实需要图内容时调 / 想问的一次在 `question` 里问全、绝不对同一张图反复看;"何时不调"加"图内容对当前任务可有可无"。纯 prompt 调整,无行为代码变化。

View File

@ -1,3 +1,3 @@
# zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。 # zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。
# 改版本只动这一行。 # 改版本只动这一行。
__version__ = "0.58.15" __version__ = "0.58.16"

View File

@ -677,8 +677,8 @@ async def _transcribe_wecom_voice(media_id: str) -> str:
def _list_media_variants(kind: str) -> list[tuple[str, dict]]: def _list_media_variants(kind: str) -> list[tuple[str, dict]]:
"""扫 config/media/*.yaml 的 <kind> 段 → [(variant_key, variant_cfg), ...]。 """扫 config/media/*.yaml 的 <kind> 段 → [(variant_key, variant_cfg), ...]。
provider(doubao / unifyllm)合并列举,文件名排序保证豆包在前( is_default provider(doubao / unifyllm)合并列举,文件名排序保证豆包在前(默认 variant
仍落 seedream)variant key 约定跨文件唯一;万一撞名先读的生效(后者跳过) 的选择逻辑另见 _default_image_variant)variant key 约定跨文件唯一;万一撞名先读的生效(后者跳过)
目录不存在或段空 / 仅注释 []不要求对应 API key 已设 仅纯元数据列举, 目录不存在或段空 / 仅注释 []不要求对应 API key 已设 仅纯元数据列举,
UI 拉这个画下拉真正调用时 agent_builder 那边再过 `ArkConfig.load()` UI 拉这个画下拉真正调用时 agent_builder 那边再过 `ArkConfig.load()`
( key tool 不注册) ( key tool 不注册)
@ -708,15 +708,35 @@ def _list_image_variants() -> list[tuple[str, dict]]:
return _list_media_variants("image") return _list_media_variants("image")
def _default_image_variant(allowed: Optional[set[str]]) -> str:
"""该用户(档位过滤后)的默认 image variant key;无可用 → ""
gpt_image 权限(pro / admin) 默认 GPT 生图;否则落过滤后第一个
(= 豆包 seedream,沿用 yaml 文件名排序)用户仍可在顶栏下拉自行切换
/v1/image_models is_default _resolve_image_model 的空串解析共用这里,
保证"下拉默认选中""实际起 run 用的"永远一致
"""
keys = [k for k, _ in _list_image_variants() if allowed is None or k in allowed]
if not keys:
return ""
return "gpt_image" if "gpt_image" in keys else keys[0]
def _resolve_image_model(variant: str, user_id: Optional[UUID] = None) -> str: def _resolve_image_model(variant: str, user_id: Optional[UUID] = None) -> str:
"""校验 image_model variant key。 """校验 image_model variant key。
传空 返空(agent_builder fallback 到第一个 variant);传非空 必须存在 传空 = 客户端未显式选 解析成该用户的默认 variant( /v1/image_models
config/media/*.yaml image ,否则 400user_id 非空 额外过档位门控 is_default 同源, _default_image_variant); user_id 的内部路径仍返空
(agent_builder fallback 到第一个 variant)传非空 必须存在于
config/media/*.yaml image ,否则 400user_id 非空 额外过档位门控
""" """
name = (variant or "").strip() name = (variant or "").strip()
if not name: if not name:
return "" if user_id is None:
return ""
from core.model_access import allowed_set
plan, role = _user_plan_role(user_id)
return _default_image_variant(allowed_set(plan, role))
variants = {k for k, _ in _list_image_variants()} variants = {k for k, _ in _list_image_variants()}
if name not in variants: if name not in variants:
raise HTTPException(400, f"invalid image_model {name!r}; available: {sorted(variants)}") raise HTTPException(400, f"invalid image_model {name!r}; available: {sorted(variants)}")
@ -1900,11 +1920,13 @@ def create_app() -> FastAPI:
"""图像生成模型清单(扫 config/media/*.yaml image 段,跨 provider)。 """图像生成模型清单(扫 config/media/*.yaml image 段,跨 provider)。
前端顶栏第二个下拉拉这个;空列表 没配 image variant 或本档无授权,UI 隐藏下拉 前端顶栏第二个下拉拉这个;空列表 没配 image variant 或本档无授权,UI 隐藏下拉
按用户档位过滤;`is_default` 标过滤后第一个 variant开发期不缓存, YAML 立即生效 按用户档位过滤;`is_default` _default_image_variant( gpt_image 权限
默认 GPT 生图,否则第一个)开发期不缓存, YAML 立即生效
""" """
from core.model_access import allowed_set from core.model_access import allowed_set
plan, role = _user_plan_role(user_id) plan, role = _user_plan_role(user_id)
allowed = allowed_set(plan, role) allowed = allowed_set(plan, role)
default_key = _default_image_variant(allowed)
out: list[dict] = [] out: list[dict] = []
for key, cfg in _list_image_variants(): for key, cfg in _list_image_variants():
if allowed is not None and key not in allowed: if allowed is not None and key not in allowed:
@ -1914,7 +1936,7 @@ def create_app() -> FastAPI:
"display_name": cfg.get("display_name") or key, "display_name": cfg.get("display_name") or key,
"model_id": cfg.get("model_id") or "", "model_id": cfg.get("model_id") or "",
"price_cny_per_image": cfg.get("price_cny_per_image"), "price_cny_per_image": cfg.get("price_cny_per_image"),
"is_default": not out, # 过滤后第一个 "is_default": key == default_key,
}) })
return {"models": out} return {"models": out}

View File

@ -59,8 +59,8 @@ export const state = {
models: [], models: [],
// 图像生成模型清单(GET /v1/image_models;ARK_API_KEY 未设也会拿到 yaml 元数据) // 图像生成模型清单(GET /v1/image_models;ARK_API_KEY 未设也会拿到 yaml 元数据)
imageModels: [], imageModels: [],
// 当前选中的图像生成 variant key(per-session,不入 DB);默认 = imageModels[0].variant // 当前选中的图像生成 variant key(per-session,不入 DB);默认 = is_default 项
// (=yaml 第一个 = agent_builder fallback)。下次 send 消息时随 POST body 带给 backend。 // (后端按档位定:有 gpt_image 权限默认 GPT 生图,否则 seedream)。下次 send 消息时随 POST body 带给 backend。
imageModel: "", imageModel: "",
// 视频生成模型清单(GET /v1/video_models)+ 当前选中 variant。同 imageModels 范式。 // 视频生成模型清单(GET /v1/video_models)+ 当前选中 variant。同 imageModels 范式。
videoModels: [], videoModels: [],