feat(ui): 页内弹框替代原生 alert/confirm/prompt——分 dialog/message 两族,复用改密码卡片样式(bump 0.58.42)

新增 web/static/js/dialog.js 两族原语(对标 Element dialog/message):
- dialogConfirm/dialogPrompt:遮罩+卡片 modal,Promise 化以一比一替换同步的
  confirm/prompt;Esc/点遮罩=取消,Enter=确认,删除类走 danger 红键。
- message:顶部居中轻 toast,success/error/info 三态,自动消失、点即消。

dev.html 加 #app-dialog 容器 + #app-messages toast + 对应 CSS。全量替换主应用
8 文件调用点(chat/files/crons/procs/wechat/skills/media/auth,共 30 处)。
admin.js 独立 admin-only 页(自带 <style>、无 CSS 变量)刻意保留原生 alert
避免缺样式。纯前端加法,不动后端/接口。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
caoqianming 2026-07-19 09:38:31 +08:00
parent 6efda0a157
commit 20c7512370
13 changed files with 241 additions and 36 deletions

View File

@ -5,6 +5,10 @@
> 所以不是每个版本号都有条目。条目格式 `## <版本> — <日期>`,新条目加在最上面。
> 工程口径的完整记录见 `PROGRESS.md` / git log。
## 0.58.42 — 2026-07-19
- 提示和确认弹框换新样式:重命名、删除、清空、解绑等操作的确认框,以及各类操作提示,不再用浏览器那种顶部灰框,改为和「修改密码」一致的页内弹框;成功/失败提示改为页面顶部轻提示,几秒后自动消失。
## 0.58.41 — 2026-07-18
- 可以给任务改名了:在任务列表行或聊天区顶部的「⋯」菜单里选「重命名」即可修改任务显示名,改完列表和标题栏立即同步。改名只动显示名,不影响工作目录和已生成的文件。

View File

@ -2,7 +2,7 @@
> 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 1-2 句:做了啥 + 关键判断;细节查 `git log` / `git diff` / `DESIGN §7.9`
最后更新:2026-07-18(PROGRESS 精简:2026-07 段压回全文一行式,字符数减半,bump 0.58.40)
最后更新:2026-07-19(页内弹框替代原生 alert/confirm/prompt,新增 dialog.js,bump 0.58.42)
---
@ -23,6 +23,7 @@
### 2026-07
- **07-19 / 0.58.42**:页内弹框替代原生 alert/confirm/prompt——用户反馈原生浏览器弹框(顶部丑框)应改成页内样式,且区分两类(对标 Element `dialog`/`message`)。新增 `web/static/js/dialog.js`:`dialogConfirm`/`dialogPrompt`(遮罩+卡片 modal,复用改密码那套头/体/脚布局,Promise 化以一比一替换同步的 `confirm`/`prompt`,Esc/点遮罩=取消、Enter=确认、danger 变体红键)+ `message`(顶部居中轻量 toast,success/error/info 三态,自动消失、点即消)。dev.html 加 `#app-dialog` 容器 + `#app-messages` toast + 对应 CSS。全量替换主应用 8 文件调用点(chat/files/crons/procs/wechat/skills/media/auth,共 30 处);`admin.js`(独立 admin-only 页、独立 `<style>`、无 CSS 变量)刻意保留原生 alert 避免缺样式。纯前端加法,不动后端/接口。
- **07-18 / 0.58.41**:任务改名 UI——后端 `PATCH /v1/tasks/{id}` 早支持 `name`(validate_task_name 校验)但前端无入口;`taskMenuItems` 加「重命名」项(列表行 ⋯ 与顶栏 ⋯ 共用此菜单,一处插入两处生效),`renameTask()` 走 prompt→PATCH→更 `state.tasksById`/`taskMeta.name` + `renderChatMeta()` + `loadTaskList()` 全同步。刻意只改 DB 显示名,不动 working_dir/磁盘宪法文件(定位锚是 task_id/short_id,name 设计为自由可变标签)。仅改 `web/static/js/chat.js`
- **07-18 / 0.58.39**:JWT 滑动续签——活跃用户临期(剩余寿命 < TTL 1/4,env `ZCBOT_JWT_REFRESH_THRESHOLD_SECONDS` 可调0 ) `_authenticate` 中间件就地重签发新 token 走响应头 `X-Refreshed-Token`/`X-Token-Expires-At`,前端 `api()` 无感吸收换 `state.token`,不再因 7d TTL 掉线additive 惠及 platform + SPA(不认退回原重登行为);CORS `expose_headers`
- **07-17 / 0.58.38**:修定时简报产物文件链接点开 404——推送端 `_build_push_message` 把产物写成完整 `/v1/files/download?path=` URL,撞前端相对路径拦截器致二次包装 404。A 拦截器识别 `^/?v1/files/` 抽真 rel 救 DB 历史存量 + B 推送端源头收敛为 user_root 相对 rel(企微 send_file 独立通道不碰此链接,不受影响)。

View File

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

View File

@ -200,6 +200,54 @@
display: flex; gap: 8px; justify-content: flex-end;
}
/* ───── 通用页内弹框 #app-dialog(confirm / prompt,复用改密码头/体/脚布局)───── */
#app-dialog { z-index: 130; }
#app-dialog .card { width: 400px; max-width: calc(100vw - 32px); display: flex; flex-direction: column; }
#app-dialog h3 {
margin: 0; padding: 14px 18px; font-size: 16px;
border-bottom: 1px solid var(--border);
}
#app-dialog .body { padding: 16px 18px; }
#app-dialog .body .msg { font-size: 14px; line-height: 1.6; white-space: pre-wrap; word-break: break-word; }
#app-dialog label {
display: block; margin-bottom: 6px; font-size: 12px; color: var(--muted);
}
#app-dialog input {
width: 100%; padding: 8px 10px; border-radius: var(--r-md);
border: 1px solid var(--border); background: #fafafa;
}
#app-dialog input:focus { outline: none; border-color: var(--accent); background: #fff; }
#app-dialog .actions {
padding: 12px 18px; border-top: 1px solid var(--border);
display: flex; gap: 8px; justify-content: flex-end;
}
#app-dialog button.primary.danger { background: var(--c-red); border-color: var(--c-red); }
/* ───── 单向提示 toast #app-messages(顶部居中,自动消失,对标 Element message)───── */
#app-messages {
position: fixed; top: 20px; left: 50%; transform: translateX(-50%);
z-index: 200; display: flex; flex-direction: column; align-items: center; gap: 10px;
pointer-events: none;
}
.app-msg {
pointer-events: auto; cursor: pointer;
max-width: min(90vw, 480px);
padding: 10px 16px; border-radius: var(--r-md);
background: var(--panel); border: 1px solid var(--border);
box-shadow: 0 6px 20px rgba(0,0,0,.14);
font-size: 13px; line-height: 1.5; color: var(--text);
display: flex; align-items: center; gap: 8px;
opacity: 0; transform: translateY(-12px);
transition: opacity .25s ease, transform .25s ease;
}
.app-msg.in { opacity: 1; transform: translateY(0); }
.app-msg::before { font-weight: 700; }
.app-msg.success { border-color: var(--c-green-bd); background: var(--c-green-bg); color: #1e7e45; }
.app-msg.success::before { content: "\2713"; color: var(--c-green); }
.app-msg.error { border-color: var(--c-red-bd); background: var(--c-red-bg); color: var(--c-red); }
.app-msg.error::before { content: "\2715"; color: var(--c-red); }
.app-msg.info::before { content: "\2139"; color: var(--c-blue); }
/* ───── 左侧 rail 底部「我的资源」入口(技能,后续可加记忆)───── */
#rail-resources {
flex-shrink: 0; border-top: 1px solid var(--border);
@ -1454,6 +1502,9 @@
</div>
</div>
<!-- ───── 通用页内弹框(confirm / prompt,内容由 dialog.js 动态填)───── -->
<div id="app-dialog" class="modal"></div>
<!-- ───── 技能查看 modal ───── -->
<div id="skills-modal" class="modal">
<div class="card">

View File

@ -8,6 +8,7 @@ import { $ } from "./dom.js";
import { api } from "./api.js";
import { enterApp } from "./main.js";
import { embedPostToParent, embedShowWaiting, embedWecomRelogin } from "./embed.js";
import { message } from "./dialog.js";
// ───── login ─────
let loginTab = "pw"; // "pw" | "key";持久化 last-used tab 在 LS,刷新后默认那个
@ -222,7 +223,7 @@ async function doChangePassword() {
await api("POST", "/v1/auth/change_password", { old_password: oldPw, new_password: newPw });
closeChpwModal();
// 不登出:同一会话 JWT 仍有效,下次登录用新密码即可
alert("密码已修改,下次登录请用新密码");
message("密码已修改,下次登录请用新密码", "success");
} catch (e) {
if (e.status === 401) { closeChpwModal(); logout(); return; }
$("cp-err").textContent = e.message;

View File

@ -10,6 +10,7 @@ import { api } from "./api.js";
import { escapeHtml, fmtTime, fmtTokens, fmtTimeAgo, taskUsageTooltip, formatTaskUsage, formatContextStats, formatUsageStats, ctxRingHtml } from "./format.js";
import { renderMd, highlightIn } from "./markdown.js";
import { mqPhone, setMobileView } from "./layout.js";
import { dialogConfirm, dialogPrompt, message } from "./dialog.js";
// 微信 logo(simple-icons WeChat path),用于渠道任务徽章;fill 走 currentColor(徽章里为白)
const WECHAT_ICON = `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8.691 2.188C3.891 2.188 0 5.476 0 9.53c0 2.212 1.17 4.203 3.002 5.55a.59.59 0 0 1 .213.665l-.39 1.48c-.019.07-.048.141-.048.213 0 .163.13.295.29.295a.326.326 0 0 0 .167-.054l1.903-1.114a.864.864 0 0 1 .717-.098 10.16 10.16 0 0 0 2.837.403c.276 0 .543-.027.811-.05-.857-2.578.157-4.972 1.5-6.446 1.45-1.595 3.711-2.55 6.286-2.55.165 0 .33.01.495.027C18.486 4.916 13.929 2.188 8.691 2.188zM5.785 5.991c.642 0 1.162.529 1.162 1.18a1.17 1.17 0 0 1-1.162 1.178A1.17 1.17 0 0 1 4.623 7.17c0-.651.52-1.18 1.162-1.18zm5.813 0c.642 0 1.162.529 1.162 1.18a1.17 1.17 0 0 1-1.162 1.178 1.17 1.17 0 0 1-1.162-1.178c0-.651.52-1.18 1.162-1.18zm5.34 2.867c-1.797-.052-3.746.512-5.28 1.786-1.72 1.428-2.687 3.72-1.78 6.22.942 2.453 3.666 4.229 6.884 4.229.826 0 1.622-.121 2.361-.343a.722.722 0 0 1 .598.082l1.584.926a.272.272 0 0 0 .14.047c.134 0 .24-.111.24-.247 0-.06-.023-.12-.038-.177l-.327-1.233a.582.582 0 0 1-.023-.156.49.49 0 0 1 .201-.398C23.024 18.48 24 16.82 24 14.98c0-3.21-2.931-5.837-6.656-6.088a8.067 8.067 0 0 0-.346-.034zm-2.71 3.711c.535 0 .969.44.969.982a.976.976 0 0 1-.969.983.976.976 0 0 1-.969-.983c0-.542.434-.982.969-.982zm4.844 0c.535 0 .969.44.969.982a.976.976 0 0 1-.969.983.976.976 0 0 1-.969-.983c0-.542.434-.982.969-.982z"/></svg>`;
@ -1504,7 +1505,7 @@ async function deletePastedFile(rel, wrap) {
} catch (e) {
if (btn) btn.disabled = false;
if (e.status === 401) { logout(); return; }
alert("删除失败:" + e.message);
message("删除失败:" + e.message, "error");
}
}
@ -2513,7 +2514,11 @@ $("btn-task-menu").onclick = (e) => {
};
async function clearMessages(tid, name, nMsg) {
if (!confirm(`确认清空「${name}」的对话(${nMsg} 条消息)?\n\n将删除全部对话历史并重置 token 计数;工作目录下的文件保留。`)) return;
if (!await dialogConfirm({
title: "清空对话",
message: `确认清空「${name}」的对话(${nMsg} 条消息)?\n\n将删除全部对话历史并重置 token 计数;工作目录下的文件保留。`,
okText: "清空", danger: true,
})) return;
try {
const updated = await api("POST", "/v1/tasks/" + tid + "/clear");
if (state.taskId === tid) {
@ -2530,14 +2535,16 @@ async function clearMessages(tid, name, nMsg) {
loadTaskList();
} catch (e) {
if (e.status === 401) { logout(); return; }
alert("清空失败:" + e.message);
message("清空失败:" + e.message, "error");
}
}
async function renameTask(tid, curName) {
// 只改 tasks.name(显示名),不动 working_dir/磁盘产物 —— 定位主锚是 task_id/short_id。
// 后端 PATCH 带 validate_task_name 校验;成功后前端各处(列表/标题栏)重渲染即同步。
const next = prompt("将任务重命名为:", curName || "");
const next = await dialogPrompt({
title: "重命名任务", label: "任务名称", value: curName || "", placeholder: "输入新的任务名",
});
if (next == null) return;
const trimmed = next.trim();
if (!trimmed || trimmed === curName) return;
@ -2551,25 +2558,32 @@ async function renameTask(tid, curName) {
loadTaskList(); // 左栏列表重拉,行内名字同步
} catch (e) {
if (e.status === 401) { logout(); return; }
alert("重命名失败:" + e.message);
message("重命名失败:" + e.message, "error");
}
}
async function setTaskStatus(tid, status, name) {
const labels = { completed: "已完成", abandoned: "已废弃" };
if (!confirm(`确认将「${name || tid.slice(0,8)}」置为「${labels[status] || status}」?`)) return;
if (!await dialogConfirm({
title: "更改任务状态",
message: `确认将「${name || tid.slice(0,8)}」置为「${labels[status] || status}」?`,
})) return;
try {
await api("PATCH", "/v1/tasks/" + tid, { status });
if (state.taskId === tid) await selectTask(tid);
loadTaskList();
} catch (e) {
if (e.status === 401) { logout(); return; }
alert("操作失败:" + e.message);
message("操作失败:" + e.message, "error");
}
}
async function deleteTask(tid, name, nMsg) {
if (!confirm(`确认硬删除任务「${name}」(${nMsg} 条消息)?\n\n将清掉对话历史和 DB 行,但工作目录下的文件保留。`)) return;
if (!await dialogConfirm({
title: "删除任务",
message: `确认硬删除任务「${name}」(${nMsg} 条消息)?\n\n将清掉对话历史和 DB 行,但工作目录下的文件保留。`,
okText: "删除", danger: true,
})) return;
try {
await api("DELETE", "/v1/tasks/" + tid);
if (state.taskId === tid) {
@ -2589,7 +2603,7 @@ async function deleteTask(tid, name, nMsg) {
loadFiles();
} catch (e) {
if (e.status === 401) { logout(); return; }
alert("删除失败:" + e.message);
message("删除失败:" + e.message, "error");
}
}
@ -2597,7 +2611,7 @@ function exportTask(tid) {
fetch("/v1/tasks/" + tid + "/export", {
headers: { "Authorization": "Bearer " + state.token },
}).then(async (r) => {
if (!r.ok) { alert("导出失败:" + r.status); return; }
if (!r.ok) { message("导出失败:" + r.status, "error"); return; }
const blob = await r.blob();
const a = document.createElement("a");
a.href = URL.createObjectURL(blob);

View File

@ -7,6 +7,7 @@ import { $ } from "./dom.js";
import { api } from "./api.js";
import { escapeHtml } from "./format.js";
import { selectTask } from "./chat.js";
import { dialogConfirm, message } from "./dialog.js";
const PLACEHOLDER = '<div class="sk-empty">← 选一个定时任务查看详情</div>';
let _jobs = [];
@ -230,7 +231,7 @@ $("cr-detail").addEventListener("click", async (e) => {
await renderList();
selectJob(id);
} catch (err) {
alert("操作失败: " + err.message);
message("操作失败: " + err.message, "error");
toggle.disabled = false;
}
return;
@ -238,14 +239,18 @@ $("cr-detail").addEventListener("click", async (e) => {
if (del) {
const id = del.getAttribute("data-del");
const j = _jobs.find((x) => x.job_id === id);
if (!confirm(`删除定时任务「${j ? j.name : id}」?不可撤销(停用可保留改用「停用」)。`)) return;
if (!await dialogConfirm({
title: "删除定时任务",
message: `删除定时任务「${j ? j.name : id}」?不可撤销(停用可保留改用「停用」)。`,
okText: "删除", danger: true,
})) return;
del.disabled = true;
try {
await api("DELETE", "/v1/schedules/" + encodeURIComponent(id));
$("cr-detail").innerHTML = PLACEHOLDER;
await renderList();
} catch (err) {
alert("删除失败: " + err.message);
message("删除失败: " + err.message, "error");
del.disabled = false;
}
}

115
web/static/js/dialog.js Normal file
View File

@ -0,0 +1,115 @@
// 页内弹框原语:替代原生 alert/confirm/prompt。分两类(对标 Element 的 dialog / message):
// - dialogConfirm / dialogPrompt:需要交互(确认 / 输入)→ 遮罩 + 卡片 modal,复用 .modal/.card。
// - message:单向提示(成功 / 报错 / 信息)→ 顶部居中轻量 toast,自动消失。
// 复用改密码 modal 那套头/体/脚布局;单例容器 #app-dialog 每次调用重建内容,同一时刻只开一个。
import { $ } from "./dom.js";
import { escapeHtml } from "./format.js";
// ───── 交互式 modal(confirm / prompt)─────
let _resolve = null; // 当前打开弹框的 resolve
let _onKey = null; // 当前绑定的 keydown 处理器(关闭时解绑)
function closeDialog(result) {
const wrap = $("app-dialog");
wrap.classList.remove("show");
wrap.innerHTML = "";
if (_onKey) { document.removeEventListener("keydown", _onKey, true); _onKey = null; }
const r = _resolve; _resolve = null;
if (r) r(result);
}
// 内部通用打开:kind = "confirm" | "prompt"
function openDialog(kind, opts) {
const {
title = "",
message = "",
label = "",
value = "",
placeholder = "",
okText = "确认",
cancelText = "取消",
danger = false,
} = opts || {};
// 若已有弹框未关(极少见:非阻塞下重复触发),先把旧的按取消收掉
if (_resolve) closeDialog(kind === "prompt" ? null : false);
const wrap = $("app-dialog");
const isPrompt = kind === "prompt";
const bodyHtml = isPrompt
? `${label ? `<label for="app-dialog-input">${escapeHtml(label)}</label>` : ""}
<input id="app-dialog-input" type="text" autocomplete="off" />`
: `<div class="msg">${escapeHtml(message)}</div>`;
wrap.innerHTML = `
<div class="card" role="dialog" aria-modal="true">
${title ? `<h3>${escapeHtml(title)}</h3>` : ""}
<div class="body">${bodyHtml}</div>
<div class="actions">
<button id="app-dialog-cancel">${escapeHtml(cancelText)}</button>
<button id="app-dialog-ok" class="primary${danger ? " danger" : ""}">${escapeHtml(okText)}</button>
</div>
</div>`;
wrap.classList.add("show");
const inp = isPrompt ? $("app-dialog-input") : null;
if (inp) { inp.value = value; inp.placeholder = placeholder; }
const ok = () => closeDialog(isPrompt ? (inp.value) : true);
const cancel = () => closeDialog(isPrompt ? null : false);
$("app-dialog-ok").onclick = ok;
$("app-dialog-cancel").onclick = cancel;
wrap.onclick = (e) => { if (e.target === wrap) cancel(); }; // 点遮罩 = 取消
_onKey = (e) => {
if (e.key === "Escape") { e.stopPropagation(); cancel(); }
else if (e.key === "Enter" && (!isPrompt || e.target === inp)) { e.preventDefault(); ok(); }
};
document.addEventListener("keydown", _onKey, true);
// 聚焦:输入框选中已有文字(方便重命名整段改),否则聚焦确认键
if (inp) { inp.focus(); inp.select(); }
else $("app-dialog-ok").focus();
return new Promise((res) => { _resolve = res; });
}
// 确认框 → Promise<boolean>
export function dialogConfirm(opts) {
return openDialog("confirm", typeof opts === "string" ? { message: opts } : opts);
}
// 输入框 → Promise<string|null>(取消返回 null;确认返回原始输入,调用方自行 trim)
export function dialogPrompt(opts) {
return openDialog("prompt", opts);
}
// 供 main.js Esc 栈查询 / 关闭(与其它 modal 统一)
export function isDialogOpen() { return !!_resolve; }
export function closeTopDialog() { if (_resolve) closeDialog(false); }
// ───── 单向提示 toast(message)─────
// type: "info" | "success" | "error";顶部居中,自动消失。
export function message(text, type = "info", duration = 3200) {
let wrap = $("app-messages");
if (!wrap) {
wrap = document.createElement("div");
wrap.id = "app-messages";
document.body.appendChild(wrap);
}
const el = document.createElement("div");
el.className = "app-msg " + type;
el.textContent = text;
wrap.appendChild(el);
// 触发进入动画
requestAnimationFrame(() => el.classList.add("in"));
const kill = () => {
el.classList.remove("in");
el.addEventListener("transitionend", () => el.remove(), { once: true });
setTimeout(() => el.remove(), 400); // 兜底(transition 未触发时)
};
setTimeout(kill, duration);
el.onclick = kill; // 点一下立刻消
}

View File

@ -13,6 +13,7 @@ import { logout } from "./auth.js";
import { downloadFile } from "./media.js";
import { selectTask, loadTaskList } from "./chat.js";
import { loadFolderSuggestions } from "./newtask.js";
import { dialogConfirm, dialogPrompt, message } from "./dialog.js";
// ───── files(user-rooted,不绑 task) ─────
$("btn-refresh-files").onclick = () => loadFiles();
@ -128,7 +129,7 @@ async function doSrcTransfer(mode) {
await loadFolderSuggestions();
} catch (e) {
if (e.status === 401) { logout(); return; }
alert(verb + "失败:" + e.message);
message(verb + "失败:" + e.message, "error");
}
}
@ -285,7 +286,7 @@ function fileMenuItems(e) {
async function deleteFile(rel, name, isDir) {
let recursive = false;
if (!isDir) {
if (!confirm(`确认删除文件 "${name}"?`)) return;
if (!await dialogConfirm({ title: "删除文件", message: `确认删除文件 "${name}"?`, okText: "删除", danger: true })) return;
} else {
// 探一下目录内容:空目录走普通 rmdir;非空才递归,二次确认显示条目数
let entries;
@ -294,19 +295,22 @@ async function deleteFile(rel, name, isDir) {
entries = data.entries || [];
} catch (e) {
if (e.status === 401) { logout(); return; }
alert("读目录失败:" + e.message);
message("读目录失败:" + e.message, "error");
return;
}
if (entries.length === 0) {
if (!confirm(`确认删除空目录 "${name}"?`)) return;
if (!await dialogConfirm({ title: "删除目录", message: `确认删除空目录 "${name}"?`, okText: "删除", danger: true })) return;
} else {
const hasSub = entries.some((x) => x.is_dir);
const tip = hasSub ? "(含子目录)" : "";
if (!confirm(
`目录 "${name}" 含 ${entries.length}${tip},` +
`将递归删除全部内容,不可恢复。\n` +
`(若为顶层目录且仍被 task 引用,需先删 task)\n确认?`
)) return;
if (!await dialogConfirm({
title: "删除目录",
message:
`目录 "${name}" 含 ${entries.length}${tip},` +
`将递归删除全部内容,不可恢复。\n` +
`(若为顶层目录且仍被 task 引用,需先删 task)\n确认?`,
okText: "递归删除", danger: true,
})) return;
recursive = true;
}
}
@ -317,13 +321,15 @@ async function deleteFile(rel, name, isDir) {
await loadFolderSuggestions();
} catch (e) {
if (e.status === 401) { logout(); return; }
alert("删除失败:" + e.message);
message("删除失败:" + e.message, "error");
}
}
async function renameFile(rel, name, isDir) {
const what = isDir ? "目录" : "文件";
const newName = prompt(`${what} "${name}" 重命名为:`, name);
const newName = await dialogPrompt({
title: `重命名${what}`, label: `${what}名称`, value: name, placeholder: `输入新的${what}`,
});
if (newName == null) return;
const trimmed = newName.trim();
if (!trimmed || trimmed === name) return;
@ -344,7 +350,7 @@ async function renameFile(rel, name, isDir) {
await loadFiles();
} catch (e) {
if (e.status === 401) { logout(); return; }
alert("重命名失败:" + e.message);
message("重命名失败:" + e.message, "error");
}
}
@ -424,7 +430,7 @@ export async function uploadFiles(files, opts = {}) {
return data.saved || [];
} catch (e) {
if (e.status === 401) { logout(); return null; }
alert("上传失败:" + e.message);
message("上传失败:" + e.message, "error");
return null;
}
}

View File

@ -7,6 +7,7 @@ import { state } from "./state.js";
import { escapeHtml } from "./format.js";
import { _categorize } from "./preview.js";
import { logout } from "./auth.js";
import { message } from "./dialog.js";
// ───── artifact 抽取(对话内 chip → 复用文件预览 modal) ─────
// task.working_dir 在 DB 是 `workspace/users/<uuid>/<name>` 形态(to_db_path),
@ -239,7 +240,7 @@ export function downloadFile(rel) {
fetch("/v1/files/download?path=" + encodeURIComponent(rel), {
headers: { "Authorization": "Bearer " + state.token },
}).then(async (r) => {
if (!r.ok) { alert("下载失败:" + r.status); return; }
if (!r.ok) { message("下载失败:" + r.status, "error"); return; }
const blob = await r.blob();
const a = document.createElement("a");
a.href = URL.createObjectURL(blob);

View File

@ -12,6 +12,7 @@ import { state } from "./state.js";
import { $ } from "./dom.js";
import { escapeHtml } from "./format.js";
import { selectTask, syncBgprocLock } from "./chat.js";
import { dialogConfirm } from "./dialog.js";
const POLL_MS = 5000;
let _pollTimer = null;
@ -52,7 +53,7 @@ export async function killTaskProcs(taskId) {
(p) => p.task_id === taskId && p.status === "running"
);
if (!running.length) return;
if (!confirm("确定终止后台进程?已产生的输出会保留,进程本身无法恢复。")) return;
if (!await dialogConfirm({ title: "终止后台进程", message: "确定终止后台进程?已产生的输出会保留,进程本身无法恢复。", okText: "终止", danger: true })) return;
for (const p of running) {
try {
await api("POST", `/v1/tasks/${p.task_id}/procs/${p.proc_id}/kill`);
@ -105,7 +106,7 @@ export function decorateBgprocCard(det, txt) {
async function killProc(procId, btn) {
const p = _byId.get(procId);
if (!p) return;
if (!confirm("确定终止这个后台进程?已产生的输出会保留,进程本身无法恢复。")) return;
if (!await dialogConfirm({ title: "终止后台进程", message: "确定终止这个后台进程?已产生的输出会保留,进程本身无法恢复。", okText: "终止", danger: true })) return;
btn.disabled = true;
try {
await api("POST", `/v1/tasks/${p.task_id}/procs/${procId}/kill`);

View File

@ -8,6 +8,7 @@ import { state } from "./state.js";
import { api } from "./api.js";
import { escapeHtml } from "./format.js";
import { renderMd, highlightIn } from "./markdown.js";
import { dialogConfirm, message } from "./dialog.js";
const PLACEHOLDER = '<div class="sk-empty">← 从左侧选一个 skill 查看完整说明</div>';
@ -110,7 +111,11 @@ $("sk-detail").addEventListener("click", async (e) => {
const del = e.target.closest("[data-del]");
if (!del) return;
const name = del.getAttribute("data-del");
if (!confirm(`删除你的 skill「${name}」?不可撤销(平台同名 skill 不受影响)。`)) return;
if (!await dialogConfirm({
title: "删除 skill",
message: `删除你的 skill「${name}」?不可撤销(平台同名 skill 不受影响)。`,
okText: "删除", danger: true,
})) return;
del.disabled = true;
try {
await api("DELETE", "/v1/skills/" + encodeURIComponent(name));
@ -118,7 +123,7 @@ $("sk-detail").addEventListener("click", async (e) => {
$("sk-detail").innerHTML = PLACEHOLDER;
renderList();
} catch (err) {
alert("删除失败: " + err.message);
message("删除失败: " + err.message, "error");
del.disabled = false;
}
});

View File

@ -3,6 +3,7 @@
// GET /v1/wechat/bind、DELETE /v1/wechat/bind、POST /v1/wechat/test。
import { $ } from "./dom.js";
import { api } from "./api.js";
import { dialogConfirm } from "./dialog.js";
let _polling = false;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
@ -141,7 +142,7 @@ $("wechat-modal").addEventListener("click", (e) => {
$("wx-bind").onclick = bindFlow;
$("wx-unbind").onclick = async () => {
_polling = false;
if (!confirm("确定解绑微信?")) return;
if (!await dialogConfirm({ title: "解绑微信", message: "确定解绑微信?", okText: "解绑", danger: true })) return;
try {
await api("DELETE", "/v1/wechat/bind");
$("wx-qrbox").hidden = true;
@ -172,7 +173,7 @@ $("wc-bind-id").onclick = async () => {
finally { $("wc-bind-id").disabled = false; }
};
$("wc-unbind").onclick = async () => {
if (!confirm("确定解绑企业微信?")) return;
if (!await dialogConfirm({ title: "解绑企业微信", message: "确定解绑企业微信?", okText: "解绑", danger: true })) return;
try { await api("DELETE", "/v1/wecom/bind"); await refreshWecom(); }
catch (e) { setWc("err", "解绑失败: " + e.message); }
};