myagent-ai 1.23.78 → 1.23.79

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.
@@ -0,0 +1,482 @@
1
+ """
2
+ core/agent_storage.py - Agent 配置数据库管理
3
+ =============================================
4
+ 将 agent 的配置信息从 config.json 文件迁移到 SQLite 数据库。
5
+ 文件类内容(soul.md, identity.md, user.md, workspace/, avatar.png)仍保留在本地目录。
6
+
7
+ 数据库表结构:
8
+ agents: 存储所有 agent 配置信息
9
+
10
+ 迁移策略:
11
+ 首次启动时自动检测并迁移已有 config.json 到数据库。
12
+ 迁移完成后不再读写 config.json。
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import os
18
+ import sqlite3
19
+ import time
20
+ import uuid
21
+ from dataclasses import dataclass, field, asdict
22
+ from pathlib import Path
23
+ from typing import Any, Dict, List, Optional
24
+
25
+ from core.logger import get_logger
26
+
27
+ logger = get_logger("myagent.agent_storage")
28
+
29
+
30
+ # ==============================================================================
31
+ # 数据模型
32
+ # ==============================================================================
33
+
34
+ @dataclass
35
+ class AgentConfig:
36
+ """Agent 配置(对应数据库一行)"""
37
+ path: str = "" # Agent 路径,如 "default", "coder/python-expert"
38
+ id: str = "" # 唯一 ID,12位 hex
39
+ name: str = "" # 显示名称
40
+ description: str = "" # 描述
41
+ avatar_color: str = "" # 头像颜色
42
+ avatar_emoji: str = "" # 头像 emoji
43
+ avatar_image: str = "" # 头像图片 URL
44
+ execution_mode: str = "sandbox" # 执行模式: local / sandbox
45
+ enabled: bool = True # 是否启用
46
+ system: bool = False # 是否系统内置 Agent
47
+ system_prompt: str = "" # 系统提示词
48
+ model: str = "" # 模型覆盖
49
+ parent: str = "" # 父 Agent 路径
50
+ # 平台绑定
51
+ platform: str = "" # 平台标识
52
+ platform_token: str = "" # 平台 Token
53
+ platform_app_id: str = "" # 平台 App ID
54
+ platform_app_secret: str = "" # 平台 App Secret
55
+ # 模型库引用
56
+ model_id: str = "" # 模型库 ID
57
+ backup_model_ids: str = "" # 备选模型 ID(JSON 数组字符串)
58
+ # 其他
59
+ work_dir: str = "" # 自定义工作目录
60
+ department: str = "" # 所属部门
61
+ # 时间戳
62
+ created_at: str = ""
63
+ updated_at: str = ""
64
+
65
+ # ---- 以下字段不存数据库,运行时从文件读取 ----
66
+
67
+ def to_dict(self) -> dict:
68
+ """转为字典"""
69
+ d = {}
70
+ for k in self.__dataclass_fields__:
71
+ v = getattr(self, k)
72
+ if k == "enabled" or k == "system":
73
+ d[k] = bool(v)
74
+ else:
75
+ d[k] = v if v is not None else ""
76
+ return d
77
+
78
+ def to_db_row(self) -> dict:
79
+ """转为数据库行(只包含数据库字段)"""
80
+ return self.to_dict()
81
+
82
+ @classmethod
83
+ def from_db_row(cls, row: sqlite3.Row) -> "AgentConfig":
84
+ """从数据库行创建"""
85
+ d = dict(row)
86
+ return cls(**{k: d.get(k, "") for k in cls.__dataclass_fields__})
87
+
88
+ @classmethod
89
+ def from_json(cls, path: str, data: dict) -> "AgentConfig":
90
+ """从 config.json 字典创建"""
91
+ return cls(
92
+ path=path,
93
+ id=data.get("id", ""),
94
+ name=data.get("name", path),
95
+ description=data.get("description", ""),
96
+ avatar_color=data.get("avatar_color", ""),
97
+ avatar_emoji=data.get("avatar_emoji", ""),
98
+ avatar_image=data.get("avatar_image", ""),
99
+ execution_mode=data.get("execution_mode", "sandbox"),
100
+ enabled=bool(data.get("enabled", True)),
101
+ system=bool(data.get("system", False)),
102
+ system_prompt=data.get("system_prompt", ""),
103
+ model=data.get("model", ""),
104
+ parent=data.get("parent", ""),
105
+ platform=data.get("platform", ""),
106
+ platform_token=data.get("platform_token", ""),
107
+ platform_app_id=data.get("platform_app_id", ""),
108
+ platform_app_secret=data.get("platform_app_secret", ""),
109
+ model_id=data.get("model_id", ""),
110
+ backup_model_ids=json.dumps(data.get("backup_model_ids", []), ensure_ascii=False) if isinstance(data.get("backup_model_ids"), list) else data.get("backup_model_ids", ""),
111
+ work_dir=data.get("work_dir", ""),
112
+ department=data.get("department", ""),
113
+ created_at=data.get("created_at", ""),
114
+ updated_at=data.get("updated_at", ""),
115
+ )
116
+
117
+
118
+ # ==============================================================================
119
+ # AgentStorage - 数据库管理类
120
+ # ==============================================================================
121
+
122
+ class AgentStorage:
123
+ """
124
+ Agent 配置数据库管理器。
125
+
126
+ 职责:
127
+ - 建表和版本管理
128
+ - Agent CRUD(增删改查)
129
+ - 首次启动时自动从 config.json 迁移数据
130
+ - 提供兼容旧代码的接口
131
+ """
132
+
133
+ def __init__(self, db_path: str = ""):
134
+ if not db_path:
135
+ db_path = str(Path.home() / ".myagent" / "data" / "agents.db")
136
+ self._db_path = db_path
137
+ self._conn: Optional[sqlite3.Connection] = None
138
+ self._lock = __import__("threading").Lock()
139
+
140
+ def _get_conn(self) -> sqlite3.Connection:
141
+ """获取数据库连接(线程安全)"""
142
+ if self._conn is None:
143
+ self._conn = sqlite3.connect(self._db_path, check_same_thread=False)
144
+ self._conn.row_factory = sqlite3.Row
145
+ self._conn.execute("PRAGMA journal_mode=WAL")
146
+ self._conn.execute("PRAGMA foreign_keys=ON")
147
+ self._init_tables()
148
+ return self._conn
149
+
150
+ def _init_tables(self):
151
+ """初始化数据库表"""
152
+ conn = self._get_conn()
153
+ conn.executescript("""
154
+ CREATE TABLE IF NOT EXISTS agents (
155
+ path TEXT PRIMARY KEY,
156
+ id TEXT NOT NULL DEFAULT '',
157
+ name TEXT NOT NULL DEFAULT '',
158
+ description TEXT NOT NULL DEFAULT '',
159
+ avatar_color TEXT NOT NULL DEFAULT '',
160
+ avatar_emoji TEXT NOT NULL DEFAULT '',
161
+ avatar_image TEXT NOT NULL DEFAULT '',
162
+ execution_mode TEXT NOT NULL DEFAULT 'sandbox',
163
+ enabled INTEGER NOT NULL DEFAULT 1,
164
+ system INTEGER NOT NULL DEFAULT 0,
165
+ system_prompt TEXT NOT NULL DEFAULT '',
166
+ model TEXT NOT NULL DEFAULT '',
167
+ parent TEXT NOT NULL DEFAULT '',
168
+ platform TEXT NOT NULL DEFAULT '',
169
+ platform_token TEXT NOT NULL DEFAULT '',
170
+ platform_app_id TEXT NOT NULL DEFAULT '',
171
+ platform_app_secret TEXT NOT NULL DEFAULT '',
172
+ model_id TEXT NOT NULL DEFAULT '',
173
+ backup_model_ids TEXT NOT NULL DEFAULT '[]',
174
+ work_dir TEXT NOT NULL DEFAULT '',
175
+ department TEXT NOT NULL DEFAULT '',
176
+ created_at TEXT NOT NULL DEFAULT '',
177
+ updated_at TEXT NOT NULL DEFAULT ''
178
+ );
179
+
180
+ CREATE INDEX IF NOT EXISTS idx_agents_id ON agents(id);
181
+ CREATE INDEX IF NOT EXISTS idx_agents_parent ON agents(parent);
182
+ CREATE INDEX IF NOT EXISTS idx_agents_department ON agents(department);
183
+ CREATE INDEX IF NOT EXISTS idx_agents_enabled ON agents(enabled);
184
+ """)
185
+ conn.commit()
186
+
187
+ # ==========================================================================
188
+ # CRUD 操作
189
+ # ==========================================================================
190
+
191
+ def get(self, path: str) -> Optional[AgentConfig]:
192
+ """根据 path 获取 agent 配置"""
193
+ conn = self._get_conn()
194
+ with self._lock:
195
+ row = conn.execute("SELECT * FROM agents WHERE path = ?", (path,)).fetchone()
196
+ if row:
197
+ return AgentConfig.from_db_row(row)
198
+ return None
199
+
200
+ def get_by_id(self, agent_id: str) -> Optional[AgentConfig]:
201
+ """根据唯一 ID 获取 agent 配置"""
202
+ conn = self._get_conn()
203
+ with self._lock:
204
+ row = conn.execute("SELECT * FROM agents WHERE id = ?", (agent_id,)).fetchone()
205
+ if row:
206
+ return AgentConfig.from_db_row(row)
207
+ return None
208
+
209
+ def list_all(self, enabled_only: bool = False) -> List[AgentConfig]:
210
+ """列出所有 agent"""
211
+ conn = self._get_conn()
212
+ with self._lock:
213
+ if enabled_only:
214
+ rows = conn.execute("SELECT * FROM agents WHERE enabled = 1 ORDER BY path").fetchall()
215
+ else:
216
+ rows = conn.execute("SELECT * FROM agents ORDER BY path").fetchall()
217
+ return [AgentConfig.from_db_row(r) for r in rows]
218
+
219
+ def list_children(self, parent_path: str) -> List[AgentConfig]:
220
+ """列出子 agent"""
221
+ conn = self._get_conn()
222
+ with self._lock:
223
+ rows = conn.execute("SELECT * FROM agents WHERE parent = ? ORDER BY path", (parent_path,)).fetchall()
224
+ return [AgentConfig.from_db_row(r) for r in rows]
225
+
226
+ def exists(self, path: str) -> bool:
227
+ """检查 agent 是否存在"""
228
+ conn = self._get_conn()
229
+ with self._lock:
230
+ row = conn.execute("SELECT 1 FROM agents WHERE path = ?", (path,)).fetchone()
231
+ return row is not None
232
+
233
+ def exists_id(self, agent_id: str) -> bool:
234
+ """检查 ID 是否存在"""
235
+ conn = self._get_conn()
236
+ with self._lock:
237
+ row = conn.execute("SELECT 1 FROM agents WHERE id = ?", (agent_id,)).fetchone()
238
+ return row is not None
239
+
240
+ def create(self, cfg: AgentConfig) -> AgentConfig:
241
+ """创建 agent,返回创建后的配置(含自动填充字段)"""
242
+ conn = self._get_conn()
243
+ now = _now_iso()
244
+ if not cfg.id:
245
+ cfg.id = uuid.uuid4().hex[:12]
246
+ if not cfg.created_at:
247
+ cfg.created_at = now
248
+ if not cfg.updated_at:
249
+ cfg.updated_at = now
250
+ if not cfg.name:
251
+ cfg.name = cfg.path
252
+
253
+ with self._lock:
254
+ conn.execute("""
255
+ INSERT INTO agents (path, id, name, description, avatar_color, avatar_emoji,
256
+ avatar_image, execution_mode, enabled, system, system_prompt, model,
257
+ parent, platform, platform_token, platform_app_id, platform_app_secret,
258
+ model_id, backup_model_ids, work_dir, department, created_at, updated_at)
259
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
260
+ """, (
261
+ cfg.path, cfg.id, cfg.name, cfg.description, cfg.avatar_color, cfg.avatar_emoji,
262
+ cfg.avatar_image, cfg.execution_mode, int(cfg.enabled), int(cfg.system),
263
+ cfg.system_prompt, cfg.model, cfg.parent, cfg.platform, cfg.platform_token,
264
+ cfg.platform_app_id, cfg.platform_app_secret, cfg.model_id, cfg.backup_model_ids,
265
+ cfg.work_dir, cfg.department, cfg.created_at, cfg.updated_at,
266
+ ))
267
+ conn.commit()
268
+ return cfg
269
+
270
+ def update(self, path: str, updates: dict) -> Optional[AgentConfig]:
271
+ """
272
+ 更新 agent 配置。
273
+ updates: 要更新的字段字典,只允许更新已知字段。
274
+ 返回更新后的配置,如果不存在返回 None。
275
+ """
276
+ # 允许更新的字段白名单
277
+ ALLOWED = {
278
+ "name", "description", "avatar_color", "avatar_emoji", "avatar_image",
279
+ "execution_mode", "enabled", "system_prompt", "model",
280
+ "platform", "platform_token", "platform_app_id", "platform_app_secret",
281
+ "model_id", "backup_model_ids", "work_dir", "department",
282
+ }
283
+ filtered = {k: v for k, v in updates.items() if k in ALLOWED}
284
+ if not filtered:
285
+ return self.get(path)
286
+
287
+ conn = self._get_conn()
288
+ now = _now_iso()
289
+
290
+ # backup_model_ids 需要 JSON 序列化
291
+ if "backup_model_ids" in filtered and isinstance(filtered["backup_model_ids"], list):
292
+ filtered["backup_model_ids"] = json.dumps(filtered["backup_model_ids"], ensure_ascii=False)
293
+
294
+ # enabled 需要转 int
295
+ if "enabled" in filtered:
296
+ filtered["enabled"] = int(bool(filtered["enabled"]))
297
+
298
+ filtered["updated_at"] = now
299
+
300
+ set_parts = ", ".join(f"{k} = ?" for k in filtered)
301
+ values = list(filtered.values()) + [path]
302
+
303
+ with self._lock:
304
+ cursor = conn.execute(f"UPDATE agents SET {set_parts} WHERE path = ?", values)
305
+ conn.commit()
306
+ if cursor.rowcount == 0:
307
+ return None
308
+ return self.get(path)
309
+
310
+ def rename(self, old_path: str, new_path: str) -> bool:
311
+ """
312
+ 重命名 agent(更新 path 及所有子 agent 的 path)。
313
+ 同时需要更新 parent 字段引用。
314
+ """
315
+ conn = self._get_conn()
316
+ with self._lock:
317
+ # 检查是否存在
318
+ existing = conn.execute("SELECT 1 FROM agents WHERE path = ?", (old_path,)).fetchone()
319
+ if not existing:
320
+ return False
321
+ # 检查目标是否已存在
322
+ target = conn.execute("SELECT 1 FROM agents WHERE path = ?", (new_path,)).fetchone()
323
+ if target:
324
+ return False
325
+
326
+ now = _now_iso()
327
+
328
+ # 1. 更新自身
329
+ conn.execute("UPDATE agents SET path = ?, updated_at = ? WHERE path = ?", (new_path, now, old_path))
330
+
331
+ # 2. 更新所有子 agent 的 path(前缀替换)和 parent
332
+ old_prefix = old_path + "/"
333
+ new_prefix = new_path + "/"
334
+ children = conn.execute("SELECT path FROM agents WHERE path LIKE ?", (old_prefix + "%",)).fetchall()
335
+ for child in children:
336
+ child_old = child["path"]
337
+ child_new = new_prefix + child_old[len(old_prefix):]
338
+ conn.execute("UPDATE agents SET path = ?, parent = ?, updated_at = ? WHERE path = ?",
339
+ (child_new, new_path, now, child_old))
340
+
341
+ # 3. 更新其他 agent 中 parent 引用
342
+ conn.execute("UPDATE agents SET parent = ?, updated_at = ? WHERE parent = ?",
343
+ (new_path, now, old_path))
344
+
345
+ # 4. 更新 department 字段中引用了 old_path 的记录(不太可能,但保险起见)
346
+ # department 存的是部门名不是 path,不需要更新
347
+
348
+ conn.commit()
349
+ return True
350
+
351
+ def delete(self, path: str) -> bool:
352
+ """
353
+ 删除 agent 及其所有子 agent。
354
+ 返回删除的行数。
355
+ """
356
+ conn = self._get_conn()
357
+ with self._lock:
358
+ # 先删子 agent(递归删除所有以 path/ 开头的)
359
+ conn.execute("DELETE FROM agents WHERE path LIKE ?", (path + "/%",))
360
+ # 再删自身
361
+ cursor = conn.execute("DELETE FROM agents WHERE path = ?", (path,))
362
+ conn.commit()
363
+ return cursor.rowcount > 0
364
+
365
+ def count(self) -> int:
366
+ """统计 agent 总数"""
367
+ conn = self._get_conn()
368
+ with self._lock:
369
+ row = conn.execute("SELECT COUNT(*) as cnt FROM agents").fetchone()
370
+ return row["cnt"] if row else 0
371
+
372
+ def search(self, keyword: str, limit: int = 20) -> List[AgentConfig]:
373
+ """按名称或描述搜索 agent"""
374
+ conn = self._get_conn()
375
+ with self._lock:
376
+ rows = conn.execute(
377
+ "SELECT * FROM agents WHERE name LIKE ? OR description LIKE ? ORDER BY path LIMIT ?",
378
+ (f"%{keyword}%", f"%{keyword}%", limit),
379
+ ).fetchall()
380
+ return [AgentConfig.from_db_row(r) for r in rows]
381
+
382
+ def get_all_ids(self) -> List[str]:
383
+ """获取所有 agent ID 列表(用于调试)"""
384
+ conn = self._get_conn()
385
+ with self._lock:
386
+ rows = conn.execute("SELECT id FROM agents WHERE id != ''").fetchall()
387
+ return [r["id"] for r in rows]
388
+
389
+ # ==========================================================================
390
+ # 迁移: config.json → SQLite
391
+ # ==========================================================================
392
+
393
+ def migrate_from_json(self, agents_dir: Path, force: bool = False) -> dict:
394
+ """
395
+ 从文件系统迁移 agent 配置到数据库。
396
+
397
+ Args:
398
+ agents_dir: agents 数据目录路径(~/.myagent/data/agents/)
399
+ force: 是否强制重新迁移
400
+
401
+ Returns:
402
+ {"migrated": int, "skipped": int, "errors": int}
403
+ """
404
+ conn = self._get_conn()
405
+ result = {"migrated": 0, "skipped": 0, "errors": 0}
406
+
407
+ if not agents_dir.exists():
408
+ logger.info(f"Agent 数据目录不存在,跳过迁移: {agents_dir}")
409
+ return result
410
+
411
+ if self.count() > 0 and not force:
412
+ logger.info(f"数据库已有 {self.count()} 个 agent,跳过迁移(使用 force 强制重新迁移)")
413
+ result["skipped"] = self.count()
414
+ return result
415
+
416
+ now = _now_iso()
417
+ for cfg_path in agents_dir.rglob("config.json"):
418
+ try:
419
+ # 跳过符号链接
420
+ if cfg_path.parent.is_symlink():
421
+ continue
422
+
423
+ raw = json.loads(cfg_path.read_text(encoding="utf-8"))
424
+ rel_path = cfg_path.parent.relative_to(agents_dir).as_posix()
425
+
426
+ # 补全缺失字段
427
+ if "id" not in raw or not raw["id"]:
428
+ raw["id"] = uuid.uuid4().hex[:12]
429
+ if "created_at" not in raw or not raw["created_at"]:
430
+ raw["created_at"] = now
431
+ if "updated_at" not in raw or not raw["updated_at"]:
432
+ raw["updated_at"] = now
433
+
434
+ cfg = AgentConfig.from_json(rel_path, raw)
435
+
436
+ with self._lock:
437
+ # 使用 INSERT OR REPLACE 避免重复
438
+ conn.execute("""
439
+ INSERT OR REPLACE INTO agents (path, id, name, description, avatar_color, avatar_emoji,
440
+ avatar_image, execution_mode, enabled, system, system_prompt, model,
441
+ parent, platform, platform_token, platform_app_id, platform_app_secret,
442
+ model_id, backup_model_ids, work_dir, department, created_at, updated_at)
443
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
444
+ """, (
445
+ cfg.path, cfg.id, cfg.name, cfg.description, cfg.avatar_color, cfg.avatar_emoji,
446
+ cfg.avatar_image, cfg.execution_mode, int(cfg.enabled), int(cfg.system),
447
+ cfg.system_prompt, cfg.model, cfg.parent, cfg.platform, cfg.platform_token,
448
+ cfg.platform_app_id, cfg.platform_app_secret, cfg.model_id, cfg.backup_model_ids,
449
+ cfg.work_dir, cfg.department, cfg.created_at, cfg.updated_at,
450
+ ))
451
+ result["migrated"] += 1
452
+
453
+ except Exception as e:
454
+ result["errors"] += 1
455
+ logger.warning(f"迁移 agent 失败: {cfg_path} -> {e}")
456
+
457
+ with self._lock:
458
+ conn.commit()
459
+
460
+ logger.info(f"Agent 配置迁移完成: {result['migrated']} 迁移, {result['skipped']} 跳过, {result['errors']} 错误")
461
+ return result
462
+
463
+ def close(self):
464
+ """关闭数据库连接"""
465
+ if self._conn:
466
+ self._conn.close()
467
+ self._conn = None
468
+
469
+
470
+ # ==============================================================================
471
+ # 工具函数
472
+ # ==============================================================================
473
+
474
+ def _now_iso() -> str:
475
+ """返回 ISO 格式时间戳"""
476
+ from datetime import datetime, timezone
477
+ try:
478
+ from core.utils import get_config_tz
479
+ tz = get_config_tz()
480
+ return datetime.now(tz).strftime("%Y-%m-%dT%H:%M:%S")
481
+ except Exception:
482
+ return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myagent-ai",
3
- "version": "1.23.78",
3
+ "version": "1.23.79",
4
4
  "description": "本地桌面端执行型AI助手 - Open Interpreter 风格 | Local Desktop Execution-Oriented AI Assistant",
5
5
  "main": "main.py",
6
6
  "bin": {
package/scripts/cli.py CHANGED
@@ -1093,28 +1093,83 @@ async def cmd_chat(args):
1093
1093
  print("错误: 必须指定 --message 或 --file", file=sys.stderr)
1094
1094
  sys.exit(1)
1095
1095
 
1096
- # [v1.23.73] Agent 查找:按 ID 匹配,递归搜索所有目录层级
1097
- agents_data_dir = Path.home() / ".myagent" / "data" / "agents"
1098
- resolved_path = None
1096
+ # [v1.23.75] Agent 查找:从数据库按 ID 匹配
1097
+ _config_file = Path.home() / ".myagent" / "config.json"
1098
+ _base_dir = Path.home() / ".myagent"
1099
+ _db_path = Path.home() / ".myagent" / "data" / "agents.db"
1100
+ try:
1101
+ if _config_file.exists():
1102
+ _cfg_raw = json.loads(_config_file.read_text(encoding="utf-8"))
1103
+ _custom_dir = _cfg_raw.get("data_dir", "")
1104
+ if _custom_dir:
1105
+ _expanded = os.path.expanduser(_custom_dir)
1106
+ if not os.path.isabs(_expanded):
1107
+ _expanded = str(Path.home() / _expanded)
1108
+ _base_dir = Path(_expanded)
1109
+ except Exception:
1110
+ pass
1099
1111
 
1100
- if agents_data_dir.exists():
1101
- for cfg_path in agents_data_dir.rglob("config.json"):
1102
- try:
1103
- ag_cfg = json.loads(cfg_path.read_text(encoding="utf-8"))
1104
- if ag_cfg.get("id", "") == a.agent:
1105
- resolved_path = cfg_path.parent.relative_to(agents_data_dir).as_posix()
1112
+ resolved_path = None
1113
+ found_ids = []
1114
+
1115
+ # 优先从数据库查询
1116
+ if _db_path.exists():
1117
+ try:
1118
+ import sqlite3
1119
+ conn = sqlite3.connect(str(_db_path))
1120
+ conn.row_factory = sqlite3.Row
1121
+ rows = conn.execute("SELECT path, id FROM agents WHERE id != ''").fetchall()
1122
+ conn.close()
1123
+ for row in rows:
1124
+ aid = row["id"]
1125
+ found_ids.append(aid)
1126
+ if aid == a.agent:
1127
+ resolved_path = row["path"]
1106
1128
  break
1107
- except Exception:
1108
- pass
1129
+ except Exception:
1130
+ pass
1109
1131
 
1132
+ # 数据库查不到,回退到文件系统(兼容旧版本)
1110
1133
  if not resolved_path:
1111
- print(f"错误: Agent 不存在: {a.agent}(请使用Agent的ID,从群成员列表中获取)", file=sys.stderr)
1134
+ agents_data_dir = _base_dir / "data" / "agents"
1135
+ if agents_data_dir.exists():
1136
+ for cfg_path in agents_data_dir.rglob("config.json"):
1137
+ try:
1138
+ ag_cfg = json.loads(cfg_path.read_text(encoding="utf-8"))
1139
+ aid = ag_cfg.get("id", "")
1140
+ if aid and aid not in found_ids:
1141
+ found_ids.append(aid)
1142
+ if aid == a.agent:
1143
+ resolved_path = cfg_path.parent.relative_to(agents_data_dir).as_posix()
1144
+ break
1145
+ except Exception:
1146
+ pass
1147
+
1148
+ if not resolved_path:
1149
+ err_parts = [f"错误: Agent 不存在: {a.agent}"]
1150
+ if not _db_path.exists() and not (_base_dir / "data" / "agents").exists():
1151
+ err_parts.append(f"agents 数据不存在(数据库和目录均未找到)")
1152
+ else:
1153
+ cfg_count = len(found_ids)
1154
+ err_parts.append(f"共扫描到 {cfg_count} 个 Agent")
1155
+ if cfg_count > 0:
1156
+ sample = found_ids[:5]
1157
+ err_parts.append(f"已有 ID: {', '.join(sample)}")
1158
+ err_parts.append(f"(请使用Agent的ID,从群成员列表中获取)")
1159
+ print(" | ".join(err_parts), file=sys.stderr)
1112
1160
  sys.exit(1)
1113
1161
 
1114
- agent_dir = agents_data_dir / resolved_path
1115
- cfg_file = agent_dir / "config.json"
1116
- cfg = json.loads(cfg_file.read_text(encoding="utf-8"))
1117
- agent_name = cfg.get("name", resolved_path)
1162
+ # 从数据库获取 agent_name
1163
+ agent_name = resolved_path
1164
+ try:
1165
+ conn = sqlite3.connect(str(_db_path))
1166
+ conn.row_factory = sqlite3.Row
1167
+ row = conn.execute("SELECT name FROM agents WHERE path = ?", (resolved_path,)).fetchone()
1168
+ conn.close()
1169
+ if row and row["name"]:
1170
+ agent_name = row["name"]
1171
+ except Exception:
1172
+ pass
1118
1173
 
1119
1174
 
1120
1175
  # 验证文件是否存在