myagent-ai 1.23.80 → 1.23.81

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/groups/manager.py CHANGED
@@ -258,6 +258,14 @@ class GroupManager:
258
258
  db_path = self._data_dir / "groups" / "messages.db"
259
259
  self._db_conn = sqlite3.connect(str(db_path), check_same_thread=False)
260
260
  self._db_conn.row_factory = sqlite3.Row
261
+ # [v1.23.81] 群聊 session 映射表(每个群一个纯数字 session ID)
262
+ self._db_conn.execute("""
263
+ CREATE TABLE IF NOT EXISTS group_sessions (
264
+ group_id TEXT PRIMARY KEY,
265
+ session_id TEXT NOT NULL UNIQUE,
266
+ created_at REAL NOT NULL
267
+ )
268
+ """)
261
269
  self._db_conn.execute("""
262
270
  CREATE TABLE IF NOT EXISTS group_messages (
263
271
  id TEXT PRIMARY KEY,
@@ -638,6 +646,64 @@ class GroupManager:
638
646
 
639
647
  return group
640
648
 
649
+ # ==================================================================
650
+ # [v1.23.81] 群聊 Session 管理(纯数字 ID,嵌入 URL 用于导航恢复)
651
+ # ==================================================================
652
+
653
+ def get_or_create_group_session(self, group_id: str) -> str:
654
+ """
655
+ 获取或创建群聊的 session ID。
656
+ 每个群有且仅有一个 session ID(纯数字,以 9 开头,递增)。
657
+ 此 session ID 用于前端 URL 参数,确保从后台管理返回时能正确恢复群聊视图。
658
+ """
659
+ if not self._db_conn:
660
+ return ""
661
+ # 查找已有 session
662
+ row = self._db_conn.execute(
663
+ "SELECT session_id FROM group_sessions WHERE group_id = ?", (group_id,)
664
+ ).fetchone()
665
+ if row:
666
+ return row["session_id"]
667
+ # 生成新的纯数字 session ID(9 开头,13 位数字,时间戳后 8 位 + 随机 4 位)
668
+ import random
669
+ ts_part = str(int(time.time()))[-8:]
670
+ rand_part = str(random.randint(1000, 9999))
671
+ new_sid = "9" + ts_part + rand_part # 共 13 位纯数字
672
+ # 确保唯一性
673
+ for _ in range(10):
674
+ existing = self._db_conn.execute(
675
+ "SELECT 1 FROM group_sessions WHERE session_id = ?", (new_sid,)
676
+ ).fetchone()
677
+ if not existing:
678
+ break
679
+ rand_part = str(random.randint(1000, 9999))
680
+ new_sid = "9" + ts_part + rand_part
681
+ self._db_conn.execute(
682
+ "INSERT INTO group_sessions (group_id, session_id, created_at) VALUES (?, ?, ?)",
683
+ (group_id, new_sid, time.time()),
684
+ )
685
+ self._db_conn.commit()
686
+ logger.info(f"群聊 session 创建: group={group_id}, session={new_sid}")
687
+ return new_sid
688
+
689
+ def get_group_id_by_session(self, session_id: str) -> Optional[str]:
690
+ """通过 session ID 查找 group_id(用于前端 URL 恢复)"""
691
+ if not self._db_conn:
692
+ return None
693
+ row = self._db_conn.execute(
694
+ "SELECT group_id FROM group_sessions WHERE session_id = ?", (session_id,)
695
+ ).fetchone()
696
+ return row["group_id"] if row else None
697
+
698
+ def get_session_by_group(self, group_id: str) -> Optional[str]:
699
+ """通过 group_id 查找 session_id"""
700
+ if not self._db_conn:
701
+ return None
702
+ row = self._db_conn.execute(
703
+ "SELECT session_id FROM group_sessions WHERE group_id = ?", (group_id,)
704
+ ).fetchone()
705
+ return row["session_id"] if row else None
706
+
641
707
  # ==================================================================
642
708
  # 消息管理
643
709
  # ==================================================================
@@ -258,6 +258,14 @@ class GroupManager:
258
258
  db_path = self._data_dir / "groups" / "messages.db"
259
259
  self._db_conn = sqlite3.connect(str(db_path), check_same_thread=False)
260
260
  self._db_conn.row_factory = sqlite3.Row
261
+ # [v1.23.81] 群聊 session 映射表(每个群一个纯数字 session ID)
262
+ self._db_conn.execute("""
263
+ CREATE TABLE IF NOT EXISTS group_sessions (
264
+ group_id TEXT PRIMARY KEY,
265
+ session_id TEXT NOT NULL UNIQUE,
266
+ created_at REAL NOT NULL
267
+ )
268
+ """)
261
269
  self._db_conn.execute("""
262
270
  CREATE TABLE IF NOT EXISTS group_messages (
263
271
  id TEXT PRIMARY KEY,
@@ -638,6 +646,64 @@ class GroupManager:
638
646
 
639
647
  return group
640
648
 
649
+ # ==================================================================
650
+ # [v1.23.81] 群聊 Session 管理(纯数字 ID,嵌入 URL 用于导航恢复)
651
+ # ==================================================================
652
+
653
+ def get_or_create_group_session(self, group_id: str) -> str:
654
+ """
655
+ 获取或创建群聊的 session ID。
656
+ 每个群有且仅有一个 session ID(纯数字,以 9 开头,递增)。
657
+ 此 session ID 用于前端 URL 参数,确保从后台管理返回时能正确恢复群聊视图。
658
+ """
659
+ if not self._db_conn:
660
+ return ""
661
+ # 查找已有 session
662
+ row = self._db_conn.execute(
663
+ "SELECT session_id FROM group_sessions WHERE group_id = ?", (group_id,)
664
+ ).fetchone()
665
+ if row:
666
+ return row["session_id"]
667
+ # 生成新的纯数字 session ID(9 开头,13 位数字,时间戳后 8 位 + 随机 4 位)
668
+ import random
669
+ ts_part = str(int(time.time()))[-8:]
670
+ rand_part = str(random.randint(1000, 9999))
671
+ new_sid = "9" + ts_part + rand_part # 共 13 位纯数字
672
+ # 确保唯一性
673
+ for _ in range(10):
674
+ existing = self._db_conn.execute(
675
+ "SELECT 1 FROM group_sessions WHERE session_id = ?", (new_sid,)
676
+ ).fetchone()
677
+ if not existing:
678
+ break
679
+ rand_part = str(random.randint(1000, 9999))
680
+ new_sid = "9" + ts_part + rand_part
681
+ self._db_conn.execute(
682
+ "INSERT INTO group_sessions (group_id, session_id, created_at) VALUES (?, ?, ?)",
683
+ (group_id, new_sid, time.time()),
684
+ )
685
+ self._db_conn.commit()
686
+ logger.info(f"群聊 session 创建: group={group_id}, session={new_sid}")
687
+ return new_sid
688
+
689
+ def get_group_id_by_session(self, session_id: str) -> Optional[str]:
690
+ """通过 session ID 查找 group_id(用于前端 URL 恢复)"""
691
+ if not self._db_conn:
692
+ return None
693
+ row = self._db_conn.execute(
694
+ "SELECT group_id FROM group_sessions WHERE session_id = ?", (session_id,)
695
+ ).fetchone()
696
+ return row["group_id"] if row else None
697
+
698
+ def get_session_by_group(self, group_id: str) -> Optional[str]:
699
+ """通过 group_id 查找 session_id"""
700
+ if not self._db_conn:
701
+ return None
702
+ row = self._db_conn.execute(
703
+ "SELECT session_id FROM group_sessions WHERE group_id = ?", (group_id,)
704
+ ).fetchone()
705
+ return row["session_id"] if row else None
706
+
641
707
  # ==================================================================
642
708
  # 消息管理
643
709
  # ==================================================================
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myagent-ai",
3
- "version": "1.23.80",
3
+ "version": "1.23.81",
4
4
  "description": "本地桌面端执行型AI助手 - Open Interpreter 风格 | Local Desktop Execution-Oriented AI Assistant",
5
5
  "main": "main.py",
6
6
  "bin": {
@@ -122,7 +122,7 @@ document.getElementById('adminSidebar')?.addEventListener('click',function(e){
122
122
  loadVersion();
123
123
  setTimeout(()=>checkUpdate(false),30000);
124
124
 
125
- // [v1.23.57] 返回聊天:支持群聊上下文恢复
125
+ // [v1.23.81] 返回聊天:通过纯数字 session ID 恢复群聊(不再传递 group_id)
126
126
  (function(){
127
127
  function _buildReturnUrl(){
128
128
  var params=new URLSearchParams(window.location.search);
@@ -130,13 +130,21 @@ setTimeout(()=>checkUpdate(false),30000);
130
130
  // 从 URL 参数中恢复聊天状态
131
131
  var agent=params.get('from_agent');
132
132
  var session=params.get('from_session');
133
- var group=params.get('from_group');
133
+ // [v1.23.81] 群聊纯数字 session ID(优先级最高,覆盖 ?s= 个人会话参数)
134
+ var groupSession=params.get('from_group_session');
134
135
  var mode=params.get('from_mode');
135
136
  var parts=[];
136
- if(agent)parts.push('a='+encodeURIComponent(agent));
137
- if(mode)parts.push('mode='+encodeURIComponent(mode));
138
- if(session)parts.push('s='+encodeURIComponent(session));
139
- if(group)parts.push('group='+encodeURIComponent(group));
137
+ // 如果有群聊 session,用 ?g= 参数(纯数字 ID)
138
+ if(groupSession && /^[0-9]+$/.test(groupSession)){
139
+ parts.push('g='+groupSession);
140
+ if(agent)parts.push('a='+encodeURIComponent(agent));
141
+ if(mode)parts.push('mode='+encodeURIComponent(mode));
142
+ } else {
143
+ // 个人聊天模式
144
+ if(agent)parts.push('a='+encodeURIComponent(agent));
145
+ if(mode)parts.push('mode='+encodeURIComponent(mode));
146
+ if(session)parts.push('s='+encodeURIComponent(session));
147
+ }
140
148
  if(parts.length>0)base+='?'+parts.join('&');
141
149
  return base;
142
150
  }
@@ -3,7 +3,7 @@
3
3
 
4
4
  (function() {
5
5
  var base = new URL('.', window.location.href).href;
6
- var v = '?v=12';
6
+ var v = '?v=14';
7
7
  var scripts = [
8
8
  'groupchat.js', // 群聊模块(状态 + API + 渲染 + 发送)
9
9
  'flow_engine.js', // 文本处理流引擎(SSE 流式 + 大文本 + 执行事件)
@@ -86,16 +86,19 @@ function closeMobileAgentPanel() {
86
86
  if (overlay) overlay.classList.remove('active');
87
87
  }
88
88
 
89
- // [v1.23.57] 跳转后台管理:携带当前聊天上下文(agent、session、群聊),以便"返回聊天"正确恢复
89
+ // [v1.23.81] 跳转后台管理:携带当前聊天上下文(agent、session、群聊session ID),以便"返回聊天"正确恢复
90
90
  function goToAdmin() {
91
91
  var params = new URLSearchParams();
92
92
  var page = 'dashboard';
93
93
  // 传递当前聊天状态,供"返回聊天"恢复
94
94
  if (state.activeAgent) params.set('from_agent', state.activeAgent);
95
95
  if (state.chatMode) params.set('from_mode', state.chatMode);
96
- if (state.activeSessionId) params.set('from_session', state.activeSessionId);
97
- if (typeof currentView !== 'undefined' && currentView === 'group' && typeof currentGroupId !== 'undefined' && currentGroupId) {
98
- params.set('from_group', currentGroupId);
96
+ if (state.activeSessionId && currentView !== 'group') params.set('from_session', state.activeSessionId);
97
+ // [v1.23.81] 传递群聊纯数字 session ID(从 URL 读取),而非 JS 变量
98
+ if (typeof currentView !== 'undefined' && currentView === 'group') {
99
+ var urlP = new URLSearchParams(window.location.search);
100
+ var gSession = urlP.get('g');
101
+ if (gSession) params.set('from_group_session', gSession);
99
102
  }
100
103
  // 保留原始 ?page= 参数
101
104
  var origParams = new URLSearchParams(window.location.search);
@@ -346,14 +349,10 @@ const StatePersistence = {
346
349
  if (savedSessionId) {
347
350
  state._pendingSessionRestore = savedSessionId;
348
351
  }
349
- // [v1.23.34] 恢复群聊状态
350
- var savedView = StatePersistence.load('currentView', 'chat');
351
- var savedGroupId = StatePersistence.load('currentGroupId', null);
352
- if (savedView === 'group' && savedGroupId) {
353
- currentView = 'group';
354
- currentGroupId = savedGroupId;
355
- state._pendingGroupRestore = savedGroupId;
356
- }
352
+ // [v1.23.81] 不再从 localStorage 恢复群聊状态(统一由 URL ?g= 参数恢复)
353
+ // 仅保留兼容处理:如果旧版遗留了 localStorage 群聊状态,清除它
354
+ StatePersistence.remove('currentView');
355
+ StatePersistence.remove('currentGroupId');
357
356
  },
358
357
  /** 标记 setup 已完成(避免每次刷新弹出向导) */
359
358
  markSetupDone() { StatePersistence.save('setupDone', true); },
@@ -369,14 +368,17 @@ async function initChat() {
369
368
  // [v1.16.12] 初始化附件上传 UI
370
369
  initAttachmentUI();
371
370
 
372
- // URL 参数处理: ?a=xxx&mode=exec&s=xxx&popout=1(agent/session 使用 UrlCodec 编码)
371
+ // URL 参数处理: ?a=xxx&mode=exec&s=xxx&g=xxx&popout=1(agent/session 使用 UrlCodec 编码)
373
372
  const urlParams = new URLSearchParams(window.location.search);
374
373
  const urlAgent = UrlCodec.decode(urlParams.get('a') || '') || UrlCodec.decode(urlParams.get('agent') || '');
375
374
  const urlMode = urlParams.get('mode');
376
375
  const urlSession = UrlCodec.decode(urlParams.get('s') || '') || UrlCodec.decode(urlParams.get('session') || '');
377
376
  const isPopout = urlParams.get('popout') === '1';
378
- // [v1.23.57] 支持从后台管理"返回聊天"时恢复群聊状态
379
- const urlGroup = urlParams.get('group');
377
+ // [v1.23.81] 群聊纯数字 session ID(?g= 参数,优先级最高)
378
+ const urlGroupSession = urlParams.get('g') || '';
379
+ // 兼容旧版 ?group= 参数(从后台管理返回时的旧格式)
380
+ const urlGroupSessionLegacy = urlParams.get('from_group_session') || '';
381
+ const _effectiveGroupSession = urlGroupSession || urlGroupSessionLegacy;
380
382
  if (urlMode === 'chat' || urlMode === 'exec') {
381
383
  state.chatMode = urlMode;
382
384
  }
@@ -462,27 +464,37 @@ async function initChat() {
462
464
  document.title = (agentObj ? agentObj.name : urlAgent || 'MyAgent') + ' - MyAgent';
463
465
  }
464
466
 
465
- // [v1.23.80] 群聊恢复逻辑(URL 参数优先于 localStorage)
466
- var _pendingGroupId = urlGroup || state._pendingGroupRestore || null;
467
- state._pendingGroupRestore = null;
467
+ // [v1.23.81] 群聊恢复逻辑:从 URL ?g= 纯数字 session ID 恢复(优先级最高)
468
+ // 不再依赖 JS 状态传递(from_group),完全通过 URL session ID 恢复
469
+ state._pendingGroupRestore = null; // 不再使用 localStorage 中的群聊状态
468
470
 
469
- function _restoreGroupChat(gid) {
470
- if (typeof groups !== 'undefined' && groups.length > 0) {
471
- selectGroup(gid);
472
- } else {
473
- setTimeout(function() { _restoreGroupChat(gid); }, 200);
471
+ async function _restoreGroupBySession(sessionId) {
472
+ // 通过 API 将纯数字 session ID 解析为 group_id
473
+ try {
474
+ var resolveData = await api('/api/group-session/' + encodeURIComponent(sessionId));
475
+ if (resolveData && resolveData.group_id) {
476
+ if (typeof groups !== 'undefined' && groups.length > 0) {
477
+ selectGroup(resolveData.group_id);
478
+ } else {
479
+ // groups 尚未加载,等待后重试
480
+ setTimeout(function() { _restoreGroupBySession(sessionId); }, 200);
481
+ }
482
+ } else {
483
+ console.warn('群聊 session 解析失败:', sessionId);
484
+ }
485
+ } catch (e) {
486
+ console.warn('解析群聊 session 失败:', e);
474
487
  }
475
488
  }
476
489
 
477
- // [v1.23.80] 如果即将恢复群聊,跳过 selectAgent 避免竞态(selectAgent 会清除 currentView=group 状态)
478
- if (_pendingGroupId) {
479
- // 群聊模式:不需要 selectAgent,直接恢复群聊视图
480
- // 但如果有 from_agent 参数,更新 state.activeAgent 以便后续群聊私聊时使用
490
+ // 如果 URL 中有 ?g= 纯数字 session ID,优先恢复群聊
491
+ if (_effectiveGroupSession && /^[0-9]+$/.test(_effectiveGroupSession)) {
492
+ // 群聊模式:如果有 from_agent 参数,更新 state.activeAgent 以便后续群聊私聊时使用
481
493
  if (urlAgent && urlAgent !== state.activeAgent) {
482
494
  state.activeAgent = urlAgent;
483
495
  StatePersistence.save('activeAgent', urlAgent);
484
496
  }
485
- _restoreGroupChat(_pendingGroupId);
497
+ _restoreGroupBySession(_effectiveGroupSession);
486
498
  } else if (urlAgent) {
487
499
  // URL 指定了 agent,校验后切换
488
500
  var resolved = urlAgent;
@@ -167,10 +167,21 @@ async function selectGroup(gid) {
167
167
  }
168
168
  currentView = 'group';
169
169
  currentGroupId = gid;
170
- // [v1.23.34] 持久化群聊状态,刷新页面后恢复
171
- if (typeof StatePersistence !== 'undefined') {
172
- StatePersistence.save('currentView', 'group');
173
- StatePersistence.save('currentGroupId', gid);
170
+
171
+ // [v1.23.81] 获取群聊 session ID 并写入 URL(纯数字,用于导航恢复)
172
+ try {
173
+ var sessionData = await api('/api/groups/' + encodeURIComponent(gid) + '/session');
174
+ if (sessionData && sessionData.session_id) {
175
+ var sessionId = sessionData.session_id;
176
+ // 用群聊专用 ?g= 参数更新 URL(不刷新页面),纯数字 session ID
177
+ var newUrl = '/ui/chat/chat_container.html?g=' + sessionId;
178
+ if (typeof state !== 'undefined' && state.activeAgent) {
179
+ newUrl += '&a=' + encodeURIComponent(state.activeAgent);
180
+ }
181
+ window.history.replaceState({groupSession: sessionId, groupId: gid}, '', newUrl);
182
+ }
183
+ } catch (e) {
184
+ console.warn('获取群聊 session 失败:', e);
174
185
  }
175
186
 
176
187
  // [v1.23.51] 群聊模式:隐藏"新对话"按钮,搜索框改为搜索群聊
@@ -238,11 +249,19 @@ function exitGroupChat() {
238
249
  currentView = 'chat';
239
250
  currentGroupId = null;
240
251
  groupMessages = [];
241
- // [v1.23.34] 清除持久化的群聊状态
242
- if (typeof StatePersistence !== 'undefined') {
243
- StatePersistence.save('currentView', 'chat');
244
- StatePersistence.remove('currentGroupId');
252
+ // [v1.23.81] 清除群聊 ?g= session URL,恢复普通聊天 URL
253
+ var cleanUrl = '/ui/chat/chat_container.html';
254
+ if (typeof state !== 'undefined' && state.activeAgent && state.activeAgent !== 'default') {
255
+ cleanUrl += '?a=' + encodeURIComponent(state.activeAgent);
256
+ }
257
+ if (typeof state !== 'undefined' && state.activeSessionId && state.activeSessionId !== '__new__') {
258
+ cleanUrl += (cleanUrl.indexOf('?') >= 0 ? '&' : '?') + 's=' + encodeURIComponent(state.activeSessionId);
245
259
  }
260
+ window.history.replaceState({}, '', cleanUrl);
261
+ // 清除群聊 localStorage 持久化状态
262
+ StatePersistence.remove('currentView');
263
+ StatePersistence.remove('currentGroupId');
264
+
246
265
  // 清空残留的聊天状态,避免切回个人聊天时显示群聊消息
247
266
  state.messages = [];
248
267
  state.activeSessionId = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myagent-ai",
3
- "version": "1.23.80",
3
+ "version": "1.23.81",
4
4
  "description": "本地桌面端执行型AI助手 - Open Interpreter 风格 | Local Desktop Execution-Oriented AI Assistant",
5
5
  "main": "main.py",
6
6
  "bin": {
package/web/api_server.py CHANGED
@@ -474,6 +474,9 @@ class ApiServer:
474
474
  r.add_get("/api/groups/{gid}/messages", self.handle_get_group_messages)
475
475
  r.add_post("/api/groups/{gid}/messages", self.handle_send_group_message)
476
476
  r.add_delete("/api/groups/{gid}/messages", self.handle_clear_group_messages)
477
+ # [v1.23.81] 群聊 session 管理(纯数字 ID,用于 URL 导航恢复)
478
+ r.add_get("/api/groups/{gid}/session", self.handle_get_group_session)
479
+ r.add_get("/api/group-session/{sid}", self.handle_resolve_group_session)
477
480
  # [v1.23.37] Agent间私聊记录查询
478
481
  r.add_get("/api/agent-chat/pairs", self.handle_get_agent_chat_pairs)
479
482
  r.add_get("/api/agent-chat/messages", self.handle_get_agent_chat_messages)
@@ -7979,6 +7982,30 @@ window.addEventListener('beforeunload', function() {{
7979
7982
  ok = mgr.clear_messages(gid)
7980
7983
  return web.json_response({"ok": ok})
7981
7984
 
7985
+ # ── [v1.23.81] 群聊 Session API ──
7986
+
7987
+ async def handle_get_group_session(self, request):
7988
+ """GET /api/groups/{gid}/session - 获取或创建群聊的 session ID(纯数字)"""
7989
+ gid = request.match_info["gid"]
7990
+ mgr = self._get_group_manager()
7991
+ group = mgr.get_group(gid)
7992
+ if not group:
7993
+ return web.json_response({"error": "群不存在"}, status=404)
7994
+ session_id = mgr.get_or_create_group_session(gid)
7995
+ return web.json_response({"session_id": session_id, "group_id": gid})
7996
+
7997
+ async def handle_resolve_group_session(self, request):
7998
+ """GET /api/group-session/{sid} - 通过 session ID 查找 group_id(用于 URL 恢复)"""
7999
+ sid = request.match_info["sid"]
8000
+ mgr = self._get_group_manager()
8001
+ group_id = mgr.get_group_id_by_session(sid)
8002
+ if not group_id:
8003
+ return web.json_response({"error": "session 不存在"}, status=404)
8004
+ group = mgr.get_group(group_id)
8005
+ if not group:
8006
+ return web.json_response({"error": "群已不存在"}, status=404)
8007
+ return web.json_response({"session_id": sid, "group_id": group_id, "group": self._enrich_group_dict(group.to_dict())})
8008
+
7982
8009
  # ── [v1.23.37] Agent间私聊记录 API ──
7983
8010
 
7984
8011
  async def handle_get_agent_chat_pairs(self, request):
@@ -122,7 +122,7 @@ document.getElementById('adminSidebar')?.addEventListener('click',function(e){
122
122
  loadVersion();
123
123
  setTimeout(()=>checkUpdate(false),30000);
124
124
 
125
- // [v1.23.57] 返回聊天:支持群聊上下文恢复
125
+ // [v1.23.81] 返回聊天:通过纯数字 session ID 恢复群聊(不再传递 group_id)
126
126
  (function(){
127
127
  function _buildReturnUrl(){
128
128
  var params=new URLSearchParams(window.location.search);
@@ -130,13 +130,21 @@ setTimeout(()=>checkUpdate(false),30000);
130
130
  // 从 URL 参数中恢复聊天状态
131
131
  var agent=params.get('from_agent');
132
132
  var session=params.get('from_session');
133
- var group=params.get('from_group');
133
+ // [v1.23.81] 群聊纯数字 session ID(优先级最高,覆盖 ?s= 个人会话参数)
134
+ var groupSession=params.get('from_group_session');
134
135
  var mode=params.get('from_mode');
135
136
  var parts=[];
136
- if(agent)parts.push('a='+encodeURIComponent(agent));
137
- if(mode)parts.push('mode='+encodeURIComponent(mode));
138
- if(session)parts.push('s='+encodeURIComponent(session));
139
- if(group)parts.push('group='+encodeURIComponent(group));
137
+ // 如果有群聊 session,用 ?g= 参数(纯数字 ID)
138
+ if(groupSession && /^[0-9]+$/.test(groupSession)){
139
+ parts.push('g='+groupSession);
140
+ if(agent)parts.push('a='+encodeURIComponent(agent));
141
+ if(mode)parts.push('mode='+encodeURIComponent(mode));
142
+ } else {
143
+ // 个人聊天模式
144
+ if(agent)parts.push('a='+encodeURIComponent(agent));
145
+ if(mode)parts.push('mode='+encodeURIComponent(mode));
146
+ if(session)parts.push('s='+encodeURIComponent(session));
147
+ }
140
148
  if(parts.length>0)base+='?'+parts.join('&');
141
149
  return base;
142
150
  }
@@ -3,7 +3,7 @@
3
3
 
4
4
  (function() {
5
5
  var base = new URL('.', window.location.href).href;
6
- var v = '?v=12';
6
+ var v = '?v=14';
7
7
  var scripts = [
8
8
  'groupchat.js', // 群聊模块(状态 + API + 渲染 + 发送)
9
9
  'flow_engine.js', // 文本处理流引擎(SSE 流式 + 大文本 + 执行事件)
@@ -86,16 +86,19 @@ function closeMobileAgentPanel() {
86
86
  if (overlay) overlay.classList.remove('active');
87
87
  }
88
88
 
89
- // [v1.23.57] 跳转后台管理:携带当前聊天上下文(agent、session、群聊),以便"返回聊天"正确恢复
89
+ // [v1.23.81] 跳转后台管理:携带当前聊天上下文(agent、session、群聊session ID),以便"返回聊天"正确恢复
90
90
  function goToAdmin() {
91
91
  var params = new URLSearchParams();
92
92
  var page = 'dashboard';
93
93
  // 传递当前聊天状态,供"返回聊天"恢复
94
94
  if (state.activeAgent) params.set('from_agent', state.activeAgent);
95
95
  if (state.chatMode) params.set('from_mode', state.chatMode);
96
- if (state.activeSessionId) params.set('from_session', state.activeSessionId);
97
- if (typeof currentView !== 'undefined' && currentView === 'group' && typeof currentGroupId !== 'undefined' && currentGroupId) {
98
- params.set('from_group', currentGroupId);
96
+ if (state.activeSessionId && currentView !== 'group') params.set('from_session', state.activeSessionId);
97
+ // [v1.23.81] 传递群聊纯数字 session ID(从 URL 读取),而非 JS 变量
98
+ if (typeof currentView !== 'undefined' && currentView === 'group') {
99
+ var urlP = new URLSearchParams(window.location.search);
100
+ var gSession = urlP.get('g');
101
+ if (gSession) params.set('from_group_session', gSession);
99
102
  }
100
103
  // 保留原始 ?page= 参数
101
104
  var origParams = new URLSearchParams(window.location.search);
@@ -346,14 +349,10 @@ const StatePersistence = {
346
349
  if (savedSessionId) {
347
350
  state._pendingSessionRestore = savedSessionId;
348
351
  }
349
- // [v1.23.34] 恢复群聊状态
350
- var savedView = StatePersistence.load('currentView', 'chat');
351
- var savedGroupId = StatePersistence.load('currentGroupId', null);
352
- if (savedView === 'group' && savedGroupId) {
353
- currentView = 'group';
354
- currentGroupId = savedGroupId;
355
- state._pendingGroupRestore = savedGroupId;
356
- }
352
+ // [v1.23.81] 不再从 localStorage 恢复群聊状态(统一由 URL ?g= 参数恢复)
353
+ // 仅保留兼容处理:如果旧版遗留了 localStorage 群聊状态,清除它
354
+ StatePersistence.remove('currentView');
355
+ StatePersistence.remove('currentGroupId');
357
356
  },
358
357
  /** 标记 setup 已完成(避免每次刷新弹出向导) */
359
358
  markSetupDone() { StatePersistence.save('setupDone', true); },
@@ -369,14 +368,17 @@ async function initChat() {
369
368
  // [v1.16.12] 初始化附件上传 UI
370
369
  initAttachmentUI();
371
370
 
372
- // URL 参数处理: ?a=xxx&mode=exec&s=xxx&popout=1(agent/session 使用 UrlCodec 编码)
371
+ // URL 参数处理: ?a=xxx&mode=exec&s=xxx&g=xxx&popout=1(agent/session 使用 UrlCodec 编码)
373
372
  const urlParams = new URLSearchParams(window.location.search);
374
373
  const urlAgent = UrlCodec.decode(urlParams.get('a') || '') || UrlCodec.decode(urlParams.get('agent') || '');
375
374
  const urlMode = urlParams.get('mode');
376
375
  const urlSession = UrlCodec.decode(urlParams.get('s') || '') || UrlCodec.decode(urlParams.get('session') || '');
377
376
  const isPopout = urlParams.get('popout') === '1';
378
- // [v1.23.57] 支持从后台管理"返回聊天"时恢复群聊状态
379
- const urlGroup = urlParams.get('group');
377
+ // [v1.23.81] 群聊纯数字 session ID(?g= 参数,优先级最高)
378
+ const urlGroupSession = urlParams.get('g') || '';
379
+ // 兼容旧版 ?group= 参数(从后台管理返回时的旧格式)
380
+ const urlGroupSessionLegacy = urlParams.get('from_group_session') || '';
381
+ const _effectiveGroupSession = urlGroupSession || urlGroupSessionLegacy;
380
382
  if (urlMode === 'chat' || urlMode === 'exec') {
381
383
  state.chatMode = urlMode;
382
384
  }
@@ -462,27 +464,37 @@ async function initChat() {
462
464
  document.title = (agentObj ? agentObj.name : urlAgent || 'MyAgent') + ' - MyAgent';
463
465
  }
464
466
 
465
- // [v1.23.80] 群聊恢复逻辑(URL 参数优先于 localStorage)
466
- var _pendingGroupId = urlGroup || state._pendingGroupRestore || null;
467
- state._pendingGroupRestore = null;
467
+ // [v1.23.81] 群聊恢复逻辑:从 URL ?g= 纯数字 session ID 恢复(优先级最高)
468
+ // 不再依赖 JS 状态传递(from_group),完全通过 URL session ID 恢复
469
+ state._pendingGroupRestore = null; // 不再使用 localStorage 中的群聊状态
468
470
 
469
- function _restoreGroupChat(gid) {
470
- if (typeof groups !== 'undefined' && groups.length > 0) {
471
- selectGroup(gid);
472
- } else {
473
- setTimeout(function() { _restoreGroupChat(gid); }, 200);
471
+ async function _restoreGroupBySession(sessionId) {
472
+ // 通过 API 将纯数字 session ID 解析为 group_id
473
+ try {
474
+ var resolveData = await api('/api/group-session/' + encodeURIComponent(sessionId));
475
+ if (resolveData && resolveData.group_id) {
476
+ if (typeof groups !== 'undefined' && groups.length > 0) {
477
+ selectGroup(resolveData.group_id);
478
+ } else {
479
+ // groups 尚未加载,等待后重试
480
+ setTimeout(function() { _restoreGroupBySession(sessionId); }, 200);
481
+ }
482
+ } else {
483
+ console.warn('群聊 session 解析失败:', sessionId);
484
+ }
485
+ } catch (e) {
486
+ console.warn('解析群聊 session 失败:', e);
474
487
  }
475
488
  }
476
489
 
477
- // [v1.23.80] 如果即将恢复群聊,跳过 selectAgent 避免竞态(selectAgent 会清除 currentView=group 状态)
478
- if (_pendingGroupId) {
479
- // 群聊模式:不需要 selectAgent,直接恢复群聊视图
480
- // 但如果有 from_agent 参数,更新 state.activeAgent 以便后续群聊私聊时使用
490
+ // 如果 URL 中有 ?g= 纯数字 session ID,优先恢复群聊
491
+ if (_effectiveGroupSession && /^[0-9]+$/.test(_effectiveGroupSession)) {
492
+ // 群聊模式:如果有 from_agent 参数,更新 state.activeAgent 以便后续群聊私聊时使用
481
493
  if (urlAgent && urlAgent !== state.activeAgent) {
482
494
  state.activeAgent = urlAgent;
483
495
  StatePersistence.save('activeAgent', urlAgent);
484
496
  }
485
- _restoreGroupChat(_pendingGroupId);
497
+ _restoreGroupBySession(_effectiveGroupSession);
486
498
  } else if (urlAgent) {
487
499
  // URL 指定了 agent,校验后切换
488
500
  var resolved = urlAgent;
@@ -167,10 +167,21 @@ async function selectGroup(gid) {
167
167
  }
168
168
  currentView = 'group';
169
169
  currentGroupId = gid;
170
- // [v1.23.34] 持久化群聊状态,刷新页面后恢复
171
- if (typeof StatePersistence !== 'undefined') {
172
- StatePersistence.save('currentView', 'group');
173
- StatePersistence.save('currentGroupId', gid);
170
+
171
+ // [v1.23.81] 获取群聊 session ID 并写入 URL(纯数字,用于导航恢复)
172
+ try {
173
+ var sessionData = await api('/api/groups/' + encodeURIComponent(gid) + '/session');
174
+ if (sessionData && sessionData.session_id) {
175
+ var sessionId = sessionData.session_id;
176
+ // 用群聊专用 ?g= 参数更新 URL(不刷新页面),纯数字 session ID
177
+ var newUrl = '/ui/chat/chat_container.html?g=' + sessionId;
178
+ if (typeof state !== 'undefined' && state.activeAgent) {
179
+ newUrl += '&a=' + encodeURIComponent(state.activeAgent);
180
+ }
181
+ window.history.replaceState({groupSession: sessionId, groupId: gid}, '', newUrl);
182
+ }
183
+ } catch (e) {
184
+ console.warn('获取群聊 session 失败:', e);
174
185
  }
175
186
 
176
187
  // [v1.23.51] 群聊模式:隐藏"新对话"按钮,搜索框改为搜索群聊
@@ -238,11 +249,19 @@ function exitGroupChat() {
238
249
  currentView = 'chat';
239
250
  currentGroupId = null;
240
251
  groupMessages = [];
241
- // [v1.23.34] 清除持久化的群聊状态
242
- if (typeof StatePersistence !== 'undefined') {
243
- StatePersistence.save('currentView', 'chat');
244
- StatePersistence.remove('currentGroupId');
252
+ // [v1.23.81] 清除群聊 ?g= session URL,恢复普通聊天 URL
253
+ var cleanUrl = '/ui/chat/chat_container.html';
254
+ if (typeof state !== 'undefined' && state.activeAgent && state.activeAgent !== 'default') {
255
+ cleanUrl += '?a=' + encodeURIComponent(state.activeAgent);
256
+ }
257
+ if (typeof state !== 'undefined' && state.activeSessionId && state.activeSessionId !== '__new__') {
258
+ cleanUrl += (cleanUrl.indexOf('?') >= 0 ? '&' : '?') + 's=' + encodeURIComponent(state.activeSessionId);
245
259
  }
260
+ window.history.replaceState({}, '', cleanUrl);
261
+ // 清除群聊 localStorage 持久化状态
262
+ StatePersistence.remove('currentView');
263
+ StatePersistence.remove('currentGroupId');
264
+
246
265
  // 清空残留的聊天状态,避免切回个人聊天时显示群聊消息
247
266
  state.messages = [];
248
267
  state.activeSessionId = null;
package/worklog.md CHANGED
@@ -19,3 +19,21 @@ Work Log:
19
19
  Stage Summary:
20
20
  - 修复文件:admin-platforms.js, admin-core.js, index.html, chat_main.js, chat_container.html, api_server.py
21
21
  - 版本:v1.23.57 已发布到 npm
22
+ ---
23
+ Task ID: 1
24
+ Agent: main
25
+ Task: 修复从后台管理返回群聊时导航状态错误 + 群聊气泡宽度不一致
26
+
27
+ Work Log:
28
+ - 分析了从后台管理返回群聊时导航错误的根因:initChat()中selectAgent()未被await,与selectGroup()产生竞态,导致群聊UI被覆盖
29
+ - 发现goToAdmin()中使用了state.currentAgent(不存在),应该是state.activeAgent
30
+ - 重构了initChat()的agent/session/group恢复逻辑:当有pendingGroupRestore或urlGroup时,跳过selectAgent()避免竞态
31
+ - 修复了群聊气泡宽度不一致问题:为.message-row和.group-msg-row添加align-items:flex-start和flex:1规则
32
+ - 更新CSS版本缓存chat.css?v=11,JS版本?v=12
33
+ - 版本号升级到1.23.80
34
+ - 推送到GitHub,发布npm
35
+
36
+ Stage Summary:
37
+ - 修复了3个bug:goToAdmin()的state.currentAgent错误、selectAgent/selectGroup竞态条件、气泡宽度不一致
38
+ - 已推送GitHub (commit 379098e)
39
+ - 已发布npm myagent-ai@1.23.80