feat(chat): choose model for new conversations

This commit is contained in:
caoqianming 2026-08-06 14:00:48 +08:00
parent 6437e06503
commit 689059eb8e
8 changed files with 111 additions and 3 deletions

View File

@ -5,6 +5,10 @@
> 所以不是每个版本号都有条目。条目格式 `## <版本> — <日期>`,新条目加在最上面。
> 工程口径的完整记录见 `PROGRESS.md` / git log。
## 0.62.4 — 2026-08-06
- 开启新对话时可以在顶部直接选择对话模型,第一条消息会使用所选模型;工作目录候选改为浮层展示,展开时不再挤出对话区滚动条。
## 0.62.3 — 2026-08-06
- 生成 PPT 时不再自动切换到指定模型,会继续使用当前对话已选择的模型。

View File

@ -2,7 +2,7 @@
> 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 1-2 句:做了啥 + 关键判断;细节查 `git log` / `git diff` / `DESIGN §7.9`
最后更新:2026-08-06(PPT 取消定向模型,bump 0.62.3)
最后更新:2026-08-06(新对话创建前选择模型,bump 0.62.4)
---
@ -23,6 +23,7 @@
### 2026-08-06
- **08-06 / 0.62.4 / 新对话创建前选择模型 + 目录浮层优化**:快速新对话草稿在顶部复用既有模型选择位置,只展示当前用户可用模型并在首次发送或上传附件懒创建 task 时持久化所选档位;清单失败继续由后端默认模型兜底。工作目录候选改为视口级浮层,按可用空间自动向上或向下展开,不再扩大 `chat-stream` 滚动范围;窄屏保持标题与模型同排。相关静态前端、无 DB 路由及更新日志共 34 项 unittest、JavaScript 语法与 diff 检查通过;当前环境无可用浏览器实例,未连接生产 DB。无 schema、migration、HTTP API、依赖或运行方式变化。
- **08-06 / 0.62.3 / PPT 取消定向模型**:删除 ppt skill frontmatter 的 `glm.pro52` 定向配置load 后继续沿用 task 当前模型,显式选模型与通用 skill 定向机制保持不变。同步更新 Skill 清单与设计记录;定向模型 7 项 unittest、PPT frontmatter 解析及 diff 检查通过;无 schema、migration、HTTP API、依赖或运行方式变化未连接生产 DB。
- **08-06 / 0.62.2 / 外部结果持久导出 + task 点目录查看开关**:外部大响应缓存统一迁入 user/task 隔离的 `.zcbot_cache`,兼容读取 0.62.1 旧目录;用户明确要求保存时新增 `external_system_result_export`,重验当前连接授权后把完整响应及 operation/参数/查询时间溯源导出到 `data/external/`,不受 24h TTL 影响。媒体 prompt/model/cost sidecar 改存 `figures|videos/.meta/`;文件面板默认隐藏所有 dotdir当前 task 工作目录子树可用 `.*` 开关查看 `.meta/.build/.preview` 等目录user_root 平台目录和点文件始终不展示。完整 483 项 unittest 全绿(17 skip)Node 语法、Ruff 致命规则与 diff 检查通过;当前环境无可用浏览器实例,真实点击留部署后冒烟。无 schema、migration 或依赖变化,未连接生产 DB。
- **08-06 / 0.62.1 / OpenAPI 请求体契约 + 大响应无损读取**:生产 task `506c6ad0` 暴露 Swagger 2 body `$ref` 未进入搜索结果,模型无法得知 BI dataset exec 需要 `body.query`,并在 400 只剩状态码时连续盲猜;现统一解析 Swagger 2/OpenAPI 3 请求体 schema返回限长脱敏的上游错误详情。外部响应不再按字节切成半截 JSON超出模型内联额度时完整写入 user/task 隔离的 24h 缓存并返回 `result_ref`,新增 JSON Pointer、分页和字段投影 reader单响应/task/user 分别以 10/50/200 MiB 守门;完整 479 项 unittest 全绿(17 skip),外部系统专项 26 项、Ruff、隔离 mypy、Python 编译与 diff 检查通过。无 schema、migration、依赖或 HTTP API 变化,未写生产 DB。

View File

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

View File

