return JSONResponse({"ok": True, "task": task})
+@app.put("/api/tasks/{task_id}")
+async def api_update_task(task_id: str, request: Request):
+ """更新任务:名称(仅改显示名,不移动工作目录)与描述(最多 50 个汉字)。"""
+ if not _auth_ok(request):
+ return JSONResponse({"ok": False, "error": "未授权"}, status_code=401)
+ try:
+ body = await request.json()
+ except Exception:
+ return JSONResponse({"ok": False, "error": "无效的请求体"}, status_code=400)
+ data = load_tasks()
+ t = find_task(data, task_id)
+ if not t:
+ return JSONResponse({"ok": False, "error": "任务不存在"}, status_code=404)
+
+ name = body.get("name")
+ if name is not None:
+ name = name.strip()
+ if not name:
+ return JSONResponse({"ok": False, "error": "名称不能为空"}, status_code=400)
+ if "/" in name or "\\" in name or name in (".", ".."):
+ return JSONResponse({"ok": False, "error": "名称含有非法字符"}, status_code=400)
+ # 与除自身外的其它任务重名则拒绝
+ if any(x["id"] != task_id and x["name"] == name for x in data["tasks"]):
+ return JSONResponse({"ok": False, "error": "任务名已存在"}, status_code=409)
+ t["name"] = name # 仅改显示名,保留原 cwd,避免移动目录破坏会话历史
+
+ desc = body.get("description")
+ if desc is not None:
+ desc = desc.strip()
+ if len(desc) > 50:
+ return JSONResponse({"ok": False, "error": "描述最多 50 个汉字"}, status_code=400)
+ t["description"] = desc
+
+ save_tasks(data)
+ logger.info(f"更新任务: id={task_id}, name={t.get('name')}, desc_len={len(t.get('description', ''))}")
+ return JSONResponse({"ok": True, "task": t})
+
+
@app.delete("/api/tasks/{task_id}")
async def api_delete_task(task_id: str, request: Request):
if not _auth_ok(request):
.task-card.tc-idle .tc-status { color: #777; }
.task-card.tc-done { border-color: #ffa94d; }
.task-card.tc-done .tc-status { color: #ffa94d; }
+.task-card .tc-desc { font-size: 12px; color: #888; line-height: 1.4; }
+.task-card .tc-desc-empty { color: #555; font-style: italic; }
+.task-card .tc-edit {
+ position: absolute; top: 10px; right: 10px; width: 28px; height: 28px;
+ display: none; align-items: center; justify-content: center;
+ background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.15);
+ border-radius: 6px; color: #4ecca3; font-size: 14px; cursor: pointer;
+}
+.task-card:hover .tc-edit { display: flex; }
+.task-card .tc-edit:hover { background: rgba(78,204,163,0.18); color: #fff; }
+.task-card .tc-input {
+ width: 100%; box-sizing: border-box; background: #0f0f1e; color: #e0e0e0;
+ border: 1px solid rgba(255,255,255,0.2); border-radius: 6px; padding: 6px 8px;
+ font-size: 14px; font-family: inherit;
+}
+.task-card .tc-name-input { font-weight: 600; color: #4ecca3; }
+.task-card .tc-desc-wrap { display: flex; align-items: center; gap: 6px; }
+.task-card .tc-desc-wrap .tc-input { flex: 1; min-width: 0; }
+.task-card .tc-counter { font-size: 11px; color: #666; white-space: nowrap; }
+.task-card .tc-edit-actions { display: flex; gap: 8px; margin-top: 12px; }
+.task-card .tc-save { background: linear-gradient(135deg, #4ecca3, #38b2ac); color: #1a1a2e; border: none; flex: 1; padding: 9px; border-radius: 8px; cursor: pointer; font-size: 14px; font-weight: 500; }
+.task-card .tc-save:hover { opacity: 0.9; }
+.task-card .tc-cancel { background: rgba(255,255,255,0.08); color: #ccc; border: 1px solid rgba(255,255,255,0.15); flex: 0 0 56px; padding: 9px; border-radius: 8px; cursor: pointer; font-size: 14px; }
.task-new {
display: flex; align-items: center; justify-content: center; gap: 8px;
background: rgba(78,204,163,0.06); border: 2px dashed rgba(78,204,163,0.3);
card.innerHTML =
'<div class="tc-name">' + escapeHtml(t.name) + '</div>' +
'<div class="tc-cwd">' + escapeHtml(t.cwd) + '</div>' +
+ '<div class="tc-desc">' + (t.description ? escapeHtml(t.description) : '<span class="tc-desc-empty">添加描述…</span>') + '</div>' +
'<div class="tc-time">' + last + (pausedInfo ? ' · ' + pausedInfo : '') + '</div>' +
'<div class="tc-status">' + statusLine + '</div>' +
+ '<button class="tc-edit" title="编辑名称与描述">✎</button>' +
'<div class="tc-actions">' +
'<button class="tc-btn tc-enter">进入</button>' +
(isPaused
(canDelete ? '<button class="tc-btn tc-del" title="删除任务">✕</button>' : '') +
'</div>';
card.querySelector('.tc-enter').onclick = () => openTask(t.id);
+ card.querySelector('.tc-edit').onclick = (e) => { e.stopPropagation(); enterEditMode(card, t); };
if (isPaused) {
card.querySelector('.tc-resume').onclick = () => resumeTask(t.id);
if (canDelete) card.querySelector('.tc-del').onclick = () => startDeleteTask(t);
await renderTasks();
}
+ // 进入卡片内联编辑模式:编辑名称与描述
+ function enterEditMode(card, t) {
+ card.innerHTML =
+ '<div><input class="tc-input tc-name-input" value="' + escapeHtml(t.name) + '" maxlength="40"></div>' +
+ '<div class="tc-desc-wrap"><input class="tc-input" maxlength="50" placeholder="任务描述(最多50字)" value="' + escapeHtml(t.description || '') + '">' +
+ '<span class="tc-counter">0/50</span></div>' +
+ '<div class="tc-edit-actions">' +
+ '<button class="tc-btn tc-save">保存</button>' +
+ '<button class="tc-btn tc-cancel">取消</button>' +
+ '</div>';
+ const nameInput = card.querySelector('.tc-name-input');
+ const descInput = card.querySelector('.tc-desc-wrap input');
+ const counter = card.querySelector('.tc-counter');
+ const updateCounter = () => { counter.textContent = descInput.value.length + '/50'; };
+ updateCounter();
+ descInput.addEventListener('input', updateCounter);
+ nameInput.focus();
+ nameInput.select();
+ card.querySelector('.tc-cancel').onclick = () => renderTasks();
+ card.querySelector('.tc-save').onclick = async () => {
+ const newName = nameInput.value.trim();
+ const newDesc = descInput.value;
+ if (!newName) { alert('名称不能为空'); return; }
+ const res = await fetch(API + '/api/tasks/' + t.id, {
+ method: 'PUT', headers: authHeaders(),
+ body: JSON.stringify({ name: newName, description: newDesc }),
+ });
+ if (!res.ok) { const d = await res.json().catch(()=>({})); alert(d.error || '保存失败'); return; }
+ await renderTasks();
+ };
+ }
+
function escapeHtml(s) {
return String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
}