diff --git a/README.md b/README.md index 1624a7719cfa69838bd447f945a78734dd36b8f0..31040f2a2677fa00993a26496c90d2b24dfac2ef 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ pinned: false # Global Marine Foundation Data Agent -Current UI release: **v4.6.8**. +Current UI release: **v4.6.9**. ## v3.4.0 稳定性重构(第一阶段) diff --git a/VERSION b/VERSION index d3acad059334fc5d7ea299f10be58d683ff257b2..ded82a977f159f27052c0b1e6189b602a4eee13b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.6.8 +4.6.9 diff --git a/routes/manual_tasks.py b/routes/manual_tasks.py new file mode 100644 index 0000000000000000000000000000000000000000..d52cd79fc4e0f3431fecb91bbfe396bc21f0cfe2 --- /dev/null +++ b/routes/manual_tasks.py @@ -0,0 +1,66 @@ +from __future__ import annotations +from fastapi import HTTPException, Request + + +def _inject(context): + globals().update(context) + + +async def create_manual_task(request:Request): + try: + body=await request.json() + except Exception: + raise HTTPException(400,"invalid json") + uid,_=await resolve_request_user(request,str(body.get("user_id") or "")) + try: + task=MANUAL_TASK_STORE.create( + uid, + body.get("title"), + body.get("description") or "", + body.get("status") or "pending", + ) + except ValueError as exc: + raise HTTPException(400,str(exc)) + try: + await safe_memory_event(uid,"manual_task_created",{"task_id":task["task_id"],"title":task["title"],"status":task["status"]}) + except Exception: + pass + return {"status":"ok","task":task,"storage":MANUAL_TASK_STORE.storage_mode} + + +async def update_manual_task(task_id:str,request:Request): + try: + body=await request.json() + except Exception: + raise HTTPException(400,"invalid json") + uid,_=await resolve_request_user(request,str(body.get("user_id") or "")) + try: + task=MANUAL_TASK_STORE.update(uid,task_id,body) + except ValueError as exc: + raise HTTPException(400,str(exc)) + if not task: + raise HTTPException(404,"manual task not found") + try: + await safe_memory_event(uid,"manual_task_updated",{"task_id":task_id,"status":task.get("status")}) + except Exception: + pass + return {"status":"ok","task":task,"storage":MANUAL_TASK_STORE.storage_mode} + + +async def delete_manual_task(task_id:str,request:Request): + supplied=str(request.query_params.get("user_id") or "").strip() + uid,_=await resolve_request_user(request,supplied) + if not MANUAL_TASK_STORE.delete(uid,task_id): + raise HTTPException(404,"manual task not found") + try: + await safe_memory_event(uid,"manual_task_deleted",{"task_id":task_id}) + except Exception: + pass + return {"status":"ok","task_id":task_id} + + +def register_manual_task_routes(app,context): + _inject(context) + app.add_api_route("/api/sidebar/tasks/manual",create_manual_task,methods=["POST"]) + app.add_api_route("/api/sidebar/tasks/manual/{task_id}",update_manual_task,methods=["PATCH"]) + app.add_api_route("/api/sidebar/tasks/manual/{task_id}",delete_manual_task,methods=["DELETE"]) diff --git a/services/manual_tasks.py b/services/manual_tasks.py new file mode 100644 index 0000000000000000000000000000000000000000..21b55b006f89aed0fbf982dfac79edf95c27bca8 --- /dev/null +++ b/services/manual_tasks.py @@ -0,0 +1,109 @@ +from __future__ import annotations + +import json +import secrets +import time +from datetime import datetime +from pathlib import Path +from threading import RLock +from typing import Any + + +class ManualTaskStore: + VALID_STATUSES={"pending","running","completed","failed"} + + def __init__(self, root:Path, valid_user_id, max_tasks:int=300): + self.root=Path(root)/"manual_tasks" + self.root.mkdir(parents=True,exist_ok=True) + self.valid_user_id=valid_user_id + self.max_tasks=max(20,int(max_tasks)) + self.lock=RLock() + + @property + def storage_mode(self)->str: + return "persistent" if str(self.root).startswith("/data/") else "server-session" + + def _file(self,user_id:str)->Path: + if not self.valid_user_id(user_id): + raise ValueError("invalid user_id") + return self.root/f"{user_id}.json" + + def _read(self,user_id:str)->list[dict[str,Any]]: + path=self._file(user_id) + try: + raw=json.loads(path.read_text(encoding="utf-8")) + return raw if isinstance(raw,list) else [] + except Exception: + return [] + + def _write(self,user_id:str,tasks:list[dict[str,Any]])->None: + path=self._file(user_id) + tmp=path.with_suffix(".tmp") + tmp.write_text(json.dumps(tasks[:self.max_tasks],ensure_ascii=False,indent=2),encoding="utf-8") + tmp.replace(path) + + def _clean_text(self,value:Any,limit:int)->str: + return " ".join(str(value or "").replace("\x00","").split())[:limit] + + def _normalize_status(self,value:Any)->str: + status=str(value or "pending").strip().lower() + return status if status in self.VALID_STATUSES else "pending" + + def list_user(self,user_id:str,limit:int=200)->list[dict[str,Any]]: + with self.lock: + tasks=self._read(user_id) + tasks.sort(key=lambda x:str(x.get("updated_at") or x.get("created_at") or ""),reverse=True) + return tasks[:max(1,min(int(limit or 200),self.max_tasks))] + + def create(self,user_id:str,title:str,description:str="",status:str="pending")->dict[str,Any]: + now=datetime.now().astimezone().isoformat(timespec="seconds") + task={ + "kind":"manual", + "task_id":"manual_"+secrets.token_hex(8), + "user_id":user_id, + "title":self._clean_text(title,160), + "description":self._clean_text(description,4000), + "status":self._normalize_status(status), + "source":"manual", + "created_at":now, + "updated_at":now, + } + if not task["title"]: + raise ValueError("title is required") + with self.lock: + tasks=self._read(user_id) + tasks=[task]+[x for x in tasks if x.get("task_id")!=task["task_id"]] + self._write(user_id,tasks) + return task + + def update(self,user_id:str,task_id:str,patch:dict[str,Any])->dict[str,Any]|None: + with self.lock: + tasks=self._read(user_id) + target=None + for task in tasks: + if str(task.get("task_id") or "")==task_id: + target=task + break + if target is None: + return None + if "title" in patch: + title=self._clean_text(patch.get("title"),160) + if not title: + raise ValueError("title is required") + target["title"]=title + if "description" in patch: + target["description"]=self._clean_text(patch.get("description"),4000) + if "status" in patch: + target["status"]=self._normalize_status(patch.get("status")) + target["updated_at"]=datetime.now().astimezone().isoformat(timespec="seconds") + self._write(user_id,tasks) + return dict(target) + + def delete(self,user_id:str,task_id:str)->bool: + with self.lock: + tasks=self._read(user_id) + kept=[x for x in tasks if str(x.get("task_id") or "")!=task_id] + if len(kept)==len(tasks): + return False + self._write(user_id,kept) + return True diff --git a/static/js/app.js b/static/js/app.js index ce27324301d306e605147cf1cfb33defd39b2a74..112403ac018b43c721ab93c33036266c580f1fb3 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -1087,8 +1087,8 @@ async function renderServices(){ const TASK_OPERATION_ZH={quality_check:"数据质量检查",marine_export:"海洋数据导出",marine_query:"海洋数据查询",marine_subset:"海洋区域裁剪",fisheries_query:"渔业数据查询",task:"数据处理任务"}; function taskOperationLabel(v){return TASK_OPERATION_ZH[String(v||"")]||String(v||"数据处理任务")} -function taskStatusGroup(v){const s=String(v||"");if(/completed|done|success|成功|完成/i.test(s))return "completed";if(/failed|error|失败|异常/i.test(s))return "failed";return "running"} -function taskStatusLabel(v){const g=taskStatusGroup(v);return g==="completed"?"已完成":g==="failed"?"失败":"处理中"} +function taskStatusGroup(v){const s=String(v||"");if(/completed|done|success|成功|完成/i.test(s))return "completed";if(/failed|error|失败|异常/i.test(s))return "failed";if(/pending|todo|queued|待处理|等待/i.test(s))return "pending";return "running"} +function taskStatusLabel(v){const g=taskStatusGroup(v);return g==="completed"?"已完成":g==="failed"?"失败":g==="pending"?"待处理":"处理中"} function taskProgressHtml(task){ const g=taskStatusGroup(task.status),explicit=Number(task.progress); if(task.progress!==null&&task.progress!==undefined&&Number.isFinite(explicit))return '
处理进度:'+esc(explicit)+'%
'; @@ -1119,21 +1119,68 @@ function assetContinuePrompt(asset){return '请分析这个已生成的数据资 function openAssetDetail(asset){ document.querySelector(".drawer-backdrop")?.remove();const wrap=document.createElement("div");wrap.className="drawer-backdrop";wrap.innerHTML='';document.body.appendChild(wrap);const close=()=>wrap.remove();wrap.querySelector(".drawer-close").onclick=close;wrap.onclick=e=>{if(e.target===wrap)close()};wrap.querySelector(".asset-chat").onclick=()=>{close();goChat(assetContinuePrompt(asset))} } +function openManualTaskEditor(task=null){ + document.querySelector(".drawer-backdrop")?.remove(); + const editing=!!task; + const wrap=document.createElement("div");wrap.className="drawer-backdrop"; + wrap.innerHTML=''; + document.body.appendChild(wrap); + const close=()=>wrap.remove(); + wrap.querySelector(".drawer-close").onclick=close; + wrap.querySelector(".manual-task-cancel").onclick=close; + wrap.onclick=e=>{if(e.target===wrap)close()}; + const status=wrap.querySelector("#manualTaskStatus");status.value=task?.status||"pending"; + wrap.querySelector("#manualTaskSave").onclick=async()=>{ + const title=wrap.querySelector("#manualTaskTitle").value.trim(); + const description=wrap.querySelector("#manualTaskDescription").value.trim(); + if(!title){toast("请填写任务名称");wrap.querySelector("#manualTaskTitle").focus();return} + const btn=wrap.querySelector("#manualTaskSave");btn.disabled=true; + try{ + if(editing){ + await j("/api/sidebar/tasks/manual/"+encodeURIComponent(task.task_id),{method:"PATCH",body:JSON.stringify({user_id:USER_ID,title,description,status:status.value})}); + toast("任务已更新"); + }else{ + await j("/api/sidebar/tasks/manual",{method:"POST",body:JSON.stringify({user_id:USER_ID,title,description,status:status.value})}); + toast("任务已创建"); + } + close();await renderTasks(); + }catch(err){toast("保存失败:"+(err.message||String(err)))} + finally{btn.disabled=false} + }; + setTimeout(()=>wrap.querySelector("#manualTaskTitle")?.focus(),0); +} + +async function deleteManualTask(task){ + if(!confirm('确认删除任务“'+(task.title||"未命名任务")+'”?'))return; + try{ + await j("/api/sidebar/tasks/manual/"+encodeURIComponent(task.task_id)+"?user_id="+encodeURIComponent(USER_ID),{method:"DELETE"}); + toast("任务已删除");await renderTasks(); + }catch(err){toast("删除失败:"+(err.message||String(err)))} +} + async function renderTasks(){ - const root=$("workspaceInner");root.innerHTML='

我的任务

查看当前登录用户的数据处理任务和生成的数据资产。

正在读取任务记录…
';$("refreshTasks").onclick=renderTasks; + const root=$("workspaceInner");root.innerHTML='

我的任务

查看当前登录用户的数据处理任务和生成的数据资产。

正在读取任务记录…
';$("refreshTasks").onclick=renderTasks;$("newManualTask").onclick=()=>openManualTaskEditor(); try{ - const data=await j("/api/sidebar/tasks?user_id="+encodeURIComponent(USER_ID)),tasks=data.tasks||[],assets=data.assets||[],localTasks=data.local_tasks||[],allStatus=[...tasks,...localTasks],running=allStatus.filter(x=>["running","queued","transferring","paused"].includes(String(x.status||"").toLowerCase())).length,failed=allStatus.filter(x=>["failed","partial"].includes(String(x.status||"").toLowerCase())).length,completed=allStatus.filter(x=>String(x.status||"").toLowerCase()==="completed").length; - let html='

我的任务

'+esc(data.enabled?"任务、结果和数据资产已按当前用户关联":"任务记录服务尚未启用")+'

'+(tasks.length+localTasks.length)+'全部任务
'+running+'处理中
'+completed+'已完成
'+failed+'失败
'+assets.length+'数据资产
'; + const data=await j("/api/sidebar/tasks?user_id="+encodeURIComponent(USER_ID)),tasks=data.tasks||[],assets=data.assets||[],localTasks=data.local_tasks||[],allStatus=[...tasks,...localTasks],pending=allStatus.filter(x=>taskStatusGroup(x.status)==="pending").length,running=allStatus.filter(x=>taskStatusGroup(x.status)==="running").length,failed=allStatus.filter(x=>taskStatusGroup(x.status)==="failed").length,completed=allStatus.filter(x=>taskStatusGroup(x.status)==="completed").length; + let html='

我的任务

'+esc(data.enabled?"任务、结果和数据资产已按当前用户关联":"任务记录服务尚未启用")+'

'+(tasks.length+localTasks.length)+'全部任务
'+pending+'待处理
'+running+'处理中
'+completed+'已完成
'+failed+'失败
'+assets.length+'数据资产
手动任务存储:'+esc(data.manual_task_storage==="persistent"?"账号服务器持久化(可跨设备)":"服务器会话存储(未挂载持久卷)")+'。
'; if(data.error)html+='
'+esc(data.error)+'
'; html+='

持久化任务

处理任务

数据资产

'; - root.innerHTML=html;$("refreshTasks").onclick=renderTasks; + root.innerHTML=html;$("refreshTasks").onclick=renderTasks;$("newManualTask").onclick=()=>openManualTaskEditor(); const drawPersistent=()=>{ const box=$("persistentTaskList");if(!box)return; if(!localTasks.length){box.innerHTML='
暂无持久化任务Ocean 批量导出和大文件下载任务会保存在这里。
';return} box.innerHTML=localTasks.map((x,i)=>{ + if(x.kind==="manual"){const g=taskStatusGroup(x.status);return '
'+esc(x.title||"手动任务")+''+esc(taskStatusLabel(x.status))+'
'+esc(x.task_id||"")+' · 手动创建 · '+esc(formatTime(x.updated_at||x.created_at))+'
'+esc(x.description||"暂无任务说明")+'
'+(x.status!=="completed"?'':'')+'
'} if(x.kind==="ocean_batch"){const c=x.counts||{},pct=Number(x.progress_percent||0);return '
Ocean 批量导出'+esc(x.status||"-")+'
'+esc(x.job_id||"")+' · '+esc(formatTime(x.updated_at||x.created_at))+(x.thread_id?' · 关联会话 '+esc(x.thread_id):'')+'
'+esc(x.prompt||"")+'
'+pct.toFixed(0)+'% · 完成 '+esc(c.completed||0)+' / '+esc(x.total||0)+' · 失败 '+esc(c.failed||0)+'
'+(x.status==="paused"?'':'')+((c.failed||0)>0?'':'')+''+(x.thread_id?'':'')+'
'} return '
大文件下载'+esc(x.status||"-")+'
'+esc(x.job_id||"")+' · '+esc(x.filename||"download")+' · '+esc(formatTime(x.updated_at))+'
已传输 '+esc(formatBytes(x.transferred_bytes||0))+(x.total_bytes?' / '+esc(formatBytes(x.total_bytes)):'')+(x.resume_supported?' · 支持断点续传':'')+'
' }).join(''); + box.querySelectorAll('[data-manual-index]').forEach(card=>{const x=localTasks[Number(card.dataset.manualIndex)]; + card.querySelector('.manual-edit')?.addEventListener('click',()=>openManualTaskEditor(x)); + card.querySelector('.manual-delete')?.addEventListener('click',()=>deleteManualTask(x)); + card.querySelector('.manual-chat')?.addEventListener('click',()=>goChat('请帮我继续处理这个手动任务。\n任务:'+(x.title||'')+'\n说明:'+(x.description||'')+'\n当前状态:'+taskStatusLabel(x.status))); + card.querySelector('.manual-complete')?.addEventListener('click',async()=>{await j('/api/sidebar/tasks/manual/'+encodeURIComponent(x.task_id),{method:'PATCH',body:JSON.stringify({user_id:USER_ID,status:'completed'})});toast('已标记完成');renderTasks()}); + card.querySelector('.manual-reopen')?.addEventListener('click',async()=>{await j('/api/sidebar/tasks/manual/'+encodeURIComponent(x.task_id),{method:'PATCH',body:JSON.stringify({user_id:USER_ID,status:'pending'})});toast('任务已重新打开');renderTasks()}); + }); box.querySelectorAll('[data-local-index]').forEach(card=>{const x=localTasks[Number(card.dataset.localIndex)]; card.querySelector('.local-resume')?.addEventListener('click',async()=>{await j('/api/ocean/batch/'+encodeURIComponent(x.job_id)+'/resume',{method:'POST'});renderTasks()}); card.querySelector('.local-retry')?.addEventListener('click',async()=>{await j('/api/ocean/batch/'+encodeURIComponent(x.job_id)+'/retry',{method:'POST'});renderTasks()}); diff --git a/tests/test_api_entry_preflight_v456.py b/tests/test_api_entry_preflight_v456.py index 09c6da7d554bcd3f5c70254737166bd540d4b52b..60b7155fb5635c083fdf3d2ad0b7ca8404211537 100644 --- a/tests/test_api_entry_preflight_v456.py +++ b/tests/test_api_entry_preflight_v456.py @@ -2,7 +2,7 @@ from pathlib import Path ROOT=Path(__file__).resolve().parents[1] def test_version(): - assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.8" + assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.9" def test_guard_is_before_auth_and_dispatch(): chat=(ROOT/"routes"/"chat.py").read_text(encoding="utf-8") diff --git a/tests/test_chat_lifecycle_progress_v393.py b/tests/test_chat_lifecycle_progress_v393.py index 6646edf033f557b4ed384745627f06bc7dcb8b30..549020d9c5a7a4fedd174dfa79d1be888e41b027 100644 --- a/tests/test_chat_lifecycle_progress_v393.py +++ b/tests/test_chat_lifecycle_progress_v393.py @@ -3,7 +3,7 @@ import pathlib ROOT=pathlib.Path(__file__).resolve().parents[1] def test_version_393(): - assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.8" + assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.9" def test_frontend_lifecycle_progress(): js=(ROOT/"static"/"js"/"app.js").read_text(encoding="utf-8") diff --git a/tests/test_clarification_ui_v454.py b/tests/test_clarification_ui_v454.py index ae8fef5207001313e8fd071faaab4808afdbe2d4..f3ea456d4976e68fb8eda9f14afb070bbacce626 100644 --- a/tests/test_clarification_ui_v454.py +++ b/tests/test_clarification_ui_v454.py @@ -2,7 +2,7 @@ from pathlib import Path ROOT=Path(__file__).resolve().parents[1] def test_version(): - assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.8" + assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.9" def test_missing_date_reuses_canonical_region_bbox(): rt=(ROOT/"services"/"chat_runtime.py").read_text(encoding="utf-8") diff --git a/tests/test_classification_cleanup_v466.py b/tests/test_classification_cleanup_v466.py index 6009a49a25b18a669e240a67776f49994840e975..efaa8015f1a520ebaaa6e6d7bd50ec816450d86d 100644 --- a/tests/test_classification_cleanup_v466.py +++ b/tests/test_classification_cleanup_v466.py @@ -2,7 +2,7 @@ from pathlib import Path ROOT=Path(__file__).resolve().parents[1] def test_version(): - assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.8" + assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.9" def test_seaaroundus_alias(): py=(ROOT/"sidebar_catalog.py").read_text(encoding="utf-8") diff --git a/tests/test_context_and_multitask_finalize_v451.py b/tests/test_context_and_multitask_finalize_v451.py index f970affc7fefe5155f0c0907473845dfdcd550bf..581212191a8d869ca077868510d652c654cb2994 100644 --- a/tests/test_context_and_multitask_finalize_v451.py +++ b/tests/test_context_and_multitask_finalize_v451.py @@ -2,7 +2,7 @@ from pathlib import Path ROOT=Path(__file__).resolve().parents[1] def test_version(): - assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.8" + assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.9" def test_new_query_slot_reset(): rt=(ROOT/"services"/"chat_runtime.py").read_text(encoding="utf-8") diff --git a/tests/test_download_host_authorization_v460.py b/tests/test_download_host_authorization_v460.py index 6c0f94637003899f8608e94e9898bc9f1ba29110..7ce45f21e7dc37819c754df5a27cd7db78e03565 100644 --- a/tests/test_download_host_authorization_v460.py +++ b/tests/test_download_host_authorization_v460.py @@ -2,7 +2,7 @@ from pathlib import Path ROOT=Path(__file__).resolve().parents[1] def test_version(): - assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.8" + assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.9" def test_batch_url_authorization_helper_exists(): py=(ROOT/"routes"/"downloads.py").read_text(encoding="utf-8") diff --git a/tests/test_download_opfs_fallback_v459.py b/tests/test_download_opfs_fallback_v459.py index df0300b156df700ed1c0896f936b64e5a7837c32..3655c6d397a76affeda7619acc16029a37e20ab9 100644 --- a/tests/test_download_opfs_fallback_v459.py +++ b/tests/test_download_opfs_fallback_v459.py @@ -2,7 +2,7 @@ from pathlib import Path ROOT=Path(__file__).resolve().parents[1] def test_version(): - assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.8" + assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.9" def test_opfs_is_probed_not_assumed(): js=(ROOT/"static"/"js"/"app.js").read_text(encoding="utf-8") diff --git a/tests/test_download_progress_v391.py b/tests/test_download_progress_v391.py index 6fed6da4dd958105605a1f737fa2027bc3b099a1..8576c853b18647ec5ea4cc83875ec73437605c68 100644 --- a/tests/test_download_progress_v391.py +++ b/tests/test_download_progress_v391.py @@ -3,7 +3,7 @@ import pathlib ROOT=pathlib.Path(__file__).resolve().parents[1] def test_version_391(): - assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.8" + assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.9" def test_p1_label_removed(): js=(ROOT/"static"/"js"/"app.js").read_text(encoding="utf-8") diff --git a/tests/test_download_resilience_v422.py b/tests/test_download_resilience_v422.py index 11a4d8df2f3c838dd32e5e378659fcf07af27b3d..90c9f9e2e286fcfee1c2ceb1bc3fa5c72117c6d5 100644 --- a/tests/test_download_resilience_v422.py +++ b/tests/test_download_resilience_v422.py @@ -3,7 +3,7 @@ import pathlib ROOT=pathlib.Path(__file__).resolve().parents[1] def test_version(): - assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.8" + assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.9" def test_refresh_route_registered(): t=(ROOT/"routes"/"ocean_batch.py").read_text(encoding="utf-8") diff --git a/tests/test_frontend_preflight_v457.py b/tests/test_frontend_preflight_v457.py index e2f0a8b613e758257ea0836828a03ef9f40e0a34..e99527a5d3858b3e7e442a76019e0e563209859c 100644 --- a/tests/test_frontend_preflight_v457.py +++ b/tests/test_frontend_preflight_v457.py @@ -2,7 +2,7 @@ from pathlib import Path ROOT=Path(__file__).resolve().parents[1] def test_version(): - assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.8" + assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.9" def test_frontend_preflight_exists_before_progress(): js=(ROOT/"static"/"js"/"app.js").read_text(encoding="utf-8") diff --git a/tests/test_history_sidebar_v373.py b/tests/test_history_sidebar_v373.py index 056b02b4fec4412651c86fe3abacc9ed45a2a87f..cf4fd0a56f05ae5482ef6b16ad1a7cc061efce19 100644 --- a/tests/test_history_sidebar_v373.py +++ b/tests/test_history_sidebar_v373.py @@ -3,7 +3,7 @@ import pathlib ROOT=pathlib.Path(__file__).resolve().parents[1] def test_version_373(): - assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.8" + assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.9" def test_project_history_backend(): route=(ROOT/"routes"/"project_package.py").read_text(encoding="utf-8") diff --git a/tests/test_large_download_jobs_v400.py b/tests/test_large_download_jobs_v400.py index ef2db2c090edcbf96ac11859743006731be096c5..da08351f2cf8c30e2fe546bb38b33256df88aeff 100644 --- a/tests/test_large_download_jobs_v400.py +++ b/tests/test_large_download_jobs_v400.py @@ -3,7 +3,7 @@ import pathlib ROOT=pathlib.Path(__file__).resolve().parents[1] def test_version_400(): - assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.8" + assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.9" def test_download_job_backend(): route=(ROOT/"routes"/"downloads.py").read_text(encoding="utf-8") diff --git a/tests/test_manual_tasks_v469.py b/tests/test_manual_tasks_v469.py new file mode 100644 index 0000000000000000000000000000000000000000..de4e579eebee9492d05ab1688d3d971aad5a3e26 --- /dev/null +++ b/tests/test_manual_tasks_v469.py @@ -0,0 +1,30 @@ +from pathlib import Path +ROOT=Path(__file__).resolve().parents[1] + +def test_version(): + assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.9" + +def test_manual_task_backend_exists(): + service=(ROOT/"services/manual_tasks.py").read_text(encoding="utf-8") + routes=(ROOT/"routes/manual_tasks.py").read_text(encoding="utf-8") + assert "class ManualTaskStore" in service + assert "VALID_STATUSES" in service + assert "/api/sidebar/tasks/manual" in routes + assert 'methods=["POST"]' in routes + assert 'methods=["PATCH"]' in routes + assert 'methods=["DELETE"]' in routes + +def test_ui_server_lists_manual_tasks(): + py=(ROOT/"ui_server.py").read_text(encoding="utf-8") + assert "MANUAL_TASK_STORE.list_user" in py + assert '"kind":"manual"' in py + assert "register_manual_task_routes" in py + +def test_frontend_new_task_controls(): + js=(ROOT/"static/js/app.js").read_text(encoding="utf-8") + assert "+ 新建任务" in js + assert "openManualTaskEditor" in js + assert "标记完成" in js + assert "重新打开" in js + assert "manual-delete" in js + assert "账号服务器持久化(可跨设备)" in js diff --git a/tests/test_merged_health_v433.py b/tests/test_merged_health_v433.py index 62dd02449f46e5a0d61a7b9e3dc62172cc4b3e8c..e4d6ddb41af86dc2ff864f12e82bef125fbd065c 100644 --- a/tests/test_merged_health_v433.py +++ b/tests/test_merged_health_v433.py @@ -3,7 +3,7 @@ from services.ocean_batch import parse_ocean_batch_request ROOT=Path(__file__).resolve().parents[1] def test_merged_version(): - assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.8" + assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.9" def test_ocean_month_batch_survives_merge(): p=parse_ocean_batch_request("查询 2002 年 8 月西北太平洋的 SST 数据,并导出为 CSV 文件供我下载") diff --git a/tests/test_multitask_observability_v442.py b/tests/test_multitask_observability_v442.py index eb46a3f8b4c82fd8f00641d1d676b2acb2b3cff8..76a4eff5a4e6448f41f1e4217ea65d8091aafa17 100644 --- a/tests/test_multitask_observability_v442.py +++ b/tests/test_multitask_observability_v442.py @@ -2,7 +2,7 @@ from pathlib import Path ROOT=Path(__file__).resolve().parents[1] def test_version(): - assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.8" + assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.9" def test_frontend_sends_multitask_trace_headers(): js=(ROOT/"static"/"js"/"app.js").read_text(encoding="utf-8") diff --git a/tests/test_multitask_sse_finalize_v452.py b/tests/test_multitask_sse_finalize_v452.py index e03cdf68af1f149ab78bb1aaad1ac89d582ed2e8..1043aeb7223514639aa75984fe6de1f309106291 100644 --- a/tests/test_multitask_sse_finalize_v452.py +++ b/tests/test_multitask_sse_finalize_v452.py @@ -2,7 +2,7 @@ from pathlib import Path ROOT=Path(__file__).resolve().parents[1] def test_version(): - assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.8" + assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.9" def test_parallel_sse_uses_real_blank_line_separator(): js=(ROOT/"static"/"js"/"app.js").read_text(encoding="utf-8") diff --git a/tests/test_noaa_ramldb_roles_v465.py b/tests/test_noaa_ramldb_roles_v465.py index 25aa32da76e885dca7c2f7c2b563cc37d014b8be..d22f0fd513771f54b7cdfd95729fd2d581790017 100644 --- a/tests/test_noaa_ramldb_roles_v465.py +++ b/tests/test_noaa_ramldb_roles_v465.py @@ -2,7 +2,7 @@ from pathlib import Path ROOT=Path(__file__).resolve().parents[1] def test_version(): - assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.8" + assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.9" def test_noaa_and_ramldb_aliases(): py=(ROOT/"sidebar_catalog.py").read_text(encoding="utf-8") diff --git a/tests/test_ocean_batch_downloads_v420.py b/tests/test_ocean_batch_downloads_v420.py index 954b092ef2cd838350b8ba15d8df8861226fb00b..9d4ab34b2b185451b761d3d5e5189bc01f922052 100644 --- a/tests/test_ocean_batch_downloads_v420.py +++ b/tests/test_ocean_batch_downloads_v420.py @@ -3,7 +3,7 @@ import pathlib ROOT=pathlib.Path(__file__).resolve().parents[1] def test_version_420(): - assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.8" + assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.9" def test_manifest_has_filename_and_sizes(): svc=(ROOT/"services"/"ocean_batch.py").read_text(encoding="utf-8") diff --git a/tests/test_ocean_batch_routing_v421.py b/tests/test_ocean_batch_routing_v421.py index a55bdb613f3a6a56616796fb60262802cdd3ccd1..4873885ed9e5bf8024d0f1de1bd2b0df03947ec6 100644 --- a/tests/test_ocean_batch_routing_v421.py +++ b/tests/test_ocean_batch_routing_v421.py @@ -4,7 +4,7 @@ from services.ocean_batch import parse_ocean_batch_request ROOT=pathlib.Path(__file__).resolve().parents[1] def test_version_421(): - assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.8" + assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.9" def test_exact_user_month_query_routes_to_batch(): p=parse_ocean_batch_request("查询 2002 年 8 月西北太平洋的 SST 数据,并导出为 CSV 文件供我下载") diff --git a/tests/test_ocean_batch_v410.py b/tests/test_ocean_batch_v410.py index c84ed81da26c06c53192387b90ce7f921e42243d..c02fc48239bf3ded76b67d7e8dfdb56473bda55e 100644 --- a/tests/test_ocean_batch_v410.py +++ b/tests/test_ocean_batch_v410.py @@ -4,7 +4,7 @@ from services.ocean_batch import parse_ocean_batch_request ROOT=pathlib.Path(__file__).resolve().parents[1] def test_version_410(): - assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.8" + assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.9" def test_month_three_variable_batch_plan(): p=parse_ocean_batch_request("查询并导出 2002 年 8 月西北太平洋 120°E–140°E、20°N–40°N 的 SST、SSH、CHL 数据,并打包下载") diff --git a/tests/test_ocean_date_default_v453.py b/tests/test_ocean_date_default_v453.py index 7b03c2e738616df7ab01046f7bb124857b236f67..392452cca01b87f0bb1779f4c49176486a89d2cc 100644 --- a/tests/test_ocean_date_default_v453.py +++ b/tests/test_ocean_date_default_v453.py @@ -2,7 +2,7 @@ from pathlib import Path ROOT=Path(__file__).resolve().parents[1] def test_version(): - assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.8" + assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.9" def test_missing_date_never_defaults_to_today(): rt=(ROOT/"services"/"chat_runtime.py").read_text(encoding="utf-8") diff --git a/tests/test_ocean_download_url_base_v462.py b/tests/test_ocean_download_url_base_v462.py index a7bde428fff0e65ff7ee479ed729ab034eb1fc2a..6dfdfcb674c909ca128f27e31127910a25b1590b 100644 --- a/tests/test_ocean_download_url_base_v462.py +++ b/tests/test_ocean_download_url_base_v462.py @@ -2,7 +2,7 @@ from pathlib import Path ROOT=Path(__file__).resolve().parents[1] def test_version(): - assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.8" + assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.9" def test_generic_public_base_not_used(): py=(ROOT/"services"/"ocean_batch.py").read_text(encoding="utf-8") diff --git a/tests/test_p0_finish_v380.py b/tests/test_p0_finish_v380.py index d110472c422d3f4cae6744559a48c985680c2220..ffcecf960a413ffaabec74b9525e8d434ee82afb 100644 --- a/tests/test_p0_finish_v380.py +++ b/tests/test_p0_finish_v380.py @@ -3,7 +3,7 @@ import pathlib, re, subprocess, shutil ROOT=pathlib.Path(__file__).resolve().parents[1] def test_version_380(): - assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.8" + assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.9" def test_chat_runtime_extracted(): ui=(ROOT/"ui_server.py").read_text(encoding="utf-8") diff --git a/tests/test_p0_phase2_v371.py b/tests/test_p0_phase2_v371.py index a91d61366bd8386ce6efee123df0e230386ff326..ae1fff5eb16e2ead44c3b6421827f923a676662c 100644 --- a/tests/test_p0_phase2_v371.py +++ b/tests/test_p0_phase2_v371.py @@ -3,7 +3,7 @@ import pathlib ROOT=pathlib.Path(__file__).resolve().parents[1] def test_version(): - assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.8" + assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.9" def test_frontend_feature_modules_exist(): html=(ROOT/"app.html").read_text(encoding="utf-8") diff --git a/tests/test_p0_phase3_v372.py b/tests/test_p0_phase3_v372.py index 6a9ec0d2a3785f151e1c82dc37a4abc82240572c..a519ce573a4f279619742b68c6a06c24ea599cdf 100644 --- a/tests/test_p0_phase3_v372.py +++ b/tests/test_p0_phase3_v372.py @@ -3,7 +3,7 @@ import pathlib ROOT=pathlib.Path(__file__).resolve().parents[1] def test_version_372(): - assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.8" + assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.9" def test_route_modules_exist(): for name in ("datasets.py","user_state.py","project_package.py","data_health.py"): diff --git a/tests/test_platform_reliability_v450.py b/tests/test_platform_reliability_v450.py index 59053505d56936092e82a911a6d1ce0274911412..35220394099d968e1283935b5126142fa8626a67 100644 --- a/tests/test_platform_reliability_v450.py +++ b/tests/test_platform_reliability_v450.py @@ -6,7 +6,7 @@ from services.ocean_batch import parse_ocean_batch_request ROOT=Path(__file__).resolve().parents[1] def test_version(): - assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.8" + assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.9" def test_squid_center_uses_repository_membership(): js=(ROOT/"static"/"js"/"app.js").read_text(encoding="utf-8") diff --git a/tests/test_quality_center_v362.py b/tests/test_quality_center_v362.py index 650f803ac1cf58972446128fda5765c0ffef5055..33d2f886072e0797deb1220922cf0d2c7fa9dce3 100644 --- a/tests/test_quality_center_v362.py +++ b/tests/test_quality_center_v362.py @@ -3,7 +3,7 @@ import pathlib ROOT = pathlib.Path(__file__).resolve().parents[1] def test_version_371(): - assert (ROOT / "VERSION").read_text(encoding="utf-8").strip() == "4.6.8" + assert (ROOT / "VERSION").read_text(encoding="utf-8").strip() == "4.6.9" def test_native_health_backend(): text = (ROOT / "routes" / "data_health.py").read_text(encoding="utf-8") diff --git a/tests/test_quality_trust_v361.py b/tests/test_quality_trust_v361.py index 65db4d25c59b952460ca88b923b65c3590135c0b..bfe8c4244871359c5d1ae9d07fe4fb433c5a4772 100644 --- a/tests/test_quality_trust_v361.py +++ b/tests/test_quality_trust_v361.py @@ -2,7 +2,7 @@ from pathlib import Path ROOT=Path(__file__).resolve().parents[1] def test_version_current(): - assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.8" + assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.9" def test_quality_confidence_is_now_native_api_metric(): text=(ROOT/"routes"/"data_health.py").read_text(encoding="utf-8") diff --git a/tests/test_reference_cleanup_v468.py b/tests/test_reference_cleanup_v468.py index a6fd1a71d67ee7045718df035b84246c22b0b7f2..1b2bc47b21aaca8beb7f863468c341e5adabe89b 100644 --- a/tests/test_reference_cleanup_v468.py +++ b/tests/test_reference_cleanup_v468.py @@ -2,7 +2,7 @@ from pathlib import Path ROOT=Path(__file__).resolve().parents[1] def test_version(): - assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.8" + assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.9" def test_reference_and_reproducibility_are_auxiliary(): py=(ROOT/"routes"/"datasets.py").read_text(encoding="utf-8") diff --git a/tests/test_release_stability.py b/tests/test_release_stability.py index 6f4ee2abc8f70c08cc75cf95c24ef28dfcdbc9be..589e15730922662ad02b0557721aaaf540dec5f5 100644 --- a/tests/test_release_stability.py +++ b/tests/test_release_stability.py @@ -7,7 +7,7 @@ def test_version_is_single_source(): version = (ROOT / "VERSION").read_text(encoding="utf-8").strip() ui = (ROOT / "ui_server.py").read_text(encoding="utf-8") html = (ROOT / "app.html").read_text(encoding="utf-8") - assert version == "4.6.8" + assert version == "4.6.9" assert 'with_name("VERSION")' in ui assert "__APP_VERSION__" in html diff --git a/tests/test_resumable_batch_download_v458.py b/tests/test_resumable_batch_download_v458.py index bb0e881f446a7f5dd8c9dc7e74d08f7c28ce9d5b..7945cc6a202cf4dbfd218ff911c689e3e440c9a9 100644 --- a/tests/test_resumable_batch_download_v458.py +++ b/tests/test_resumable_batch_download_v458.py @@ -2,7 +2,7 @@ from pathlib import Path ROOT=Path(__file__).resolve().parents[1] def test_version(): - assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.8" + assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.9" def test_network_loss_pauses_not_fails_all(): js=(ROOT/"static"/"js"/"app.js").read_text(encoding="utf-8") diff --git a/tests/test_route_region_clarification_v455.py b/tests/test_route_region_clarification_v455.py index 2703906941caa71752988fa38405e6ea9bf8edb0..de7abfb307a177b6633ed9914ae760e1972aeb2a 100644 --- a/tests/test_route_region_clarification_v455.py +++ b/tests/test_route_region_clarification_v455.py @@ -2,7 +2,7 @@ from pathlib import Path ROOT=Path(__file__).resolve().parents[1] def test_version(): - assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.8" + assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.9" def test_route_guard_exists_before_agent_dispatch(): chat=(ROOT/"routes"/"chat.py").read_text(encoding="utf-8") diff --git a/tests/test_runtime_hotfix_v376.py b/tests/test_runtime_hotfix_v376.py index 3fc61f04a4bea17547f9183eb169d55945d55d04..be0fe9ae8ece79c5c4af1257da8d35c2bd9680cd 100644 --- a/tests/test_runtime_hotfix_v376.py +++ b/tests/test_runtime_hotfix_v376.py @@ -3,7 +3,7 @@ import pathlib ROOT=pathlib.Path(__file__).resolve().parents[1] def test_version_376(): - assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.8" + assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.9" def test_no_stray_async_before_sidebar_toggle(): js=(ROOT/"static"/"js"/"app.js").read_text(encoding="utf-8") diff --git a/tests/test_sidebar_collapse_v374.py b/tests/test_sidebar_collapse_v374.py index 759419d20193244eb3bc5547c8c50124fc5b0bf5..de895138b5e15a6fa1235c8498eaf89d01b6b286 100644 --- a/tests/test_sidebar_collapse_v374.py +++ b/tests/test_sidebar_collapse_v374.py @@ -3,7 +3,7 @@ import pathlib ROOT=pathlib.Path(__file__).resolve().parents[1] def test_version_374(): - assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.8" + assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.9" def test_sidebar_toggle_markup(): html=(ROOT/"app.html").read_text(encoding="utf-8") diff --git a/tests/test_sidebar_partial_collapse_v375.py b/tests/test_sidebar_partial_collapse_v375.py index 4f658d634ef9fabe1e766e748b7658d5353dabf8..8181b1d9d64129f1d2bac35305c5eb404250db1e 100644 --- a/tests/test_sidebar_partial_collapse_v375.py +++ b/tests/test_sidebar_partial_collapse_v375.py @@ -3,7 +3,7 @@ import pathlib ROOT=pathlib.Path(__file__).resolve().parents[1] def test_version_375(): - assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.8" + assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.9" def test_sidebar_order_and_groups(): html=(ROOT/"app.html").read_text(encoding="utf-8") diff --git a/tests/test_smart_progress_avatar_sync_v441.py b/tests/test_smart_progress_avatar_sync_v441.py index de69455aa628dab51dd14d244a0190511a9cac04..8a435862aad9e83bc0c08be12519d65a02ba02c2 100644 --- a/tests/test_smart_progress_avatar_sync_v441.py +++ b/tests/test_smart_progress_avatar_sync_v441.py @@ -2,7 +2,7 @@ from pathlib import Path ROOT=Path(__file__).resolve().parents[1] def test_version(): - assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.8" + assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.9" def test_simple_chat_hides_progress_until_needed(): js=(ROOT/"static"/"js"/"app.js").read_text(encoding="utf-8") diff --git a/tests/test_source_mapping_roles_v464.py b/tests/test_source_mapping_roles_v464.py index cf3af14bbca1d8e0191f2dc9abeff7466056a79d..805985d109e286244a118e6d960dd462c04632e3 100644 --- a/tests/test_source_mapping_roles_v464.py +++ b/tests/test_source_mapping_roles_v464.py @@ -2,7 +2,7 @@ from pathlib import Path ROOT=Path(__file__).resolve().parents[1] def test_version(): - assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.8" + assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.9" def test_new_source_mappings(): py=(ROOT/"sidebar_catalog.py").read_text(encoding="utf-8") diff --git a/tests/test_squid_finalize_v467.py b/tests/test_squid_finalize_v467.py index 48ccecff1c747b5b4dfd1e5ed4b0f21bbe6c3c7b..c949c3970b33da0c0824d16b5f742f5740982d79 100644 --- a/tests/test_squid_finalize_v467.py +++ b/tests/test_squid_finalize_v467.py @@ -2,7 +2,7 @@ from pathlib import Path ROOT=Path(__file__).resolve().parents[1] def test_version(): - assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.8" + assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.9" def test_ccamlr_and_ommastrephidae_mappings(): py=(ROOT/"sidebar_catalog.py").read_text(encoding="utf-8") diff --git a/tests/test_sse_heartbeat_v401.py b/tests/test_sse_heartbeat_v401.py index 99011d057905a29ce5b621c7c645cb7afd3e820f..6c03b4960896857f80dd9bce5253f3df9caa2e97 100644 --- a/tests/test_sse_heartbeat_v401.py +++ b/tests/test_sse_heartbeat_v401.py @@ -3,7 +3,7 @@ import pathlib ROOT=pathlib.Path(__file__).resolve().parents[1] def test_version_401(): - assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.8" + assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.9" def test_chat_has_independent_heartbeat_wrapper(): route=(ROOT/"routes"/"chat.py").read_text(encoding="utf-8") diff --git a/tests/test_ui_avatar_multitask_v440.py b/tests/test_ui_avatar_multitask_v440.py index 3e69d3245fc504f810a01f60357150d438a30180..8d24c93ed0485bbc41a18f83a883ed43826fd1f8 100644 --- a/tests/test_ui_avatar_multitask_v440.py +++ b/tests/test_ui_avatar_multitask_v440.py @@ -2,7 +2,7 @@ from pathlib import Path ROOT=Path(__file__).resolve().parents[1] def test_version(): - assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.8" + assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.9" def test_assistant_avatar_removed_user_avatar_customizable(): js=(ROOT/"static"/"js"/"app.js").read_text(encoding="utf-8") diff --git a/tests/test_unclassified_inventory_v463.py b/tests/test_unclassified_inventory_v463.py index 537856831a84b75f60a4f8f53404c380a886100b..59321b09b58a3b027db518cd9c1659ba77afa781 100644 --- a/tests/test_unclassified_inventory_v463.py +++ b/tests/test_unclassified_inventory_v463.py @@ -2,7 +2,7 @@ from pathlib import Path ROOT=Path(__file__).resolve().parents[1] def test_version(): - assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.8" + assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.9" def test_backend_unique_classification_and_route(): py=(ROOT/"routes"/"datasets.py").read_text(encoding="utf-8") diff --git a/tests/test_unified_download_manager_v392.py b/tests/test_unified_download_manager_v392.py index 26ff77bfbb546c73a6a32369383105dac1f4d044..4857ff81fcb52cb3f67cd7b4fa15bb2aefe452ef 100644 --- a/tests/test_unified_download_manager_v392.py +++ b/tests/test_unified_download_manager_v392.py @@ -3,7 +3,7 @@ import pathlib ROOT=pathlib.Path(__file__).resolve().parents[1] def test_version_392(): - assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.8" + assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.9" def test_download_proxy_route_registered(): route=(ROOT/"routes"/"downloads.py").read_text(encoding="utf-8") diff --git a/tests/test_unified_query_v390.py b/tests/test_unified_query_v390.py index bbbd35a1c8700bcb90010339c35aac07e6ad69f1..88c2b32a1e81abb8463980e302628fcdeb279e1d 100644 --- a/tests/test_unified_query_v390.py +++ b/tests/test_unified_query_v390.py @@ -4,7 +4,7 @@ from services.unified_query import plan_unified_query ROOT=pathlib.Path(__file__).resolve().parents[1] def test_version_390(): - assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.8" + assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.9" def test_joint_squid_environment_plan(): p=plan_unified_query("查询 2002 年 8 月西北太平洋柔鱼记录,并匹配 SST、SSH、CHL") diff --git a/tests/test_upstream_refresh_retry_v461.py b/tests/test_upstream_refresh_retry_v461.py index bdac62b6ecf10a921bd55da21822c8fa3e21762f..284476720ec1d43f7629681b374cf5b55ab3ec03 100644 --- a/tests/test_upstream_refresh_retry_v461.py +++ b/tests/test_upstream_refresh_retry_v461.py @@ -2,7 +2,7 @@ from pathlib import Path ROOT=Path(__file__).resolve().parents[1] def test_version(): - assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.8" + assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.9" def test_probe_returns_status_and_rejects_dead_url(): py=(ROOT/"routes"/"downloads.py").read_text(encoding="utf-8") diff --git a/tests/test_v381_navigation_ocean_intent.py b/tests/test_v381_navigation_ocean_intent.py index c2069132ba0c4a484f27c08a098f67bd46c3da63..44a31d4d802f6a9355059aece62040deebafef06 100644 --- a/tests/test_v381_navigation_ocean_intent.py +++ b/tests/test_v381_navigation_ocean_intent.py @@ -3,7 +3,7 @@ import pathlib, ast ROOT=pathlib.Path(__file__).resolve().parents[1] def test_version_381(): - assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.8" + assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.9" def test_recent_conversation_switches_to_chat(): js=(ROOT/"static"/"js"/"app.js").read_text(encoding="utf-8") diff --git a/ui_server.py b/ui_server.py index 9d9a4171f6be02c1fe080cb3f39e9be5035da240..4776add2770c13b70d1f8f17e4e45e45fc148239 100644 --- a/ui_server.py +++ b/ui_server.py @@ -36,6 +36,8 @@ from routes.chat import register_chat_routes from routes.unified_query import register_unified_query_routes from routes.downloads import register_download_routes from routes.ocean_batch import register_ocean_batch_routes +from routes.manual_tasks import register_manual_task_routes +from services.manual_tasks import ManualTaskStore from services.ocean_batch import OceanBatchManager, parse_ocean_batch_request from services.chat_runtime import init_chat_runtime from services.context_compressor import ( @@ -470,6 +472,12 @@ _read_server_favorites = _USER_STATE.read_favorites _write_server_favorites = _USER_STATE.write_favorites _favorites_storage_mode = _USER_STATE.storage_mode +MANUAL_TASK_STORE = ManualTaskStore( + USER_STATE_ROOT, + valid_user_id, + max_tasks=int(os.environ.get("USER_STATE_MAX_MANUAL_TASKS","300")), +) + def _request_bearer_token(request:Request): value=str(request.headers.get("Authorization") or "").strip() @@ -3461,6 +3469,11 @@ async def sidebar_tasks(request: Request): # optional Memory API is not configured. This makes My Tasks a recovery # center for Ocean batch jobs and resumable downloads after refresh/restart. local_tasks=[] + try: + for x in MANUAL_TASK_STORE.list_user(uid,200): + local_tasks.append({"kind":"manual",**x}) + except Exception: + pass try: for x in OCEAN_BATCH_MANAGER.list_user(uid,100): local_tasks.append({"kind":"ocean_batch",**x}) @@ -3477,6 +3490,7 @@ async def sidebar_tasks(request: Request): return { "enabled": True, "memory_enabled": False, "tasks": [], "assets": [], "local_tasks": local_tasks, "error": "", + "manual_task_storage": MANUAL_TASK_STORE.storage_mode, } try: @@ -3488,9 +3502,9 @@ async def sidebar_tasks(request: Request): all_tasks = task_data.get("tasks") or [] user_tasks = [_public_task(task) for task in all_tasks if str(task.get("user_id") or "") == str(uid)][:50] assets = [_public_asset(asset) for asset in (context.get("assets") or [])[:50]] - return {"enabled":True,"memory_enabled":True,"tasks":user_tasks,"assets":assets,"local_tasks":local_tasks,"error":""} + return {"enabled":True,"memory_enabled":True,"tasks":user_tasks,"assets":assets,"local_tasks":local_tasks,"error":"","manual_task_storage":MANUAL_TASK_STORE.storage_mode} except Exception as exc: - return {"enabled":True,"memory_enabled":True,"tasks":[],"assets":[],"local_tasks":local_tasks,"error":"远程任务记录暂不可用,本地持久化任务仍可恢复:"+str(exc)[:300]} + return {"enabled":True,"memory_enabled":True,"tasks":[],"assets":[],"local_tasks":local_tasks,"error":"远程任务记录暂不可用,本地持久化任务仍可恢复:"+str(exc)[:300],"manual_task_storage":MANUAL_TASK_STORE.storage_mode} @@ -3501,6 +3515,7 @@ OCEAN_BATCH_MANAGER = OceanBatchManager( concurrency=int(os.environ.get("OCEAN_BATCH_CONCURRENCY","6")), ) register_ocean_batch_routes(app, globals()) +register_manual_task_routes(app, globals()) stream_chat, harness_stream_chat, dispatch_chat_stream = init_chat_runtime(globals()) register_chat_routes(app, globals())