Zin299 commited on
Commit
8222cc3
·
1 Parent(s): 95a8a23

Deploy v3.4.0 persistence update

Browse files
Files changed (6) hide show
  1. DEPLOY.md +1 -1
  2. README.md +2 -2
  3. VERSION +1 -1
  4. app.html +10 -3
  5. tests/test_release_stability.py +1 -1
  6. ui_server.py +66 -2
DEPLOY.md CHANGED
@@ -45,7 +45,7 @@ Next server phase:
45
  - add job/update APIs
46
  Then extend `marine_mcp.py` with corresponding tools.
47
 
48
- ## v3.3.0 deployment stability
49
 
50
  - `VERSION` is the single application version source.
51
  - Only root `app.html` is served; do not re-create `templates/app.html`.
 
45
  - add job/update APIs
46
  Then extend `marine_mcp.py` with corresponding tools.
47
 
48
+ ## v3.4.0 deployment stability
49
 
50
  - `VERSION` is the single application version source.
51
  - Only root `app.html` is served; do not re-create `templates/app.html`.
README.md CHANGED
@@ -9,10 +9,10 @@ app_port: 7860
9
 
10
  # Global Marine Foundation Data Agent
11
 
12
- Current UI release: **v3.3.0**.
13
 
14
 
15
- ## v3.3.0 稳定性重构(第一阶段)
16
 
17
  - 新增根目录 `VERSION`,后端与前端统一从同一版本源渲染,避免版本号漂移。
18
  - 运行时仅服务根目录 `app.html`;发布包不再携带第二份 `templates/app.html`。
 
9
 
10
  # Global Marine Foundation Data Agent
11
 
12
+ Current UI release: **v3.4.0**.
13
 
14
 
15
+ ## v3.4.0 稳定性重构(第一阶段)
16
 
17
  - 新增根目录 `VERSION`,后端与前端统一从同一版本源渲染,避免版本号漂移。
18
  - 运行时仅服务根目录 `app.html`;发布包不再携带第二份 `templates/app.html`。
VERSION CHANGED
@@ -1 +1 @@
1
- 3.3.0
 
1
+ 3.4.0
app.html CHANGED
@@ -697,6 +697,8 @@ let CURRENT_VIEW="chat";
697
  let FAVORITES=[];
698
  let FAVORITES_SYNC={enabled:false,storage_mode:"local",status:"local"};
699
  let FAVORITES_SYNC_TIMER=null;
 
 
700
  let DATASET_COMPARE=[];
701
  let SETTINGS={fontSize:"normal",enterToSend:true,refreshSeconds:15,accent:"blue",defaultView:"chat",compact:false,reduceMotion:false,longitudeRange:"-180_180",timeDisplay:"local",exportFormat:"auto",answerDetail:"standard",historyLimit:30,confirmDownload:true,messageActionTrigger:"left"};
702
  let STATUS_TIMER=null;
@@ -736,7 +738,7 @@ function loadSessions(){
736
  }
737
  let S=[];
738
 
739
- function save(){const limit=[10,30,50].includes(Number(SETTINGS.historyLimit))?Number(SETTINGS.historyLimit):30;localStorage.setItem(K,JSON.stringify(S.slice(0,limit)))}
740
  function scopedKey(base){return base+":"+USER_ID}
741
  function loadFavorites(){
742
  try{return JSON.parse(localStorage.getItem(scopedKey(FAV_BASE_K))||"[]")}
@@ -796,7 +798,11 @@ function loadSettings(){
796
  }
797
  catch{return {...SETTINGS}}
798
  }
