feat(chat): choose model for new conversations
This commit is contained in:
parent
6437e06503
commit
689059eb8e
|
|
@ -5,6 +5,10 @@
|
|||
> 所以不是每个版本号都有条目。条目格式 `## <版本> — <日期>`,新条目加在最上面。
|
||||
> 工程口径的完整记录见 `PROGRESS.md` / git log。
|
||||
|
||||
## 0.62.4 — 2026-08-06
|
||||
|
||||
- 开启新对话时可以在顶部直接选择对话模型,第一条消息会使用所选模型;工作目录候选改为浮层展示,展开时不再挤出对话区滚动条。
|
||||
|
||||
## 0.62.3 — 2026-08-06
|
||||
|
||||
- 生成 PPT 时不再自动切换到指定模型,会继续使用当前对话已选择的模型。
|
||||
|
|
|
|||
|
|
@ -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。
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
# zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。
|
||||
# 改版本只动这一行。
|
||||
__version__ = "0.62.3"
|
||||
__version__ = "0.62.4"
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 显示
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue