diff --git a/PROGRESS.md b/PROGRESS.md index 44c4db2..321876d 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -2,7 +2,7 @@ > 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 2-4 句:做了啥 + 关键判断 + 没动什么;细节查 `git log` / `git diff`。 -最后更新:2026-05-20(dev SPA 主页轻量美化:header 加 brand logo / 左 pane filter 行轻分隔 / 顶栏语义按钮改 hover 上色 / 圆角阴影微调) +最后更新:2026-05-20(dev SPA 任务列表行 meta 加最近操作时间相对显示 + 新建弹框工作目录改下拉) --- @@ -23,6 +23,8 @@ ### 2026-05-20 +- **dev SPA 左侧任务列表行加「最近操作时间」**:用户要"显示最新操作时间"。`renderTaskList` 行 meta 区(badge / skill / N 条 / N tok / id-slice)在 id-slice 之前插一个 ``,文案用新加的 `fmtTimeAgo(iso)` 相对时间 helper:`<60s`→刚刚 / `<1h`→N 分钟前 / 同日→N 小时前 / 昨日→昨天 HH:MM / 同年→MM-DD HH:MM / 跨年→YYYY-MM-DD,`title=` hover 出完整 `fmtTime` locale 串。`margin-left:auto` 从 id-slice 挪到时间 span(让两者一起靠右,中间 8px `.meta gap` 自然分隔)。字段用 `updated_at`(任务任何写操作 — 改名 / 新消息 / 状态切 — 都会更新,贴合"最新操作"语义),`/v1/tasks` payload 早已包含,后端零改。**没动**:左 pane 列表默认排序仍 `-created_at`(用户改排序顺序时另说);id-slice 保留(调试参考)。 +- **dev SPA 新建任务弹框「工作目录」从 input + datalist 改 `` autocomplete 改 `` 输入新目录名 + autofocus,提交时 `working_dir = sel === "__new__" ? nt-wd-new.value : sel`。hint 区改 `updateWdHint()` 三分支(新建 / 留空 / 复用),change + new-input + name-input 三事件触发。`` 留在 modal 内但不再被它消费,**只供左 pane 顶部 `#filter-wd` 筛选 autocomplete**(datalist 按 id 引用,DOM 位置无关);`loadFolderSuggestions()` 同次拉取灌两边。**没动**:`/v1/folders` API、提交 body 形态(仍 `working_dir: string`,空串语义不变 → 后端 fallback 用任务名)、左 pane filter-wd 仍用 input + datalist(用户只点名"任务弹框")、DESIGN / RUN。**Tradeoff**:纯 select 实现最直接但会失"新名则新建",改两段式(select 含 `+ 新建…`,触发后展开 text input)保留所有原能力。 - **dev SPA 主页轻量美化(纯 CSS / HTML,不动 JS / 路由)**:用户要"简洁美化主页"。改四处:① header 从裸 "zcbot" 文字 → brand wrapper(24px 红渐变 "Z" logo + 标题字号 14→15 + letter-spacing + 顶栏 1px 极淡阴影),沿用登录页 brand 视觉但缩小;② 左 pane 三行 pane-head(任务标签/搜索/排序)用 `#pane-left .pane-head + .pane-head` 选择器把 filter / sort 子行换白底 + `--border-soft #ececec` 分隔,弱化为子层级,把两条 inline `border-top` 顺手去掉(与新 `border-bottom` 重叠会出双线);③ 顶栏 4 个语义按钮(完成/导出/废弃/删除)+ 选入弹框的复制/移动按钮从"常态彩边 + hover 加底色"改"常态中性 + hover 一次性上语义色(color + border + bg)",给 button 基础类加 transition 让色变平滑(沿用现有 `button.danger` 的同款 hover-only 范式);④ 圆角统一:button / input / textarea / select / floating-menu / .msg 4→6,三个 modal 卡片 6→8 + 阴影 `0 8px 24px → 0 12px 32px` 略深显悬浮感。**没动**:布局 / 交互逻辑 / 任何 JS / 后端 / DESIGN(纯视觉)/ RUN(无对外接口变化);dd-item 菜单的语义色保留(菜单内本来就靠色区分动作类型,不属于"顶栏中性"范畴)。 - **加 `config/models/glm.yaml`:智谱 GLM 5.1 接入(litellm zai provider + 国内站 bigmodel.cn)**:用户要加 GLM。litellm 1.83.14 内置 `zai` provider(PR #17307 早就 merge,我初次 grep 漏了 — 只搜了 zhipu/glm/doubao),`zai/glm-5.1` 自动路由到 z.ai 国际站(`api.z.ai`,env `ZAI_API_KEY`)。**用户用国内站 bigmodel.cn**(账号 / key 跟 z.ai 国际站不通用),YAML 走 `api_base: https://open.bigmodel.cn/api/paas/v4` 覆盖 litellm 默认(`core/llm.py:71-72` 已有 `if self.api_base: kwargs["api_base"]=...` 透传通道),env 命名 `ZHIPUAI_API_KEY` 跟国际站 `ZAI_API_KEY` 分开。family=`glm`,单 variant `pro`,context 200K / reliable 100K / max_out 8192,tool calling 标 good,run_python 开。**`thinking_mode: false`**:GLM 的 thinking 协议是 body `{"type":"enabled"}` 开关 +(可选)budget,与 OpenAI/DeepSeek 的 `reasoning_effort` int 等级不同;`core/llm.py:77-78` 只透传 `reasoning_effort`,要接 GLM thinking 得加 family 分支(`if family.startswith("glm"): kwargs["extra_body"]={"thinking":{"type":"enabled"}}`),不在加 YAML 范围,留 TODO。smoke:`ModelCapabilities.load('glm.pro', ...)` 正常 + `litellm.get_llm_provider('zai/glm-5.1')` 返 `(model=glm-5.1, provider=zai, default_base=https://api.z.ai/api/paas/v4)`,YAML override 生效后实际打 bigmodel.cn;`/v1/models` 扫描结果含 `glm.pro / 'GLM 5.1' / thinking=False`。**没动**:`core/llm.py`(避免半成品 thinking 分支)、DESIGN.md(只加模型档案,非架构变更)、`default_model`(仍 `deepseek_v4.flash`,GLM 是可选项,前端下拉里出现)。**已知待办**:① 接 GLM thinking 透传;② 豆包图像/视频生成(seedream/seedance,完全不同 API 形态,要单独管线)。 - **files SPA UX 翻面 + 拖拽上传 + 修 checkbox 全局 width bug**:沿用上条新加的两路由,但前端 UX 整套换。**原模型**(select-then-pick-dest):主区行带 checkbox + 顶栏全选三态 + 黄 bar(复制到 / 移动到 / 取消)→ 弹框选目标目录。**新模型**(at-dest-pull-sources):主区只读浏览,顶栏加 `[选入…]` 按钮 → 弹框内浏览任意目录 + 跨目录勾文件 / 子目录(`Set` 跨切换保留)+ 底部 `[复制到此处]` `[移动到此处]` 两按钮直接落到主区当前 `state.filesPath`。**理由**:用户切任务时主区自动跳 task working_dir,绝大多数操作是"把外面素材喂进当前 working_dir",destination-first 比 source-first 少一次心智切换,且主区干净。**附带**:① 主区 `` 被全局 `input{ width:100%; }` 撑成全行宽 → 把 `.name`(`flex: 1; flex-basis: 0`)挤成 0 宽,行里只剩看不见的文字 + 居中的 checkbox(用户报"看不到文字"),根因不修永远埋雷,改 selector 排除 checkbox/radio/file。② 拖拽上传:`#pane-right` 监听 dragenter/over/leave/drop,有 `Files` 才响应(忽略文本拖拽),`#file-droparea` 红色虚线 overlay,落点 = `state.filesPath`,沿用 `/v1/files/upload`。**删了**:`state.selectedFiles` + `syncBulkBar` + `dirPicker` 模块 + 顶栏 selall + 黄 bar 整块 + 行 checkbox 渲染(按 CLAUDE.md 不留旧 UX)。**没动**:后端 `/v1/files/copy` `/v1/files/move`(同样的 `paths` + `dest_dir`)、DESIGN、RUN。 diff --git a/web/static/dev.html b/web/static/dev.html index ff1d207..a615ad4 100644 --- a/web/static/dev.html +++ b/web/static/dev.html @@ -654,8 +654,12 @@

新建任务

- - + + +
@@ -796,6 +800,28 @@ function fmtTime(iso) { try { return new Date(iso).toLocaleString(); } catch (e) { return iso; } } +// 相对时间(任务列表用):刚刚 / N 分钟前 / N 小时前 / 昨天 HH:MM / MM-DD / YYYY-MM-DD +function fmtTimeAgo(iso) { + if (!iso) return ""; + let d; + try { d = new Date(iso); } catch (e) { return iso; } + if (isNaN(d.getTime())) return iso; + const now = new Date(); + const diffSec = Math.floor((now - d) / 1000); + if (diffSec < 0) return d.toLocaleString(); // 时钟漂移兜底 + if (diffSec < 60) return "刚刚"; + if (diffSec < 3600) return `${Math.floor(diffSec / 60)} 分钟前`; + if (diffSec < 86400 && now.getDate() === d.getDate()) return `${Math.floor(diffSec / 3600)} 小时前`; + const pad = (n) => String(n).padStart(2, "0"); + const hhmm = `${pad(d.getHours())}:${pad(d.getMinutes())}`; + const yest = new Date(now); yest.setDate(now.getDate() - 1); + if (d.getFullYear() === yest.getFullYear() && d.getMonth() === yest.getMonth() && d.getDate() === yest.getDate()) { + return `昨天 ${hhmm}`; + } + if (d.getFullYear() === now.getFullYear()) return `${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${hhmm}`; + return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; +} + function escapeHtml(s) { return (s || "").replace(/[&<>"']/g, (c) => ( { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c] @@ -1011,7 +1037,8 @@ function renderTaskList(tasks) { ${t.skill ? `${escapeHtml(t.skill)}` : ""} ${t.n_messages || 0} 条 ${t.tokens || 0} tok - ${t.task_id.slice(0, 8)} + ${escapeHtml(fmtTimeAgo(t.updated_at))} + ${t.task_id.slice(0, 8)} @@ -2046,12 +2073,14 @@ async function uploadSelected() { // ───── new task ───── $("hd-new").onclick = async () => { - $("nt-name").value = ""; $("nt-wd").value = ""; + $("nt-name").value = ""; + $("nt-wd-new").value = ""; $("nt-wd-new").style.display = "none"; $("nt-desc").value = ""; $("nt-skill").value = ""; $("nt-err").textContent = ""; $("nt-wd-hint").textContent = ""; $("new-task-modal").classList.add("show"); await Promise.all([loadFolderSuggestions(), loadSkillOptions(), loadModels()]); + $("nt-wd-sel").value = ""; // 默认"留空·用任务名"(loadFolderSuggestions 重渲后保险再置一次) populateModelSelect(); $("nt-name").focus(); }; @@ -2069,7 +2098,8 @@ function populateModelSelect() { $("nt-cancel").onclick = () => $("new-task-modal").classList.remove("show"); $("nt-go").onclick = async () => { const name = $("nt-name").value.trim(); - const working_dir = $("nt-wd").value.trim(); + const sel = $("nt-wd-sel").value; + const working_dir = sel === "__new__" ? $("nt-wd-new").value.trim() : sel; const desc = $("nt-desc").value.trim(); const skill = $("nt-skill").value; const model_profile = $("nt-model").value; @@ -2087,18 +2117,29 @@ $("nt-go").onclick = async () => { } }; -// 工作目录 autocomplete:打开 modal 时拉一次,输入时实时提示"复用 / 新建" +// 工作目录:打开 modal 时拉一次,同时灌 modal