799
- function saveSettings(){localStorage.setItem(scopedKey(SETTINGS_BASE_K),JSON.stringify(SETTINGS))}
 
 
 
 
800
  function exportWorkspaceBackup(){
801
  const payload={schema:"squid-workspace-backup",schema_version:1,app_version:"__APP_VERSION__",exported_at:new Date().toISOString(),sessions:S.slice(0,50),favorites:FAVORITES.slice(0,100),settings:SETTINGS};
802
  const blob=new Blob([JSON.stringify(payload,null,2)],{type:"application/json;charset=utf-8"}),url=URL.createObjectURL(blob),a=document.createElement("a");
@@ -1975,9 +1981,10 @@ async function boot(){
1975
  }
1976
 
1977
  S=loadSessions();
 
1978
  FAVORITES=loadFavorites();
1979
  await syncFavoritesFromServer();
1980
- SETTINGS=loadSettings();
1981
  document.body.classList.remove("auth-pending");
1982
  hist();
1983
  status();
 
697
  let FAVORITES=[];
698
  let FAVORITES_SYNC={enabled:false,storage_mode:"local",status:"local"};
699
  let FAVORITES_SYNC_TIMER=null;
700
+ let WORKSPACE_SYNC_TIMER=null;
701
+ let WORKSPACE_SYNC={enabled:false,storage_mode:"local",status:"local"};
702
  let DATASET_COMPARE=[];
703
  let SETTINGS={fontSize:"normal",enterToSend:true,refreshSeconds:15,accent:"blue",defaultView:"chat",compact:false,reduceMotion:false,longitudeRange:"-180_180",timeDisplay:"local",exportFormat:"auto",answerDetail:"standard",historyLimit:30,confirmDownload:true,messageActionTrigger:"left"};
704
  let STATUS_TIMER=null;
 
738
  }
739
  let S=[];
740
 
741
+ function save(){const limit=[10,30,50].includes(Number(SETTINGS.historyLimit))?Number(SETTINGS.historyLimit):30;localStorage.setItem(K,JSON.stringify(S.slice(0,limit)));scheduleWorkspaceSync()}
742
  function scopedKey(base){return base+":"+USER_ID}
743
  function loadFavorites(){
744
  try{return JSON.parse(localStorage.getItem(scopedKey(FAV_BASE_K))||"[]")}
 
798
  }
799
  catch{return {...SETTINGS}}
800
  }
801
+ function saveSettings(){localStorage.setItem(scopedKey(SETTINGS_BASE_K),JSON.stringify(SETTINGS));scheduleWorkspaceSync()}
802
+ function mergeSessions(remote,local){const map=new Map();[...(remote||[]),...(local||[])].forEach(x=>{if(!x||!x.thread_id)return;const prev=map.get(x.thread_id);if(!prev||Number(x.updated||0)>=Number(prev.updated||0))map.set(x.thread_id,x)});return [...map.values()].sort((a,b)=>Number(b.updated||0)-Number(a.updated||0)).slice(0,50)}
803
+ function scheduleWorkspaceSync(){if(!AUTH_ENABLED||!AUTH_TOKEN)return;clearTimeout(WORKSPACE_SYNC_TIMER);WORKSPACE_SYNC_TIMER=setTimeout(()=>pushWorkspaceToServer().catch(()=>{}),500)}
804
+ async function pushWorkspaceToServer(){if(!AUTH_ENABLED||!AUTH_TOKEN)return;WORKSPACE_SYNC.status="syncing";const data=await j("/api/sidebar/workspace",{method:"PUT",body:JSON.stringify({user_id:USER_ID,sessions:S.slice(0,50),settings:SETTINGS})});WORKSPACE_SYNC={enabled:!!data.sync_enabled,storage_mode:data.storage_mode||"server_session",status:"synced"}}
805
+ async function syncWorkspaceFromServer(){if(!AUTH_ENABLED||!AUTH_TOKEN){WORKSPACE_SYNC={enabled:false,storage_mode:"local",status:"local"};return}try{const localSessions=loadSessions(),localSettings=loadSettings();const data=await j("/api/sidebar/workspace?user_id="+encodeURIComponent(USER_ID));S=mergeSessions(data.sessions||[],localSessions);SETTINGS={...SETTINGS,...localSettings,...(data.settings||{})};localStorage.setItem(K,JSON.stringify(S.slice(0,50)));localStorage.setItem(scopedKey(SETTINGS_BASE_K),JSON.stringify(SETTINGS));WORKSPACE_SYNC={enabled:!!data.sync_enabled,storage_mode:data.storage_mode||"server_session",status:"synced"};if(localSessions.length||Object.keys(localSettings||{}).length)await pushWorkspaceToServer()}catch(err){WORKSPACE_SYNC={enabled:false,storage_mode:"local",status:"error"};console.warn("workspace sync failed",err)}}
806
  function exportWorkspaceBackup(){
807
  const payload={schema:"squid-workspace-backup",schema_version:1,app_version:"__APP_VERSION__",exported_at:new Date().toISOString(),sessions:S.slice(0,50),favorites:FAVORITES.slice(0,100),settings:SETTINGS};
808
  const blob=new Blob([JSON.stringify(payload,null,2)],{type:"application/json;charset=utf-8"}),url=URL.createObjectURL(blob),a=document.createElement("a");
 
1981
  }
