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.
- package/core/agent_storage.py +482 -0
- package/package.json +1 -1
- package/scripts/cli.py +71 -16
- package/web/api_server.py +258 -346
- package/web/ui/admin/admin-agentchat.js +4 -2
- package/web/ui/chat/chat.js +1 -1
- package/web/ui/chat/chat_main.js +10 -5
- package/web/ui/chat/groupchat.js +70 -1
|
@@ -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
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.
|
|
1097
|
-
|
|
1098
|
-
|
|
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
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
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
|
-
|
|
1108
|
-
|
|
1129
|
+
except Exception:
|
|
1130
|
+
pass
|
|
1109
1131
|
|
|
1132
|
+
# 数据库查不到,回退到文件系统(兼容旧版本)
|
|
1110
1133
|
if not resolved_path:
|
|
1111
|
-
|
|
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
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
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
|
# 验证文件是否存在
|