myagent-ai 1.27.1 → 1.27.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myagent-ai",
3
- "version": "1.27.1",
3
+ "version": "1.27.2",
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
@@ -3505,6 +3505,9 @@ window.addEventListener('beforeunload', function() {{
3505
3505
  session_counts[ap] = session_counts.get(ap, 0) + r["cnt"]
3506
3506
  for a in agents_flat:
3507
3507
  a["session_count"] = session_counts.get(a["path"], 0)
3508
+ # [v1.27.2] 附加数字 agent_id 到每个 agent(供 URL 参数使用)
3509
+ for a in agents_flat:
3510
+ a["aid"] = self.core.memory.get_agent_id(a["path"])
3508
3511
  # 系统 agent 排在最前
3509
3512
  agents_flat.sort(key=lambda a: (0 if a.get("system") else 1, a.get("path", "")))
3510
3513
  tree = self._build_agent_tree(agents_flat)
@@ -4299,39 +4302,60 @@ window.addEventListener('beforeunload', function() {{
4299
4302
  async def handle_list_sessions(self, request):
4300
4303
  if not self.core.memory: return web.json_response([])
4301
4304
  agent = request.query.get("agent", "")
4305
+ conn = self.core.memory._get_conn()
4306
+
4307
+ # [v1.27.2] 构建 agent_id → agent_name 的映射(数字 ID → 路径名)
4308
+ _agent_id_map = {} # {int_id: str_name}
4309
+ try:
4310
+ for ar in conn.execute("SELECT id, name FROM agents").fetchall():
4311
+ _agent_id_map[int(ar["id"])] = ar["name"]
4312
+ except Exception:
4313
+ pass
4314
+
4315
+ _hidden = '(' + ','.join(["'llm_output'", "'llm_input'", "'tool_result_raw'", "'conversation_insight'"]) + ')'
4302
4316
  # 只统计用户可见的消息数(排除 llm_output 和 conversation_insight)
4303
4317
  if agent:
4304
- # [v1.25.7] 使用 agent_id 字段精确匹配,同时兼容旧数据(agent_id为空时回退到前缀匹配)
4318
+ # [v1.27.2] 先将 agent 路径转为数字 ID,用数字 ID 精确过滤
4319
+ target_aid = self.core.memory.get_agent_id(agent) if agent else 0
4305
4320
  if agent == "default":
4306
- # default agent: agent_id='' agent_id='default' 旧格式 web_default/sess_*
4307
- rows = self.core.memory._get_conn().execute(
4308
- """SELECT DISTINCT session_id, COUNT(*) as cnt, MAX(created_at) as last,
4309
- MAX(CASE WHEN agent_id != '' AND agent_id != 'default' THEN agent_id ELSE NULL END) as agent_id FROM memories
4321
+ # default agent: agent_id=0(未分配)或 agent_id=1(default 的数字 ID)或 旧格式
4322
+ rows = conn.execute(
4323
+ f"""SELECT DISTINCT session_id, COUNT(*) as cnt, MAX(created_at) as last,
4324
+ MAX(CASE WHEN agent_id > 1 THEN agent_id ELSE NULL END) as raw_agent_id FROM memories
4310
4325
  WHERE category = 'session' AND role != ''
4311
- AND key NOT IN ('llm_output', 'llm_input', 'tool_result_raw', 'conversation_insight')
4312
- AND (agent_id = '' OR agent_id = 'default' OR session_id = 'web_default' OR session_id LIKE 'sess_%')
4313
- GROUP BY session_id ORDER BY last DESC LIMIT 100""").fetchall()
4326
+ AND key NOT IN {_hidden}
4327
+ AND (agent_id = 0 OR agent_id = ? OR session_id = 'web_default' OR session_id LIKE 'sess_%')
4328
+ GROUP BY session_id ORDER BY last DESC LIMIT 100""",
4329
+ (target_aid,)).fetchall()
4314
4330
  else:
4315
- # 其他 agent: agent_id=agent 或 session_id 前缀匹配(旧数据)
4316
- rows = self.core.memory._get_conn().execute(
4317
- """SELECT DISTINCT session_id, COUNT(*) as cnt, MAX(created_at) as last,
4318
- MAX(CASE WHEN agent_id != '' AND agent_id != ? THEN agent_id ELSE NULL END) as agent_id FROM memories
4331
+ # 其他 agent: agent_id=数字ID 或 session_id 前缀匹配(旧数据)
4332
+ rows = conn.execute(
4333
+ f"""SELECT DISTINCT session_id, COUNT(*) as cnt, MAX(created_at) as last,
4334
+ MAX(CASE WHEN agent_id != ? THEN agent_id ELSE NULL END) as raw_agent_id FROM memories
4319
4335
  WHERE category = 'session' AND role != ''
4320
- AND key NOT IN ('llm_output', 'llm_input', 'tool_result_raw', 'conversation_insight')
4336
+ AND key NOT IN {_hidden}
4321
4337
  AND (agent_id = ? OR session_id LIKE ?)
4322
4338
  GROUP BY session_id ORDER BY last DESC LIMIT 100""",
4323
- (agent, agent, f"{agent}_%")).fetchall()
4339
+ (target_aid, target_aid, f"{agent}_%")).fetchall()
4324
4340
  else:
4325
- # [v1.25.7] 无 agent 过滤时也返回 agent_id,供后台管理页面使用
4326
- rows = self.core.memory._get_conn().execute(
4327
- "SELECT DISTINCT session_id, COUNT(*) as cnt, MAX(created_at) as last, "
4328
- "MAX(CASE WHEN agent_id != '' THEN agent_id ELSE NULL END) as agent_id FROM memories "
4329
- "WHERE category = 'session' AND role != '' "
4330
- "AND key NOT IN ('llm_output', 'llm_input', 'tool_result_raw', 'conversation_insight') "
4331
- "GROUP BY session_id ORDER BY last DESC LIMIT 100").fetchall()
4332
- sessions = [{"id": r["session_id"], "messages": r["cnt"], "last": r["last"],
4333
- "agent_id": (r["agent_id"] or ""),
4334
- "display_name": "", "preview": ""} for r in rows]
4341
+ # [v1.27.2] 无 agent 过滤时也返回 agent_id,供后台管理页面使用
4342
+ rows = conn.execute(
4343
+ f"SELECT DISTINCT session_id, COUNT(*) as cnt, MAX(created_at) as last, "
4344
+ f"MAX(CASE WHEN agent_id > 0 THEN agent_id ELSE NULL END) as raw_agent_id FROM memories "
4345
+ f"WHERE category = 'session' AND role != '' "
4346
+ f"AND key NOT IN {_hidden} "
4347
+ f"GROUP BY session_id ORDER BY last DESC LIMIT 100").fetchall()
4348
+ # [v1.27.2] 将数字 agent_id 转为 agent 名称
4349
+ sessions = []
4350
+ for r in rows:
4351
+ raw_aid = r["raw_agent_id"]
4352
+ agent_name = ""
4353
+ if raw_aid is not None:
4354
+ agent_name = _agent_id_map.get(int(raw_aid), "")
4355
+ sessions.append({"id": r["session_id"], "messages": r["cnt"], "last": r["last"],
4356
+ "agent_id": agent_name, # 返回 agent 名称(非数字 ID)
4357
+ "agent_db_id": (int(raw_aid) if raw_aid is not None else 0), # 数字 ID 供 URL 使用
4358
+ "display_name": "", "preview": ""})
4335
4359
  # 批量获取自定义会话名称
4336
4360
  sids = [s["id"] for s in sessions]
4337
4361
  name_map = self.core.memory.list_session_names(sids) if sids else {}
@@ -4360,22 +4384,24 @@ window.addEventListener('beforeunload', function() {{
4360
4384
  name = request.match_info["name"]
4361
4385
  if not self.core.memory:
4362
4386
  return web.json_response({"agent": name, "sessions": []})
4363
- # [v1.25.7] 使用 agent_id 字段精确匹配,同时兼容旧数据
4387
+ # [v1.27.2] 使用数字 agent_id 精确过滤,同时兼容旧数据
4388
+ target_aid = self.core.memory.get_agent_id(name)
4364
4389
  if name == "default":
4365
- # default agent: agent_id='' 或 agent_id='default' 或 旧格式 web_default/sess_*
4390
+ # default agent: agent_id=0 或 agent_id=1 或 旧格式
4366
4391
  rows = self.core.memory._get_conn().execute(
4367
4392
  """SELECT DISTINCT session_id, COUNT(*) as cnt, MAX(created_at) as last FROM memories
4368
4393
  WHERE category = 'session'
4369
- AND (agent_id = '' OR agent_id = 'default' OR session_id = 'web_default' OR session_id LIKE 'sess_%')
4370
- GROUP BY session_id ORDER BY last DESC LIMIT 100""").fetchall()
4394
+ AND (agent_id = 0 OR agent_id = ? OR session_id = 'web_default' OR session_id LIKE 'sess_%')
4395
+ GROUP BY session_id ORDER BY last DESC LIMIT 100""",
4396
+ (target_aid,)).fetchall()
4371
4397
  else:
4372
- # 其他 agent: agent_id=name 或 session_id 前缀匹配(旧数据)
4398
+ # 其他 agent: agent_id=数字ID 或 session_id 前缀匹配(旧数据)
4373
4399
  rows = self.core.memory._get_conn().execute(
4374
4400
  """SELECT DISTINCT session_id, COUNT(*) as cnt, MAX(created_at) as last FROM memories
4375
4401
  WHERE category = 'session'
4376
4402
  AND (agent_id = ? OR session_id LIKE ?)
4377
4403
  GROUP BY session_id ORDER BY last DESC LIMIT 100""",
4378
- (name, f"{name}_%")).fetchall()
4404
+ (target_aid, f"{name}_%")).fetchall()
4379
4405
  sessions = [{"id": r["session_id"], "messages": r["cnt"], "last": r["last"]} for r in rows]
4380
4406
  # 批量获取自定义会话名称
4381
4407
  sids = [s["id"] for s in sessions]
@@ -4398,30 +4424,34 @@ window.addEventListener('beforeunload', function() {{
4398
4424
  async def handle_get_messages(self, request):
4399
4425
  sid = request.match_info["sid"]
4400
4426
  if not self.core.memory: return web.json_response([])
4401
- limit = min(int(request.query.get("limit", 500)), 500)
4402
- offset = int(request.query.get("offset", 0))
4403
- entries = self.core.memory.get_conversation(sid, limit=limit + offset)
4404
- entries = entries[offset:]
4405
- # Filter out internal entries (LLM raw output only)
4406
- entries = [e for e in entries if (e.key or "") not in self._HIDDEN_KEYS]
4407
- result = []
4408
- for e in entries:
4409
- msg = {"role": e.role, "content": e.content, "time": e.created_at, "key": e.key or ""}
4410
- # [v1.16.17] 附加附件元数据
4411
- meta = e.metadata or {}
4412
- if meta.get("images"):
4413
- msg["images"] = meta["images"]
4414
- # [v1.23.19] 分离 _files 和 _media(前端用 msg._files 和 msg._media)
4415
- _raw_files = meta.get("files") or []
4416
- if _raw_files:
4417
- _file_items = [f for f in _raw_files if f.get("_type") != "media"]
4418
- _media_items = [f for f in _raw_files if f.get("_type") == "media"]
4419
- if _file_items:
4420
- msg["_files"] = _file_items
4421
- if _media_items:
4422
- msg["_media"] = _media_items
4427
+ try:
4428
+ limit = min(int(request.query.get("limit", 500)), 500)
4429
+ offset = int(request.query.get("offset", 0))
4430
+ entries = self.core.memory.get_conversation(sid, limit=limit + offset)
4431
+ entries = entries[offset:]
4432
+ # Filter out internal entries (LLM raw output only)
4433
+ entries = [e for e in entries if (e.key or "") not in self._HIDDEN_KEYS]
4434
+ result = []
4435
+ for e in entries:
4436
+ msg = {"role": e.role, "content": e.content, "time": e.created_at, "key": e.key or ""}
4437
+ # [v1.16.17] 附加附件元数据
4438
+ meta = e.metadata or {}
4439
+ if meta.get("images"):
4440
+ msg["images"] = meta["images"]
4441
+ # [v1.23.19] 分离 _files 和 _media(前端用 msg._files msg._media)
4442
+ _raw_files = meta.get("files") or []
4443
+ if _raw_files:
4444
+ _file_items = [f for f in _raw_files if f.get("_type") != "media"]
4445
+ _media_items = [f for f in _raw_files if f.get("_type") == "media"]
4446
+ if _file_items:
4447
+ msg["_files"] = _file_items
4448
+ if _media_items:
4449
+ msg["_media"] = _media_items
4423
4450
  result.append(msg)
4424
4451
  return web.json_response(result)
4452
+ except Exception as ex:
4453
+ logger.error(f"加载会话消息失败 sid={sid}: {ex}")
4454
+ return web.json_response({"error": f"加载消息失败: {str(ex)[:200]}"}, status=500)
4425
4455
 
4426
4456
  async def handle_get_raw_messages(self, request):
4427
4457
  """GET /api/sessions/{sid}/raw - 获取会话全部原始消息(含 llm_output 等)"""
@@ -4459,30 +4489,34 @@ window.addEventListener('beforeunload', function() {{
4459
4489
  if not sid:
4460
4490
  return web.json_response([])
4461
4491
  if not self.core.memory: return web.json_response([])
4462
- limit = min(int(request.query.get("limit", 500)), 500)
4463
- offset = int(request.query.get("offset", 0))
4464
- entries = self.core.memory.get_conversation(sid, limit=limit + offset)
4465
- entries = entries[offset:]
4466
- # Filter out internal entries (LLM raw output only)
4467
- entries = [e for e in entries if (e.key or "") not in self._HIDDEN_KEYS]
4468
- result = []
4469
- for e in entries:
4470
- msg = {"role": e.role, "content": e.content, "time": e.created_at, "key": e.key or ""}
4471
- # [v1.16.17] 附加附件元数据
4472
- meta = e.metadata or {}
4473
- if meta.get("images"):
4474
- msg["images"] = meta["images"]
4475
- # [v1.23.19] 分离 _files 和 _media(前端用 msg._files 和 msg._media)
4476
- _raw_files = meta.get("files") or []
4477
- if _raw_files:
4478
- _file_items = [f for f in _raw_files if f.get("_type") != "media"]
4479
- _media_items = [f for f in _raw_files if f.get("_type") == "media"]
4480
- if _file_items:
4481
- msg["_files"] = _file_items
4482
- if _media_items:
4483
- msg["_media"] = _media_items
4484
- result.append(msg)
4485
- return web.json_response(result)
4492
+ try:
4493
+ limit = min(int(request.query.get("limit", 500)), 500)
4494
+ offset = int(request.query.get("offset", 0))
4495
+ entries = self.core.memory.get_conversation(sid, limit=limit + offset)
4496
+ entries = entries[offset:]
4497
+ # Filter out internal entries (LLM raw output only)
4498
+ entries = [e for e in entries if (e.key or "") not in self._HIDDEN_KEYS]
4499
+ result = []
4500
+ for e in entries:
4501
+ msg = {"role": e.role, "content": e.content, "time": e.created_at, "key": e.key or ""}
4502
+ # [v1.16.17] 附加附件元数据
4503
+ meta = e.metadata or {}
4504
+ if meta.get("images"):
4505
+ msg["images"] = meta["images"]
4506
+ # [v1.23.19] 分离 _files 和 _media(前端用 msg._files msg._media)
4507
+ _raw_files = meta.get("files") or []
4508
+ if _raw_files:
4509
+ _file_items = [f for f in _raw_files if f.get("_type") != "media"]
4510
+ _media_items = [f for f in _raw_files if f.get("_type") == "media"]
4511
+ if _file_items:
4512
+ msg["_files"] = _file_items
4513
+ if _media_items:
4514
+ msg["_media"] = _media_items
4515
+ result.append(msg)
4516
+ return web.json_response(result)
4517
+ except Exception as ex:
4518
+ logger.error(f"加载会话消息失败 sid={sid}: {ex}")
4519
+ return web.json_response({"error": f"加载消息失败: {str(ex)[:200]}"}, status=500)
4486
4520
 
4487
4521
  def _cleanup_session_state(self, sid: str):
4488
4522
  """删除会话时清理所有关联的后端状态"""
@@ -557,8 +557,9 @@ function confirmDeleteAgent(path,name){
557
557
  }
558
558
 
559
559
  // 直接以执行模式打开与指定 Agent 的对话
560
+ // [v1.27.2] 使用 a 参数(UrlCodec 编码),不再明文暴露 agent 路径
560
561
  function chatWithAgent(path){
561
- window.location.href='/ui/chat/chat_container.html?agent='+encodeURIComponent(path)+'&mode=exec';
562
+ window.location.href='/ui/chat/chat_container.html?mode=exec&a='+UrlCodec.encode(path);
562
563
  }
563
564
 
564
565
  if (typeof window._adminRenderers === 'undefined') window._adminRenderers = {};
@@ -7,6 +7,9 @@ let allAgentsCache=[];
7
7
  let allModelsCache=[];
8
8
  let allDeptsCache=[];
9
9
 
10
+ // [v1.27.2] UrlCodec: 与 chat 页面共享的 URL 编解码(避免 agent/session 明文暴露)
11
+ var UrlCodec=(function(){var _p='x';function _encode(str){if(!str)return'';try{var bytes=new TextEncoder().encode(str);var bin='';for(var i=0;i<bytes.length;i++)bin+=String.fromCharCode(bytes[i]);return _p+btoa(bin).replace(/=/g,'').replace(/\+/g,'-').replace(/\//g,'_')}catch(e){return str}}function _decode(encoded){if(!encoded||encoded[0]!==_p)return encoded;try{var b64=encoded.substring(1).replace(/-/g,'+').replace(/_/g,'/');while(b64.length%4)b64+='=';var bin=atob(b64);var bytes=new Uint8Array(bin.length);for(var i=0;i<bin.length;i++)bytes[i]=bin.charCodeAt(i);return new TextDecoder().decode(bytes)}catch(e){return encoded}}return{encode:_encode,decode:_decode}})();
12
+
10
13
  function esc(s){if(!s)return'';return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');}
11
14
 
12
15
  async function api(url,opts={}){
@@ -7,7 +7,7 @@ async function renderSessions(){
7
7
  }
8
8
  let html='<div class="table-wrap"><table><tr><th>会话</th><th>Agent</th><th>消息数</th><th>最后活动</th><th>操作</th></tr>';
9
9
  for(const s of (ss||[])){
10
- // [v1.25.7] 优先使用后端返回的 agent_id,不再从 session_id 猜测
10
+ // [v1.27.2] 优先使用后端返回的 agent_id(现在是 agent 名称),回退从前缀猜测
11
11
  let agentName=s.agent_id||'';
12
12
  if(!agentName){
13
13
  // 回退:从 session_id 前缀提取(兼容旧格式)
@@ -20,8 +20,9 @@ async function renderSessions(){
20
20
  }
21
21
  }
22
22
  const displayName=s.display_name||s.id;
23
+ const dbId=s.agent_db_id||1;
23
24
  html+=`<tr><td title="${escHtml(s.id)}">${escHtml(displayName.length>30?displayName.slice(0,30)+'...':displayName)}</td><td>${escHtml(agentName)}</td><td>${s.messages}</td><td>${s.last?.slice(0,19)||''}</td>
24
- <td><button class="btn btn-sm" style="background:var(--success);color:#fff" onclick="enterSession('${escHtml(s.id)}','${escHtml(agentName)}')">切入</button>
25
+ <td><button class="btn btn-sm" style="background:var(--success);color:#fff" onclick="enterSession('${escHtml(s.id)}',${dbId})">切入</button>
25
26
  <button class="btn btn-sm btn-ghost" onclick="viewSession('${escHtml(s.id)}')">查看</button>
26
27
  <button class="btn btn-sm btn-danger" onclick="clearSession('${escHtml(s.id)}')">清除</button></td></tr>`;}
27
28
  html+='</table></div>';
@@ -86,7 +87,7 @@ async function _loadSessionMessages(){
86
87
  if(hasMore)html+=`<button class="btn btn-ghost mt-8" onclick="window._viewSessionOffset=${offset+100};_loadSessionMessages()">加载更多...</button>`;
87
88
  html+='<div class="flex gap-8 mt-8"><button class="btn btn-ghost" onclick="goBack()">返回</button>';
88
89
  html+=`<button class="btn" style="background:var(--accent);color:#fff" onclick="viewSessionRaw('${escHtml(sid)}')">Raw 原始消息</button>`;
89
- html+=`<button class="btn btn-primary" onclick="enterSession('${escHtml(sid)}','${escHtml(sid.split('_web_')[0]||'default')}')">在聊天中查看完整记录</button></div>`;
90
+ html+=`<button class="btn btn-primary" onclick="enterSession('${escHtml(sid)}',1)">在聊天中查看完整记录</button></div>`;
90
91
  $('content').innerHTML=html;
91
92
  }
92
93
  // ========== Raw 原始消息查看 ==========
@@ -233,8 +234,9 @@ function _buildTimeNav(msgs){
233
234
  }
234
235
  async function clearSession(sid){await api(`/api/sessions/${encodeURIComponent(sid)}`,{method:'DELETE'});renderSessions()}
235
236
  // 切入会话: 打开聊天页面并自动加载指定会话
236
- function enterSession(sid,agentName){
237
- window.location.href='/ui/chat/chat_container.html?agent='+encodeURIComponent(agentName)+'&mode=exec&session='+encodeURIComponent(sid);
237
+ // [v1.27.2] 使用 aid(数字 agent ID)+ s(编码 session ID),不再暴露明文 agent 名字
238
+ function enterSession(sid,agentDbId){
239
+ window.location.href='/ui/chat/chat_container.html?aid='+encodeURIComponent(agentDbId||1)+'&mode=exec&s='+UrlCodec.encode(sid);
238
240
  }
239
241
 
240
242
  if (typeof window._adminRenderers === 'undefined') window._adminRenderers = {};
@@ -136,10 +136,12 @@ function popoutChat() {
136
136
  if (isMobile()) return;
137
137
  // Build URL with current agent + session + mode, plus popout flag
138
138
  // Note: chat_container.html is always at /ui/chat/chat_container.html
139
+ // [v1.27.2] 使用 aid + s,不再暴露明文 agent 名字
139
140
  const baseUrl = window.location.origin + '/ui/chat/chat_container.html';
140
141
  const params = new URLSearchParams();
141
- if (state.activeAgent) params.set('agent', state.activeAgent);
142
- if (state.activeSessionId && state.activeSessionId !== '__new__') params.set('session', state.activeSessionId);
142
+ var _popAgent = findAgentByPath(state.activeAgent);
143
+ if (_popAgent && _popAgent.aid) params.set('aid', _popAgent.aid);
144
+ if (state.activeSessionId && state.activeSessionId !== '__new__') params.set('s', UrlCodec.encode(state.activeSessionId));
143
145
  if (state.chatMode) params.set('mode', state.chatMode);
144
146
  params.set('popout', '1');
145
147
  const popoutUrl = baseUrl + '?' + params.toString();
@@ -373,9 +375,12 @@ async function initChat() {
373
375
  // [v1.16.12] 初始化附件上传 UI
374
376
  initAttachmentUI();
375
377
 
376
- // URL 参数处理: ?a=xxx&mode=exec&s=xxx&g=xxx&popout=1(agent/session 使用 UrlCodec 编码)
378
+ // URL 参数处理: ?aid=数字ID&mode=exec&s=编码session&g=群聊ID&popout=1
379
+ // [v1.27.2] 使用 aid(数字 agent ID)代替明文 agent 名字
377
380
  const urlParams = new URLSearchParams(window.location.search);
378
- const urlAgent = UrlCodec.decode(urlParams.get('a') || '') || UrlCodec.decode(urlParams.get('agent') || '');
381
+ const urlAid = parseInt(urlParams.get('aid') || '', 10) || 0;
382
+ // 兼容旧格式 ?a= 编码 或 ?agent= 明文
383
+ const urlAgentLegacy = UrlCodec.decode(urlParams.get('a') || '') || UrlCodec.decode(urlParams.get('agent') || '');
379
384
  const urlMode = urlParams.get('mode');
380
385
  const urlSession = UrlCodec.decode(urlParams.get('s') || '') || UrlCodec.decode(urlParams.get('session') || '');
381
386
  const isPopout = urlParams.get('popout') === '1';
@@ -463,25 +468,47 @@ async function initChat() {
463
468
  await loadAgents();
464
469
 
465
470
  // loadAgents 完成后,agent 列表和 sessions 已加载完毕
466
- // 现在可以安全地做 agent/session 选择(不会再和 sendMessage 竞态)
471
+ // [v1.27.2] 构建 aid path 映射表
472
+ var _aidToPath = {};
473
+ state.agentsFlat.forEach(function(a) {
474
+ if (a.aid) _aidToPath[a.aid] = a.path;
475
+ });
476
+ // 从 URL 解析 agent: 优先 aid(数字),兼容 a/agent(编码/明文)
477
+ var urlAgent = '';
478
+ if (urlAid && _aidToPath[urlAid]) {
479
+ urlAgent = _aidToPath[urlAid];
480
+ } else if (urlAgentLegacy) {
481
+ urlAgent = urlAgentLegacy;
482
+ }
483
+ // 清理 URL 中的旧格式参数(agent/a/session),统一使用 aid/s
484
+ try {
485
+ var _cleanUrl = new URL(window.location.href);
486
+ _cleanUrl.searchParams.delete('agent');
487
+ _cleanUrl.searchParams.delete('a');
488
+ _cleanUrl.searchParams.delete('session');
489
+ if (urlAgent && !urlAid) {
490
+ // 如果没有 aid 但有旧格式 agent,添加 aid
491
+ var _agentObj = findAgentByPath(urlAgent);
492
+ if (_agentObj && _agentObj.aid) _cleanUrl.searchParams.set('aid', _agentObj.aid);
493
+ }
494
+ window.history.replaceState({}, '', _cleanUrl.toString());
495
+ } catch (_) {}
496
+
467
497
  if (isPopout) {
468
498
  const agentObj = findAgentByPath(urlAgent);
469
499
  document.title = (agentObj ? agentObj.name : urlAgent || 'MyAgent') + ' - MyAgent';
470
500
  }
471
501
 
472
502
  // [v1.23.81] 群聊恢复逻辑:从 URL 的 ?g= 纯数字 session ID 恢复(优先级最高)
473
- // 不再依赖 JS 状态传递(from_group),完全通过 URL session ID 恢复
474
- state._pendingGroupRestore = null; // 不再使用 localStorage 中的群聊状态
503
+ state._pendingGroupRestore = null;
475
504
 
476
505
  async function _restoreGroupBySession(sessionId) {
477
- // 通过 API 将纯数字 session ID 解析为 group_id
478
506
  try {
479
507
  var resolveData = await api('/api/group-session/' + encodeURIComponent(sessionId));
480
508
  if (resolveData && resolveData.group_id) {
481
509
  if (typeof groups !== 'undefined' && groups.length > 0) {
482
510
  selectGroup(resolveData.group_id);
483
511
  } else {
484
- // groups 尚未加载,等待后重试
485
512
  setTimeout(function() { _restoreGroupBySession(sessionId); }, 200);
486
513
  }
487
514
  } else {
@@ -494,14 +521,12 @@ async function initChat() {
494
521
 
495
522
  // 如果 URL 中有 ?g= 纯数字 session ID,优先恢复群聊
496
523
  if (_effectiveGroupSession && /^[0-9]+$/.test(_effectiveGroupSession)) {
497
- // 群聊模式:如果有 from_agent 参数,更新 state.activeAgent 以便后续群聊私聊时使用
498
524
  if (urlAgent && urlAgent !== state.activeAgent) {
499
525
  state.activeAgent = urlAgent;
500
526
  StatePersistence.save('activeAgent', urlAgent);
501
527
  }
502
528
  _restoreGroupBySession(_effectiveGroupSession);
503
529
  } else if (urlAgent) {
504
- // URL 指定了 agent,校验后切换
505
530
  var resolved = urlAgent;
506
531
  if (!findAgentByPath(resolved)) {
507
532
  console.warn('URL agent not found, fallback to default:', resolved);
@@ -2798,11 +2823,17 @@ function newChat() {
2798
2823
  state.activeSessionId = '__new__';
2799
2824
  state._selectedSessionLabel = null; // [v1.18.9] 清除选中的会话名称
2800
2825
  // ── 更新 URL(新对话移除 session 参数) ──
2826
+ // [v1.27.2] 使用 aid 代替 a
2801
2827
  try {
2802
2828
  const url = new URL(window.location.href);
2803
2829
  url.searchParams.delete('s');
2804
2830
  url.searchParams.delete('session');
2805
- url.searchParams.set('a', UrlCodec.encode(state.activeAgent || 'default'));
2831
+ url.searchParams.delete('a');
2832
+ url.searchParams.delete('agent');
2833
+ var _ncAgentObj = findAgentByPath(state.activeAgent);
2834
+ if (_ncAgentObj && _ncAgentObj.aid) {
2835
+ url.searchParams.set('aid', _ncAgentObj.aid);
2836
+ }
2806
2837
  window.history.replaceState({}, '', url.toString());
2807
2838
  } catch (_) {}
2808
2839
  state.messages = [];
@@ -2948,10 +2979,19 @@ async function selectSession(id) {
2948
2979
  state._msgLoadTotal = 0;
2949
2980
 
2950
2981
  // ── 更新 URL 参数,保留会话 ID(刷新页面可恢复会话) ──
2982
+ // [v1.27.2] 使用 aid(数字)代替 a(编码),移除明文 agent/session 参数
2951
2983
  try {
2952
2984
  const url = new URL(window.location.href);
2953
2985
  url.searchParams.set('s', UrlCodec.encode(id));
2954
- url.searchParams.set('a', UrlCodec.encode(state.activeAgent || 'default'));
2986
+ // 移除旧格式参数
2987
+ url.searchParams.delete('a');
2988
+ url.searchParams.delete('agent');
2989
+ url.searchParams.delete('session');
2990
+ // 设置 aid(数字 agent ID)
2991
+ var _selAgentObj = findAgentByPath(state.activeAgent);
2992
+ if (_selAgentObj && _selAgentObj.aid) {
2993
+ url.searchParams.set('aid', _selAgentObj.aid);
2994
+ }
2955
2995
  if (state.chatMode) url.searchParams.set('mode', state.chatMode);
2956
2996
  window.history.replaceState({}, '', url.toString());
2957
2997
  } catch (_) {}
@@ -1648,7 +1648,12 @@ async function sendMessage(opts) {
1648
1648
  try {
1649
1649
  const url = new URL(window.location.href);
1650
1650
  url.searchParams.set('s', UrlCodec.encode(sessionId));
1651
- url.searchParams.set('a', UrlCodec.encode(state.activeAgent || 'default'));
1651
+ // [v1.27.2] 使用 aid 代替 a
1652
+ url.searchParams.delete('a');
1653
+ url.searchParams.delete('agent');
1654
+ url.searchParams.delete('session');
1655
+ var _feAgentObj = (typeof findAgentByPath === 'function') ? findAgentByPath(state.activeAgent) : null;
1656
+ if (_feAgentObj && _feAgentObj.aid) url.searchParams.set('aid', _feAgentObj.aid);
1652
1657
  window.history.replaceState({}, '', url.toString());
1653
1658
  } catch (_) {}
1654
1659
  }
@@ -2874,7 +2879,12 @@ const StreamingRecovery = {
2874
2879
  try {
2875
2880
  const url = new URL(window.location.href);
2876
2881
  url.searchParams.set('s', UrlCodec.encode(sessionId));
2877
- url.searchParams.set('a', UrlCodec.encode(state.activeAgent || 'default'));
2882
+ // [v1.27.2] 使用 aid 代替 a
2883
+ url.searchParams.delete('a');
2884
+ url.searchParams.delete('agent');
2885
+ url.searchParams.delete('session');
2886
+ var _rvAgentObj = (typeof findAgentByPath === 'function') ? findAgentByPath(state.activeAgent) : null;
2887
+ if (_rvAgentObj && _rvAgentObj.aid) url.searchParams.set('aid', _rvAgentObj.aid);
2878
2888
  window.history.replaceState({}, '', url.toString());
2879
2889
  } catch (e) {
2880
2890
  console.error('[Recovery] 更新URL失败:', e);