1982
 
1983
  S=loadSessions();
1984
+ SETTINGS=loadSettings();
1985
  FAVORITES=loadFavorites();
1986
  await syncFavoritesFromServer();
1987
+ await syncWorkspaceFromServer();
1988
  document.body.classList.remove("auth-pending");
1989
  hist();
1990
  status();
tests/test_release_stability.py CHANGED
@@ -7,7 +7,7 @@ def test_version_is_single_source():
7
  version = (ROOT / "VERSION").read_text(encoding="utf-8").strip()
8
  ui = (ROOT / "ui_server.py").read_text(encoding="utf-8")
9
  html = (ROOT / "app.html").read_text(encoding="utf-8")
10
- assert version == "3.3.0"
11
  assert 'with_name("VERSION")' in ui
12
  assert "__APP_VERSION__" in html
13
 
 
7
  version = (ROOT / "VERSION").read_text(encoding="utf-8").strip()
8
  ui = (ROOT / "ui_server.py").read_text(encoding="utf-8")
9
  html = (ROOT / "app.html").read_text(encoding="utf-8")
10
+ assert version == "3.4.0"
11
  assert 'with_name("VERSION")' in ui
12
  assert "__APP_VERSION__" in html
13
 
ui_server.py CHANGED
@@ -45,9 +45,9 @@ MEMORY_API_TOKEN = os.environ.get("MEMORY_API_TOKEN", "").strip()
45
  ADMIN_DASHBOARD_PASSWORD = os.environ.get("ADMIN_DASHBOARD_PASSWORD", "").strip()
46
  VERSION_FILE = Path(__file__).with_name("VERSION")
47
  try:
48
- CODE_VERSION = VERSION_FILE.read_text(encoding="utf-8").strip() or "3.3.0"
49
  except Exception:
50
- CODE_VERSION = "3.3.0"
51
  APP_VERSION = CODE_VERSION
52
  APP_REVISION = (
53
  os.environ.get("SPACE_REVISION", "").strip()
@@ -366,6 +366,55 @@ class FavoritesSyncRequest(BaseModel):
366
  user_id: str = ""
367
  favorites: list[dict[str, Any]] = Field(default_factory=list)
368
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
369
  def _favorite_state_file(user_id: str) -> Path:
370
  if not valid_user_id(user_id):
371
  raise HTTPException(400, "invalid user_id")
@@ -3994,6 +4043,21 @@ async def put_synced_favorites(body: FavoritesSyncRequest, request: Request):
3994
  "count": len(items),
3995
  }
3996
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3997
  @app.get("/api/status")
3998
  async def status(request:Request, thread_id: str | None = None):
3999
  if AUTH_ENABLED:
 
45
  ADMIN_DASHBOARD_PASSWORD = os.environ.get("ADMIN_DASHBOARD_PASSWORD", "").strip()
46
  VERSION_FILE = Path(__file__).with_name("VERSION")
47
  try:
48
+ CODE_VERSION = VERSION_FILE.read_text(encoding="utf-8").strip() or "3.4.0"
49
  except Exception:
50
+ CODE_VERSION = "3.4.0"
51
  APP_VERSION = CODE_VERSION
52
  APP_REVISION = (
53
  os.environ.get("SPACE_REVISION", "").strip()
 
366
  user_id: str = ""
367
  favorites: list[dict[str, Any]] = Field(default_factory=list)
368
 
369
+ class WorkspaceSyncRequest(BaseModel):
370
+ user_id: str = ""
371
+ sessions: list[dict[str, Any]] = Field(default_factory=list)
372
+ settings: dict[str, Any] = Field(default_factory=dict)
373
+
374
+ def _workspace_state_file(user_id: str) -> Path:
375
+ if not valid_user_id(user_id):
376
+ raise HTTPException(400, "invalid user_id")
377
+ return USER_STATE_ROOT / f"{user_id}.workspace.json"
378
+
379
+ def _sanitize_workspace_sessions(items: Any) -> list[dict[str, Any]]:
380
+ out=[]
381
+ for x in items if isinstance(items,list) else []:
382
+ if not isinstance(x,dict): continue
383
+ thread_id=str(x.get("thread_id") or "")[:200]
384
+ if not thread_id: continue
385
+ messages=[]
386
+ for m in (x.get("messages") if isinstance(x.get("messages"),list) else [])[-80:]:
387
+ if not isinstance(m,dict): continue
388
+ messages.append({
389
+ "role":"assistant" if m.get("role")=="assistant" else "user",
390
+ "text":str(m.get("text") or "")[:50000],
391
+ "meta":str(m.get("meta") or "")[:1000],
392
+ "time":int(m.get("time") or int(time.time()*1000)),
393
+ })
394
+ out.append({"thread_id":thread_id,"title":str(x.get("title") or "新对话")[:200],"updated":int(x.get("updated") or int(time.time()*1000)),"messages":messages})
395
+ if len(out)>=50: break
396
+ return out
397
+
398
+ def _sanitize_workspace_settings(value: Any) -> dict[str, Any]:
399
+ if not isinstance(value,dict): return {}
400
+ allowed={"fontSize","enterToSend","refreshSeconds","accent","defaultView","compact","reduceMotion","longitudeRange","timeDisplay","exportFormat","answerDetail","historyLimit","confirmDownload","messageActionTrigger"}
401
+ return {str(k):v for k,v in value.items() if k in allowed}
402
+
403
+ def _read_server_workspace(user_id: str) -> dict[str, Any]:
404
+ path=_workspace_state_file(user_id)
405
+ if not path.exists(): return {"sessions":[],"settings":{},"updated_at":""}
406
+ try: raw=json.loads(path.read_text(encoding="utf-8"))
407
+ except Exception: return {"sessions":[],"settings":{},"updated_at":""}
408
+ return {"sessions":_sanitize_workspace_sessions(raw.get("sessions",[])),"settings":_sanitize_workspace_settings(raw.get("settings",{})),"updated_at":str(raw.get("updated_at") or "")}
409
+
410
+ def _write_server_workspace(user_id: str, sessions: Any, settings: Any) -> dict[str, Any]:
411
+ clean_sessions=_sanitize_workspace_sessions(sessions)
412
+ clean_settings=_sanitize_workspace_settings(settings)
413
+ payload={"schema":"squid-server-workspace","schema_version":1,"updated_at":datetime.now().astimezone().isoformat(timespec="seconds"),"sessions":clean_sessions,"settings":clean_settings}
414
+ path=_workspace_state_file(user_id); tmp=path.with_suffix(path.suffix+".tmp")
415
+ tmp.write_text(json.dumps(payload,ensure_ascii=False,indent=2),encoding="utf-8"); tmp.replace(path)
416
+ return payload
417
+
418
  def _favorite_state_file(user_id: str) -> Path:
419
  if not valid_user_id(user_id):
420
  raise HTTPException(400, "invalid user_id")
 
4043
  "count": len(items),
4044
  }
4045
 
4046
+ @app.get("/api/sidebar/workspace")
4047
+ async def get_synced_workspace(request: Request):
4048
+ supplied_uid=str(request.query_params.get("user_id") or "").strip()
4049
+ uid,auth_user=await resolve_request_user(request,supplied_uid)
4050
+ if AUTH_ENABLED and not auth_user: raise HTTPException(401,"Authentication required")
4051
+ data=_read_server_workspace(uid)
4052
+ return {**data,"sync_enabled":bool(auth_user),"storage_mode":_favorites_storage_mode(),"user_id":uid}
4053
+
4054
+ @app.put("/api/sidebar/workspace")
4055
+ async def put_synced_workspace(body: WorkspaceSyncRequest, request: Request):
4056
+ uid,auth_user=await resolve_request_user(request,body.user_id)
4057
+ if AUTH_ENABLED and not auth_user: raise HTTPException(401,"Authentication required")
4058
+ data=_write_server_workspace(uid,body.sessions,body.settings)
4059
+ return {**data,"sync_enabled":bool(auth_user),"storage_mode":_favorites_storage_mode(),"user_id":uid}
4060
+
4061
  @app.get("/api/status")
4062
  async def status(request:Request, thread_id: str | None = None):
4063
  if AUTH_ENABLED: