ui: 新建任务弹窗工作目录改 combobox + 跟随任务名联动

- nt-wd-sel 从 <select> 改 <input list=folders-datalist>,删 "+ 新建目录…"
  sentinel 和二级 nt-wd-new 输入框
- 加 wdManuallyEdited flag:name 输入时若未脱钩则同步到 wd;wd 非空输入
  置 true 脱钩;wd 清空重置 flag 但保持空(避免 backspace 想换名字时被
  立刻填回打断)
- loadFolderSuggestions 只灌共享 datalist,缓存到 state.folders 供 hint
  比对"命中已有/新建"
- submit 保留 working_dir || name fallback 兜底空值

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
caoqianming 2026-05-21 20:35:37 +08:00
parent 468cef5fcc
commit 8d7f60d899
2 changed files with 32 additions and 43 deletions

View File

@ -23,6 +23,7 @@
### 2026-05-21 ### 2026-05-21
- **新建任务弹窗工作目录改 combobox + name 联动**:`web/static/dev.html` modal 里 `nt-wd-sel``<select>` 改成 `<input list="folders-datalist">`,删 `+ 新建目录…` sentinel + 二级 `nt-wd-new` 输入框;加 `wdManuallyEdited` flag —— name 输入时若 flag=false 自动同步到 wd(programmatic 改 value 不触发 wd input 事件不会假阳性),wd 非空输入置 flag=true 脱钩,wd 清空重置 flag=false 但保持空(避免 backspace 想换名字时被立刻填回打断);submit 保留 `working_dir || name` fallback 兜底空值。`loadFolderSuggestions` 不再渲染 select options,只灌共享 datalist + 缓存到 `state.folders` 供 hint 比对"命中已有/新建"。label 文案 `(可选,留空 → 用任务名...)``(默认跟随任务名;可输入新名或选已有目录复用)`,更直观。
- **system prompt 注入 task 预选 skill 提示**:`core/agent_builder.py::_build_system_prompt` 加 `task_skill` 参数,非空时在"工作目录与 task 上下文"段加一行 `- **task 预选 skill**: \`<name>\` — 用户创建时声明的主 skill`;空字符串走老路径,prompt 字节级一致。LLM 拿到这条事实 + `general_v1.md:17-23` 已有的"对应 skill 领域先 load_skill" 规则自然组合 → 主动 load。否决"直接把完整 SKILL.md 预注入 prompt"方案 —— 那会把 `tasks.skill` 从 metadata 升格成 binding,需要同步改 DESIGN.md / 想清楚 PATCH 改 skill 的语义,投入产出比不划算;轻量提示保渐进披露三层架构不动。 - **system prompt 注入 task 预选 skill 提示**:`core/agent_builder.py::_build_system_prompt` 加 `task_skill` 参数,非空时在"工作目录与 task 上下文"段加一行 `- **task 预选 skill**: \`<name>\` — 用户创建时声明的主 skill`;空字符串走老路径,prompt 字节级一致。LLM 拿到这条事实 + `general_v1.md:17-23` 已有的"对应 skill 领域先 load_skill" 规则自然组合 → 主动 load。否决"直接把完整 SKILL.md 预注入 prompt"方案 —— 那会把 `tasks.skill` 从 metadata 升格成 binding,需要同步改 DESIGN.md / 想清楚 PATCH 改 skill 的语义,投入产出比不划算;轻量提示保渐进披露三层架构不动。
- **imagegen skill 加 ⛔ 调 tool 前必须贴 prompt 给用户 + BLOCKING 等确认硬约束**:用户反馈之前流程"模糊就问"不够,清楚的描述也可能模型脑里和用户脑里对不上,事后看图才发现白烧 ¥0.22。改:① 顶部流程一句话加"⛔ 把 prompt 完整贴给用户看 + 问改不改 → 用户明确确认后 → 调 seedream"步骤;② 加「调 tool 前的强制门(铁律)」段定义回复分类(可以/OK/画吧/嗯 算确认;改 X → 重贴重等;沉默/追问别的 → 继续等;模棱两可 → 追问到明确);③ 加「调 tool 前再过一道」段给具体贴 prompt 的对话格式(代码块 + 参数清单 + 预计花费 + 一句"开烧?改什么?");④ 调用范式段加"前置条件:已拿到明确确认才调";⑤ 反模式加两条(没贴就调 / 模棱两可当确认)。本质是把"模型脑内装配"摊到对话层让用户最后过一眼,装配 ≠ 授权调用。:用户反馈 skill 缺图片比例引导。原 SKILL 里 size 表写"比例只能正方形"是基于 doubao.yaml + tool 参数描述只列三个正方形例子的间接推断,无验证。改:① 诊断五维 → 六维,加"比例/尺寸"(ppt 16:9 / 海报 9:16 / 头像 1:1 / 公众号 2.35:1 / 书籍 3:4);② 一次性追问范式加比例项,上下文推断里给"做 ppt/海报/公众号/学术示意"四种用途的默认比例;③ size 参数表重写成"按用途选比例,再选分辨率",列常见 size 参考值 + 明确"非方形是按比例算的参考值,豆包是否原生支持需首次小调用验证";④ 失败解药表加比例错(改 size 不动 prompt)+ API 报错回退默认两条;⑤ 反模式加"不问比例就默认走 yaml 1:1"。承认 unknown:豆包 5.0 实际支持哪些非方形 size 没验证,首次用错就回退默认 + 让用户协商,不臆造。:两根因 —— ① `general_v1.md` 「媒体生成工具」段把 `seedream` 写成一级直觉(列了"画/出/来张"等关键词 + 直接调 tool 的 how-to),压过 skill discovery block 的微弱声音;② imagegen description 关键词覆盖窄(没有"画/绘制/艺术图/图片"等朴素词)。修法:system prompt 那段改成"调 seedream 前**必须先 `load_skill('imagegen')`**",细节判断全移到 skill 里,只留 ¥0.22 计费 + 不装饰生成 + 不连发三条兜底硬约束;imagegen description 扩 17 个触发词(画/绘制/出图/来张/艺术图/写实图/场景图...)。两层联动:一级 prompt 指引到 skill,二级 description 提匹配概率。 - **imagegen skill 加 ⛔ 调 tool 前必须贴 prompt 给用户 + BLOCKING 等确认硬约束**:用户反馈之前流程"模糊就问"不够,清楚的描述也可能模型脑里和用户脑里对不上,事后看图才发现白烧 ¥0.22。改:① 顶部流程一句话加"⛔ 把 prompt 完整贴给用户看 + 问改不改 → 用户明确确认后 → 调 seedream"步骤;② 加「调 tool 前的强制门(铁律)」段定义回复分类(可以/OK/画吧/嗯 算确认;改 X → 重贴重等;沉默/追问别的 → 继续等;模棱两可 → 追问到明确);③ 加「调 tool 前再过一道」段给具体贴 prompt 的对话格式(代码块 + 参数清单 + 预计花费 + 一句"开烧?改什么?");④ 调用范式段加"前置条件:已拿到明确确认才调";⑤ 反模式加两条(没贴就调 / 模棱两可当确认)。本质是把"模型脑内装配"摊到对话层让用户最后过一眼,装配 ≠ 授权调用。:用户反馈 skill 缺图片比例引导。原 SKILL 里 size 表写"比例只能正方形"是基于 doubao.yaml + tool 参数描述只列三个正方形例子的间接推断,无验证。改:① 诊断五维 → 六维,加"比例/尺寸"(ppt 16:9 / 海报 9:16 / 头像 1:1 / 公众号 2.35:1 / 书籍 3:4);② 一次性追问范式加比例项,上下文推断里给"做 ppt/海报/公众号/学术示意"四种用途的默认比例;③ size 参数表重写成"按用途选比例,再选分辨率",列常见 size 参考值 + 明确"非方形是按比例算的参考值,豆包是否原生支持需首次小调用验证";④ 失败解药表加比例错(改 size 不动 prompt)+ API 报错回退默认两条;⑤ 反模式加"不问比例就默认走 yaml 1:1"。承认 unknown:豆包 5.0 实际支持哪些非方形 size 没验证,首次用错就回退默认 + 让用户协商,不臆造。:两根因 —— ① `general_v1.md` 「媒体生成工具」段把 `seedream` 写成一级直觉(列了"画/出/来张"等关键词 + 直接调 tool 的 how-to),压过 skill discovery block 的微弱声音;② imagegen description 关键词覆盖窄(没有"画/绘制/艺术图/图片"等朴素词)。修法:system prompt 那段改成"调 seedream 前**必须先 `load_skill('imagegen')`**",细节判断全移到 skill 里,只留 ¥0.22 计费 + 不装饰生成 + 不连发三条兜底硬约束;imagegen description 扩 17 个触发词(画/绘制/出图/来张/艺术图/写实图/场景图...)。两层联动:一级 prompt 指引到 skill,二级 description 提匹配概率。
- **新增 imagegen skill(引导用户说清楚生图需求)**:`skills/imagegen/SKILL.md` 单文件(参考 coding skill 范式无 scripts/references)。核心是"先诊断模糊度 → 一次性给推断 + 待确认项 → 用户拍板 → 装配 prompt → 调 `seedream` tool"五步法,防止用户一句"画个 XX"就直接烧 ¥0.22。五维清单(主体/场景/风格/构图/光线)缺 2 维以上就先问;mermaid vs seedream 选型给"默认倾向 mermaid + 反向选 seedream 信号 + 模糊时主动一句话问用户"三段式(没在 system prompt 那段流程图优先 mermaid 上一刀切,留 skill 层细化判断);size/watermark/search 默认值取舍 + 失败不复发的解药表 + 8 条反模式。`seedream` tool 本身不动,skill 仅是流程引导层。 - **新增 imagegen skill(引导用户说清楚生图需求)**:`skills/imagegen/SKILL.md` 单文件(参考 coding skill 范式无 scripts/references)。核心是"先诊断模糊度 → 一次性给推断 + 待确认项 → 用户拍板 → 装配 prompt → 调 `seedream` tool"五步法,防止用户一句"画个 XX"就直接烧 ¥0.22。五维清单(主体/场景/风格/构图/光线)缺 2 维以上就先问;mermaid vs seedream 选型给"默认倾向 mermaid + 反向选 seedream 信号 + 模糊时主动一句话问用户"三段式(没在 system prompt 那段流程图优先 mermaid 上一刀切,留 skill 层细化判断);size/watermark/search 默认值取舍 + 失败不复发的解药表 + 8 条反模式。`seedream` tool 本身不动,skill 仅是流程引导层。

