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+='';
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())