@ -106,6 +106,20 @@ class StaticVendorTests(unittest.TestCase):
self.assertIn("if (name) body.name = name;", newtask_js)
self.assertNotIn("任务名为必填项", newtask_js)
def test_quick_new_conversation_can_choose_model(self) -> None:
chat_js = (JS_DIR / "chat.js").read_text(encoding="utf-8")
state_js = (JS_DIR / "state.js").read_text(encoding="utf-8")
html = DEV_HTML.read_text(encoding="utf-8")
self.assertIn('id="new-chat-model-sel"', chat_js)
self.assertIn("draftModelProfile", state_js)
self.assertIn("state.draftModelProfile = defaultDraftModelProfile();", chat_js)
self.assertIn('model_profile: state.draftModelProfile || ""', chat_js)
self.assertIn('$("chat-meta").classList.add("draft-meta");', chat_js)
self.assertNotIn("new-chat-model-row", chat_js)
self.assertIn("function positionDraftFolderOptions()", chat_js)
self.assertIn("#new-chat-dir-options { position:fixed;", html)
def test_embed_initial_task_skips_root_directory_bootstrap(self) -> None:
main_js = (JS_DIR / "main.js").read_text(encoding="utf-8")
embed_js = (JS_DIR / "embed.js").read_text(encoding="utf-8")

View File

@ -787,6 +787,9 @@
max-height:260px; overflow:auto; padding:5px; border:1px solid var(--border);
border-radius:8px; background:var(--panel); box-shadow:0 8px 24px rgba(0,0,0,.16);
}
/* 快速新对话位于可滚动的 chat-stream 内;候选层改用视口定位,避免它扩大
scroll overflow 或被编辑区边界裁切。自定义任务弹框仍沿用上面的局部定位。 */
#new-chat-dir-options { position:fixed; z-index:120; top:auto; right:auto; }
.new-chat-dir-combobox.open .new-chat-dir-options { display:block; }
.new-chat-dir-option {
display:flex; align-items:center; justify-content:space-between; gap:12px;
@ -1495,6 +1498,8 @@
.mdl-text { display: none; }
.mdl-icon { display: inline; }
#chat-meta select { max-width: 48vw; }
#chat-meta.draft-meta > .spacer { flex:1 1 auto; height:auto; }
#chat-meta.draft-meta .mdl-wrap { flex:0 1 auto; }
/* 对话面板顶栏:藏 "对话" label / spacer,5 个按钮自然换行,文字 nowrap 防内部断行 */
#pane-mid > .pane-head { flex-wrap: wrap; gap: 6px; padding: 6px 8px; }
#pane-mid > .pane-head > .label,

View File

@ -38,6 +38,7 @@ export async function loadModels() {
} catch (e) {
state.models = []; // 静默兜底:无模型清单时下拉不显示,不挡正常流程
}
syncDraftModelSelect();
try {
const data = await api("GET", "/v1/image_models");
state.imageModels = data.models || [];
@ -410,7 +411,34 @@ function draftWorkingDir() {
return (state.draftWorkingDir || "").trim();
}
function defaultDraftModelProfile() {
const models = state.models || [];
const preferred = models.find(m => m.is_default) || models[0];
return preferred ? preferred.profile : "";
}
function syncDraftModelSelect() {
const sel = $("new-chat-model-sel");
if (!sel) return;
const models = state.models || [];
if (!models.length) {
state.draftModelProfile = "";
sel.innerHTML = `<option value="">默认模型</option>`;
sel.disabled = true;
return;
}
if (!models.some(m => m.profile === state.draftModelProfile)) {
state.draftModelProfile = defaultDraftModelProfile();
}
sel.innerHTML = models.map(m =>
`<option value="${escapeHtml(m.profile)}" ${m.profile === state.draftModelProfile ? "selected" : ""}>${escapeHtml(m.display_name)}</option>`
).join("");
sel.disabled = false;
sel.onchange = () => { state.draftModelProfile = sel.value || ""; };
}
let _draftFolderActiveIndex = -1;
let _draftFolderResizeObserver = null;
function syncDraftAction() {
const ready = !!draftWorkingDir();
@ -487,12 +515,37 @@ function renderDraftFolderOptions() {
});
}
function positionDraftFolderOptions() {
const combo = $("new-chat-dir-combobox");
const input = $("new-chat-dir-picker");
const list = $("new-chat-dir-options");
if (!combo || !input || !list || !combo.classList.contains("open")) return;
const rect = input.getBoundingClientRect();
const gap = 5;
const viewportMargin = 8;
const below = Math.max(0, window.innerHeight - rect.bottom - gap - viewportMargin);
const above = Math.max(0, rect.top - gap - viewportMargin);
const desiredHeight = Math.min(260, list.scrollHeight || 260);
const openBelow = below >= desiredHeight || below >= above;
const available = openBelow ? below : above;
const height = Math.min(desiredHeight, available);
const top = openBelow
? rect.bottom + gap
: Math.max(viewportMargin, rect.top - gap - height);
const left = Math.max(viewportMargin, rect.left);
list.style.left = `${left}px`;
list.style.top = `${top}px`;
list.style.width = `${Math.max(0, Math.min(rect.width, window.innerWidth - left - viewportMargin))}px`;
list.style.maxHeight = `${Math.min(260, available)}px`;
}
function setDraftFolderOpen(open) {
const combo = $("new-chat-dir-combobox");
const input = $("new-chat-dir-picker");
if (!combo || !input) return;
combo.classList.toggle("open", open);
input.setAttribute("aria-expanded", String(open));
if (open) positionDraftFolderOptions();
if (!open) {
input.dataset.searching = "";
input.value = draftWorkingDir();
@ -542,6 +595,11 @@ function bindDraftControls() {
const picker = $("new-chat-dir-picker");
const toggle = $("new-chat-dir-toggle");
if (!combo || !picker || !toggle) return;
if (_draftFolderResizeObserver) _draftFolderResizeObserver.disconnect();
if (typeof ResizeObserver !== "undefined") {
_draftFolderResizeObserver = new ResizeObserver(positionDraftFolderOptions);
_draftFolderResizeObserver.observe(picker);
}
renderDraftFolderOptions();
picker.onfocus = () => {
picker.dataset.searching = "";
@ -592,6 +650,9 @@ function bindDraftControls() {
};
}
window.addEventListener("resize", positionDraftFolderOptions);
$("chat-stream").addEventListener("scroll", positionDraftFolderOptions, { passive: true });
export async function showNewConversationDraft() {
if (state.evtSrc) { state.evtSrc.close(); state.evtSrc = null; }
if (state.taskId) { _flushMediaArtifactCache(); clearAttachTray(); }
@ -601,13 +662,23 @@ export async function showNewConversationDraft() {
state.loadedMessages = [];
state.outline = [];
state.draftNewDirMode = false;
state.draftModelProfile = defaultDraftModelProfile();
state.filesPath = "";
document.querySelectorAll(".task-row").forEach(el => el.classList.remove("active"));
syncChannelCardActive(null);
renderConcurrentWarning();
renderTaskProgressDock([]);
renderOutlineRail();
$("chat-meta").innerHTML = `<span class="muted">新对话</span>`;
$("chat-meta").classList.add("draft-meta");
$("chat-meta").innerHTML = `
<span style="font-weight:600;">新对话</span>
<span class="spacer"></span>
<span class="muted small mdl-wrap" style="display:inline-flex;align-items:center;gap:4px;">
<span class="mdl-text">模型</span><span class="mdl-icon" aria-hidden="true">💬</span>
<select id="new-chat-model-sel" class="small" title="选择新对话模型">
<option value="">默认模型</option>
</select>
</span>`;
$("chat-stream").innerHTML = `
<div class="new-chat-start">
<h2>开始一段新对话</h2>
@ -640,6 +711,7 @@ export async function showNewConversationDraft() {
$("btn-task-menu").disabled = true;
syncOptimizeBtn();
bindDraftControls();
syncDraftModelSelect();
syncDraftAction();
loadFiles();
if (mqPhone.matches) setMobileView("mv-mid");
@ -762,6 +834,7 @@ function refreshCtxRing() {
function renderChatMeta() {
const t = state.taskMeta;
$("chat-meta").classList.remove("draft-meta");
if (!t) { $("chat-meta").innerHTML = `<span class="muted">(未选中任务)</span>`; return; }
const wdName = t.working_dir ? t.working_dir.split("/").filter(Boolean).pop() : "";
const taskName = t.name || "(未命名)";
@ -2528,6 +2601,7 @@ async function createTaskFromDraft() {
if (!workingDir) throw new Error("请先选择或新建工作目录");
const t = await api("POST", "/v1/tasks", {
working_dir: workingDir,
model_profile: state.draftModelProfile || "",
});
state.taskId = t.task_id;
state.taskMeta = t;

View File

@ -46,6 +46,7 @@ export const state = {
// “+ 新对话”/登录空态共用的前端草稿;首发成功后才创建真实 task。
draftWorkingDir: "",
draftNewDirMode: false,
draftModelProfile: "",
filesPath: "",
showHiddenDirs: false, // 仅当前 task 工作目录子树生效user_root 永不展示
// 同 wd 内除自己外其他活跃 task(run_status in running/cancelling),供 banner 显示

View File

@ -762,11 +762,20 @@ header .who {
height: 0;
}
#chat-meta.draft-meta > .spacer {
flex: 1 1 auto;
height: auto;
}
#chat-meta .mdl-wrap {
flex: 1 1 auto;
min-width: 0;
}
#chat-meta.draft-meta .mdl-wrap {
flex: 0 1 auto;
}
#chat-meta select {
width: 100% !important;
max-width: none;