View File

@ -784,12 +784,8 @@
<h3>新建任务</h3> <h3>新建任务</h3>
<label for="nt-name">任务名(必填)</label> <label for="nt-name">任务名(必填)</label>
<input id="nt-name" placeholder="例如 初稿大纲" /> <input id="nt-name" placeholder="例如 初稿大纲" />
<label for="nt-wd-sel">工作目录(可选,留空 → 用任务名;已有则复用,新名则新建)</label> <label for="nt-wd-sel">工作目录(默认跟随任务名;可输入新名或选已有目录复用)</label>
<select id="nt-wd-sel"> <input id="nt-wd-sel" list="folders-datalist" placeholder="目录名" autocomplete="off" />
<option value="">(留空 · 用任务名作目录)</option>
<option value="__new__">+ 新建目录…</option>
</select>
<input id="nt-wd-new" placeholder="新目录名" style="margin-top:6px;display:none;" />
<datalist id="folders-datalist"></datalist> <datalist id="folders-datalist"></datalist>
<div class="small muted" id="nt-wd-hint" style="margin-top:4px;min-height:1em;"></div> <div class="small muted" id="nt-wd-hint" style="margin-top:4px;min-height:1em;"></div>
<label for="nt-desc">描述(可选,任务长描述)</label> <label for="nt-desc">描述(可选,任务长描述)</label>
@ -2820,15 +2816,17 @@ async function uploadSelected() {
} }
// ───── new task ───── // ───── new task ─────
// wd 跟随 name 自动同步;用户一旦手动改 wd → 脱钩;若再清空 wd → 恢复跟随
let wdManuallyEdited = false;
$("hd-new").onclick = async () => { $("hd-new").onclick = async () => {
$("nt-name").value = ""; $("nt-name").value = "";
$("nt-wd-new").value = ""; $("nt-wd-new").style.display = "none"; $("nt-wd-sel").value = "";
$("nt-desc").value = ""; $("nt-skill").value = ""; $("nt-desc").value = ""; $("nt-skill").value = "";
$("nt-err").textContent = ""; $("nt-err").textContent = "";
$("nt-wd-hint").textContent = ""; $("nt-wd-hint").textContent = "";
wdManuallyEdited = false;
$("new-task-modal").classList.add("show"); $("new-task-modal").classList.add("show");
await Promise.all([loadFolderSuggestions(), loadSkillOptions(), loadModels()]); await Promise.all([loadFolderSuggestions(), loadSkillOptions(), loadModels()]);
$("nt-wd-sel").value = ""; // 默认"留空·用任务名"(loadFolderSuggestions 重渲后保险再置一次)
populateModelSelect(); populateModelSelect();
$("nt-name").focus(); $("nt-name").focus();
}; };
@ -2846,8 +2844,8 @@ function populateModelSelect() {
$("nt-cancel").onclick = () => $("new-task-modal").classList.remove("show"); $("nt-cancel").onclick = () => $("new-task-modal").classList.remove("show");
$("nt-go").onclick = async () => { $("nt-go").onclick = async () => {
const name = $("nt-name").value.trim(); const name = $("nt-name").value.trim();
const sel = $("nt-wd-sel").value; // wd 走联动 + fallback:正常情况已跟随 name;若用户清空了 wd 也兜底用 name
const working_dir = sel === "__new__" ? $("nt-wd-new").value.trim() : sel; const working_dir = $("nt-wd-sel").value.trim() || name;
const desc = $("nt-desc").value.trim(); const desc = $("nt-desc").value.trim();
const skill = $("nt-skill").value; const skill = $("nt-skill").value;
const model_profile = $("nt-model").value; const model_profile = $("nt-model").value;
@ -2865,29 +2863,22 @@ $("nt-go").onclick = async () => {
} }
}; };
// 工作目录:打开 modal 时拉一次,同时灌 modal <select>(主) + 顶部筛选用的 datalist(副) // 工作目录:打开 modal 时拉一次,灌共享的 datalist(顶部筛选 filter-wd 和 modal nt-wd-sel 都用它)
// 同时缓存到 state.folders 供 hint 判断"命中已有"/"新建"
async function loadFolderSuggestions() { async function loadFolderSuggestions() {
let folders = []; let folders = [];
try { try {
const data = await api("GET", "/v1/folders"); const data = await api("GET", "/v1/folders");
folders = data.folders || []; folders = data.folders || [];
} catch (e) { } catch (e) {
// 静默 — select 仍有默认两项 // 静默 — datalist 为空,combobox 仍可手输
} }
state.folders = folders;
const dl = $("folders-datalist"); const dl = $("folders-datalist");
dl.innerHTML = folders.map((f) => { dl.innerHTML = folders.map((f) => {
const tag = f.n_tasks ? `${f.n_tasks} 个任务` : `空目录`; const tag = f.n_tasks ? `${f.n_tasks} 个任务` : `空目录`;
return `<option value="${escapeHtml(f.name)}" data-n="${f.n_tasks}" label="${escapeHtml(tag)}"></option>`; return `<option value="${escapeHtml(f.name)}" data-n="${f.n_tasks}" label="${escapeHtml(tag)}"></option>`;
}).join(""); }).join("");
const sel = $("nt-wd-sel");
const opts = ['<option value="">(留空 · 用任务名作目录)</option>'];
for (const f of folders) {
const tag = f.n_tasks ? `${f.n_tasks} 个任务` : `空目录`;
opts.push(`<option value="${escapeHtml(f.name)}" data-n="${f.n_tasks}">${escapeHtml(f.name)} — ${escapeHtml(tag)}</option>`);
}
opts.push('<option value="__new__">+ 新建目录…</option>');
sel.innerHTML = opts.join("");
sel.value = "";
} }
// 智能体类型下拉:skill registry 服务器端静态,首次加载后缓存到 state.skills // 智能体类型下拉:skill registry 服务器端静态,首次加载后缓存到 state.skills
@ -2912,34 +2903,31 @@ async function loadSkillOptions() {
} }
function updateWdHint() { function updateWdHint() {
const sel = $("nt-wd-sel"); const v = $("nt-wd-sel").value.trim();
const v = sel.value;
const hint = $("nt-wd-hint"); const hint = $("nt-wd-hint");
const newInp = $("nt-wd-new"); if (!v) {
if (v === "__new__") { // wd 空 → 提交时会 fallback 到 name(见 nt-go);若 name 也空则不显示
newInp.style.display = "";
const nv = newInp.value.trim();
hint.innerHTML = nv
? `<span class="muted">→ 新建目录 ${escapeHtml(nv)}</span>`
: `<span class="muted">→ 输入新目录名</span>`;
} else if (v === "") {
newInp.style.display = "none";
const fallback = $("nt-name").value.trim(); const fallback = $("nt-name").value.trim();
hint.textContent = fallback ? `留空 → 用任务名「${fallback}」作目录` : ""; hint.textContent = fallback ? `→ 用任务名「${fallback}」作目录` : "";
} else { return;
newInp.style.display = "none"; }
const opt = sel.options[sel.selectedIndex]; const hit = (state.folders || []).find(f => f.name === v);
const n = opt ? (parseInt(opt.dataset.n) || 0) : 0; if (hit) {
const n = hit.n_tasks || 0;
hint.innerHTML = `<span style="color:var(--accent);">→ 复用已有目录</span> · ${n} 个任务`; hint.innerHTML = `<span style="color:var(--accent);">→ 复用已有目录</span> · ${n} 个任务`;
} else {
hint.innerHTML = `<span class="muted">→ 新建目录 ${escapeHtml(v)}</span>`;
} }
} }
$("nt-wd-sel").addEventListener("change", () => { // name 改变 → 若 wd 未被手动改,同步;并刷新 hint
updateWdHint();
if ($("nt-wd-sel").value === "__new__") $("nt-wd-new").focus();
});
$("nt-wd-new").addEventListener("input", updateWdHint);
$("nt-name").addEventListener("input", () => { $("nt-name").addEventListener("input", () => {
if ($("nt-wd-sel").value === "") updateWdHint(); if (!wdManuallyEdited) $("nt-wd-sel").value = $("nt-name").value;
updateWdHint();
});
// wd 改变 → 非空视为手动修改;清空视为"恢复跟随",仅重置 flag(不立刻填回,避免打断输入)
$("nt-wd-sel").addEventListener("input", () => {
wdManuallyEdited = $("nt-wd-sel").value.trim() !== "";
updateWdHint();
}); });
// ───── boot ───── // ───── boot ─────