myagent-ai 1.15.18 → 1.15.20
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 +1 -1
- package/skills/search_skill.py +50 -18
- package/web/api_server.py +203 -8
- package/web/ui/chat/chat_main.js +17 -2
- package/web/ui/index.html +108 -3
package/package.json
CHANGED
package/skills/search_skill.py
CHANGED
|
@@ -101,8 +101,13 @@ class WebSearchSkill(Skill):
|
|
|
101
101
|
|
|
102
102
|
def _fetch():
|
|
103
103
|
url = "https://html.duckduckgo.com/html/"
|
|
104
|
-
|
|
105
|
-
|
|
104
|
+
try:
|
|
105
|
+
r = requests.get(url, params={"q": query}, headers=_HEADERS, timeout=15)
|
|
106
|
+
r.raise_for_status()
|
|
107
|
+
except requests.exceptions.SSLError:
|
|
108
|
+
logger.warning("DuckDuckGo SSL 验证失败,跳过证书验证重试")
|
|
109
|
+
r = requests.get(url, params={"q": query}, headers=_HEADERS, timeout=15, verify=False)
|
|
110
|
+
r.raise_for_status()
|
|
106
111
|
return r.text
|
|
107
112
|
|
|
108
113
|
html = await loop.run_in_executor(None, _fetch)
|
|
@@ -151,13 +156,24 @@ class WebSearchSkill(Skill):
|
|
|
151
156
|
|
|
152
157
|
def _fetch():
|
|
153
158
|
url = "https://www.bing.com/search"
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
159
|
+
try:
|
|
160
|
+
r = requests.get(
|
|
161
|
+
url,
|
|
162
|
+
params={"q": query, "count": num},
|
|
163
|
+
headers=_HEADERS,
|
|
164
|
+
timeout=15,
|
|
165
|
+
)
|
|
166
|
+
r.raise_for_status()
|
|
167
|
+
except requests.exceptions.SSLError:
|
|
168
|
+
logger.warning("Bing SSL 验证失败,跳过证书验证重试")
|
|
169
|
+
r = requests.get(
|
|
170
|
+
url,
|
|
171
|
+
params={"q": query, "count": num},
|
|
172
|
+
headers=_HEADERS,
|
|
173
|
+
timeout=15,
|
|
174
|
+
verify=False,
|
|
175
|
+
)
|
|
176
|
+
r.raise_for_status()
|
|
161
177
|
return r.text
|
|
162
178
|
|
|
163
179
|
html = await loop.run_in_executor(None, _fetch)
|
|
@@ -225,8 +241,13 @@ class WebReadSkill(Skill):
|
|
|
225
241
|
loop = asyncio.get_event_loop()
|
|
226
242
|
|
|
227
243
|
def _fetch():
|
|
228
|
-
|
|
229
|
-
|
|
244
|
+
try:
|
|
245
|
+
r = requests.get(url, headers=_HEADERS, timeout=30)
|
|
246
|
+
r.raise_for_status()
|
|
247
|
+
except requests.exceptions.SSLError:
|
|
248
|
+
logger.warning(f"web_read SSL 验证失败 ({url}),跳过证书验证重试")
|
|
249
|
+
r = requests.get(url, headers=_HEADERS, timeout=30, verify=False)
|
|
250
|
+
r.raise_for_status()
|
|
230
251
|
r.encoding = r.apparent_encoding
|
|
231
252
|
return r.text
|
|
232
253
|
|
|
@@ -300,13 +321,24 @@ class URLReadSkill(Skill):
|
|
|
300
321
|
loop = asyncio.get_event_loop()
|
|
301
322
|
|
|
302
323
|
def _request():
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
324
|
+
try:
|
|
325
|
+
r = requests.request(
|
|
326
|
+
method=method,
|
|
327
|
+
url=url,
|
|
328
|
+
headers=headers,
|
|
329
|
+
data=body,
|
|
330
|
+
timeout=30,
|
|
331
|
+
)
|
|
332
|
+
except requests.exceptions.SSLError:
|
|
333
|
+
logger.warning(f"url_read SSL 验证失败 ({url}),跳过证书验证重试")
|
|
334
|
+
r = requests.request(
|
|
335
|
+
method=method,
|
|
336
|
+
url=url,
|
|
337
|
+
headers=headers,
|
|
338
|
+
data=body,
|
|
339
|
+
timeout=30,
|
|
340
|
+
verify=False,
|
|
341
|
+
)
|
|
310
342
|
return r
|
|
311
343
|
|
|
312
344
|
response = await loop.run_in_executor(None, _request)
|
package/web/api_server.py
CHANGED
|
@@ -256,6 +256,8 @@ class ApiServer:
|
|
|
256
256
|
r.add_get("/api/agents/{name:[^/]+}/knowledge", self.handle_list_agent_knowledge)
|
|
257
257
|
r.add_post("/api/agents/{name:[^/]+}/knowledge/upload", self.handle_upload_agent_knowledge)
|
|
258
258
|
r.add_delete("/api/agents/{name:[^/]+}/knowledge", self.handle_delete_agent_knowledge)
|
|
259
|
+
r.add_post("/api/agents/{name:[^/]+}/avatar", self.handle_upload_agent_avatar)
|
|
260
|
+
r.add_get("/api/agents/{name:[^/]+}/avatar.png", self.handle_get_agent_avatar)
|
|
259
261
|
# ── Agent 通用 CRUD(放在子资源之后,name 不含斜杠避免吞掉子路由) ──
|
|
260
262
|
r.add_get("/api/agents/{name:[^/]+}", self.handle_get_agent)
|
|
261
263
|
r.add_put("/api/agents/{name:[^/]+}", self.handle_update_agent)
|
|
@@ -2082,7 +2084,7 @@ class ApiServer:
|
|
|
2082
2084
|
"error": f"系统 Agent '{path}' 的灵魂文件和身份文件受保护,不可修改",
|
|
2083
2085
|
}, status=403)
|
|
2084
2086
|
# 系统 Agent 只允许修改有限字段
|
|
2085
|
-
allowed_fields = ("name", "description", "avatar_color", "avatar_emoji", "model", "system_prompt",
|
|
2087
|
+
allowed_fields = ("name", "description", "avatar_color", "avatar_emoji", "avatar_image", "model", "system_prompt",
|
|
2086
2088
|
"execution_mode", "enabled", "sandbox_image", "sandbox_network", "sandbox_memory",
|
|
2087
2089
|
"platform", "platform_token", "platform_app_id", "platform_app_secret",
|
|
2088
2090
|
"model_id", "backup_model_ids", "work_dir", "department")
|
|
@@ -5018,6 +5020,110 @@ class ApiServer:
|
|
|
5018
5020
|
|
|
5019
5021
|
return web.json_response({"ok": True, "message": "已删除"})
|
|
5020
5022
|
|
|
5023
|
+
async def handle_upload_agent_avatar(self, request):
|
|
5024
|
+
"""POST /api/agents/{name}/avatar - 上传 Agent 头像图片(支持裁剪)
|
|
5025
|
+
|
|
5026
|
+
请求格式: multipart/form-data
|
|
5027
|
+
- file: 图片文件 (jpg/png/webp)
|
|
5028
|
+
- crop_x, crop_y, crop_w, crop_h: 裁剪区域(像素,可选,不传则不裁剪)
|
|
5029
|
+
- size: 输出尺寸(像素,默认128,最大512)
|
|
5030
|
+
|
|
5031
|
+
返回: {"ok": True, "url": "/api/agents/{name}/avatar.png"}
|
|
5032
|
+
"""
|
|
5033
|
+
agent_path = request.match_info["name"]
|
|
5034
|
+
ad = self._agent_dir(agent_path)
|
|
5035
|
+
if not (ad / "config.json").exists():
|
|
5036
|
+
return web.json_response({"error": "Agent 不存在"}, status=404)
|
|
5037
|
+
|
|
5038
|
+
try:
|
|
5039
|
+
reader = await request.multipart()
|
|
5040
|
+
field = await reader.next()
|
|
5041
|
+
|
|
5042
|
+
if not field or field.name != "file":
|
|
5043
|
+
return web.json_response({"error": "缺少 file 字段"}, status=400)
|
|
5044
|
+
|
|
5045
|
+
# 读取上传的图片数据
|
|
5046
|
+
image_data = bytearray()
|
|
5047
|
+
while True:
|
|
5048
|
+
chunk = await field.read_chunk()
|
|
5049
|
+
if not chunk:
|
|
5050
|
+
break
|
|
5051
|
+
image_data.extend(chunk)
|
|
5052
|
+
|
|
5053
|
+
# 解析裁剪参数
|
|
5054
|
+
crop_x = int(request.query.get("crop_x", 0))
|
|
5055
|
+
crop_y = int(request.query.get("crop_y", 0))
|
|
5056
|
+
crop_w = int(request.query.get("crop_w", 0))
|
|
5057
|
+
crop_h = int(request.query.get("crop_h", 0))
|
|
5058
|
+
out_size = min(int(request.query.get("size", 128)), 512)
|
|
5059
|
+
|
|
5060
|
+
# 使用 Pillow 处理图片
|
|
5061
|
+
from PIL import Image
|
|
5062
|
+
import io
|
|
5063
|
+
|
|
5064
|
+
img = Image.open(io.BytesIO(bytes(image_data)))
|
|
5065
|
+
|
|
5066
|
+
# 如果有 Alpha 通道,转为 RGB(JPEG 不支持透明)
|
|
5067
|
+
if img.mode in ("RGBA", "P"):
|
|
5068
|
+
# 创建白色背景
|
|
5069
|
+
bg = Image.new("RGB", img.size, (255, 255, 255))
|
|
5070
|
+
if img.mode == "P":
|
|
5071
|
+
img = img.convert("RGBA")
|
|
5072
|
+
bg.paste(img, mask=img.split()[3] if img.mode == "RGBA" else None)
|
|
5073
|
+
img = bg
|
|
5074
|
+
elif img.mode != "RGB":
|
|
5075
|
+
img = img.convert("RGB")
|
|
5076
|
+
|
|
5077
|
+
# 裁剪
|
|
5078
|
+
if crop_w > 0 and crop_h > 0:
|
|
5079
|
+
img = img.crop((crop_x, crop_y, crop_x + crop_w, crop_y + crop_h))
|
|
5080
|
+
|
|
5081
|
+
# 居中裁剪为正方形(如果还不是正方形)
|
|
5082
|
+
w, h = img.size
|
|
5083
|
+
min_side = min(w, h)
|
|
5084
|
+
left = (w - min_side) // 2
|
|
5085
|
+
top = (h - min_side) // 2
|
|
5086
|
+
img = img.crop((left, top, left + min_side, top + min_side))
|
|
5087
|
+
|
|
5088
|
+
# 缩放到目标尺寸
|
|
5089
|
+
img = img.resize((out_size, out_size), Image.LANCZOS)
|
|
5090
|
+
|
|
5091
|
+
# 保存为 PNG
|
|
5092
|
+
avatar_path = ad / "avatar.png"
|
|
5093
|
+
img.save(str(avatar_path), "PNG", optimize=True)
|
|
5094
|
+
|
|
5095
|
+
# 更新 config.json 中的 avatar_image 字段
|
|
5096
|
+
cfg = self._read_agent_config(agent_path)
|
|
5097
|
+
if cfg:
|
|
5098
|
+
cfg["avatar_image"] = f"/api/agents/{agent_path}/avatar.png"
|
|
5099
|
+
(ad / "config.json").write_text(
|
|
5100
|
+
json.dumps(cfg, indent=2, ensure_ascii=False), encoding="utf-8"
|
|
5101
|
+
)
|
|
5102
|
+
|
|
5103
|
+
# 确保头像文件可被静态访问:注册 avatar 路由
|
|
5104
|
+
avatar_url = f"/api/agents/{agent_path}/avatar.png"
|
|
5105
|
+
|
|
5106
|
+
return web.json_response({
|
|
5107
|
+
"ok": True,
|
|
5108
|
+
"url": avatar_url,
|
|
5109
|
+
"size": out_size,
|
|
5110
|
+
})
|
|
5111
|
+
|
|
5112
|
+
except ImportError:
|
|
5113
|
+
return web.json_response({"error": "需要 Pillow 库: pip install Pillow"}, status=500)
|
|
5114
|
+
except Exception as e:
|
|
5115
|
+
logger.error(f"头像上传失败 ({agent_path}): {e}", exc_info=True)
|
|
5116
|
+
return web.json_response({"error": f"头像上传失败: {e}"}, status=500)
|
|
5117
|
+
|
|
5118
|
+
async def handle_get_agent_avatar(self, request):
|
|
5119
|
+
"""GET /api/agents/{name}/avatar.png - 获取 Agent 头像图片"""
|
|
5120
|
+
agent_path = request.match_info["name"]
|
|
5121
|
+
ad = self._agent_dir(agent_path)
|
|
5122
|
+
avatar_file = ad / "avatar.png"
|
|
5123
|
+
if not avatar_file.exists():
|
|
5124
|
+
return web.json_response({"error": "头像不存在"}, status=404)
|
|
5125
|
+
return web.FileResponse(str(avatar_file))
|
|
5126
|
+
|
|
5021
5127
|
# ── 知识库 RAG 搜索 ──
|
|
5022
5128
|
|
|
5023
5129
|
async def handle_knowledge_search(self, request):
|
|
@@ -5253,14 +5359,62 @@ class ApiServer:
|
|
|
5253
5359
|
model_chain = self._build_model_chain(agent_cfg, agent_path)
|
|
5254
5360
|
session_id = f"group_{gid}_{agent_path}"
|
|
5255
5361
|
|
|
5362
|
+
# [v1.15.18] 构建 Agent 专属系统提示词(与1:1聊天一致)
|
|
5363
|
+
_, agent_system_prompt = self._build_agent_chat_context(agent_path, agent_cfg, content)
|
|
5364
|
+
|
|
5365
|
+
# [v1.15.18] 构建群聊上下文:群信息 + 成员列表 + 发言者身份
|
|
5366
|
+
member_lines = []
|
|
5367
|
+
for m in group.members:
|
|
5368
|
+
mc = self._read_agent_config(m.agent_path)
|
|
5369
|
+
m_name = mc.get("name", m.agent_path) if mc else m.agent_path
|
|
5370
|
+
m_desc = mc.get("description", "") if mc else ""
|
|
5371
|
+
role_label = {"owner": "群主", "admin": "管理员"}.get(m.role, "成员")
|
|
5372
|
+
nick = f"(昵称: {m.nickname})" if m.nickname else ""
|
|
5373
|
+
line = f" - {m_name} [{m.agent_path}] ({role_label})"
|
|
5374
|
+
if m_desc:
|
|
5375
|
+
line += f" — {m_desc}"
|
|
5376
|
+
line += nick
|
|
5377
|
+
if m.muted:
|
|
5378
|
+
line += "(已禁言)"
|
|
5379
|
+
member_lines.append(line)
|
|
5380
|
+
|
|
5381
|
+
my_name = agent_cfg.get("name", agent_path) if agent_cfg else agent_path
|
|
5382
|
+
my_role = {"owner": "群主", "admin": "管理员"}.get(member.role, "成员")
|
|
5383
|
+
my_desc = agent_cfg.get("description", "") if agent_cfg else ""
|
|
5384
|
+
|
|
5385
|
+
group_context = (
|
|
5386
|
+
f"## 群聊上下文\n"
|
|
5387
|
+
f"- 群名称: {group.name}\n"
|
|
5388
|
+
f"- 群ID: {group.id}\n"
|
|
5389
|
+
f"- 群描述: {group.description}\n"
|
|
5390
|
+
f"- 当前发言者: 用户\n"
|
|
5391
|
+
f"- 你的身份: {my_name} ({my_role})"
|
|
5392
|
+
+ (f" — {my_desc}" if my_desc else "")
|
|
5393
|
+
+ f"\n- 群成员 ({len(group.members)}人):\n"
|
|
5394
|
+
+ "\n".join(member_lines)
|
|
5395
|
+
+ "\n\n注意:你只代表自己发言,回复时使用第一人称。"
|
|
5396
|
+
"如果消息不是跟你相关的,可以简短回复或不回复。"
|
|
5397
|
+
)
|
|
5398
|
+
|
|
5399
|
+
# 将群聊上下文追加到 agent_system_prompt
|
|
5400
|
+
if agent_system_prompt:
|
|
5401
|
+
agent_system_prompt += "\n\n" + group_context
|
|
5402
|
+
else:
|
|
5403
|
+
agent_system_prompt = group_context
|
|
5404
|
+
|
|
5256
5405
|
# 构建部门上下文(如果此群属于某个部门)
|
|
5257
|
-
agent_content = content
|
|
5258
5406
|
dept_context = self._build_dept_context(gid, agent_path)
|
|
5259
5407
|
if dept_context:
|
|
5260
|
-
|
|
5408
|
+
agent_system_prompt += "\n\n" + dept_context
|
|
5409
|
+
|
|
5410
|
+
# 构建最终消息(用户原文不包含任何注入内容)
|
|
5411
|
+
agent_content = content
|
|
5261
5412
|
|
|
5262
5413
|
if model_chain and self.core.llm:
|
|
5263
|
-
response = await self._try_model_chain(
|
|
5414
|
+
response = await self._try_model_chain(
|
|
5415
|
+
model_chain, agent_content, session_id,
|
|
5416
|
+
agent_path=agent_path, agent_system_prompt=agent_system_prompt,
|
|
5417
|
+
)
|
|
5264
5418
|
else:
|
|
5265
5419
|
response = await self.core.process_message(agent_content, session_id)
|
|
5266
5420
|
|
|
@@ -5446,14 +5600,55 @@ class ApiServer:
|
|
|
5446
5600
|
model_chain = self._build_model_chain(agent_cfg, agent_path)
|
|
5447
5601
|
session_id = f"group_{group_id}_{agent_path}"
|
|
5448
5602
|
|
|
5449
|
-
#
|
|
5450
|
-
|
|
5603
|
+
# [v1.15.18] 构建 Agent 专属系统提示词 + 群聊上下文(与发送群消息一致)
|
|
5604
|
+
_, agent_system_prompt = self._build_agent_chat_context(agent_path, agent_cfg, description)
|
|
5605
|
+
|
|
5606
|
+
# 构建群聊上下文
|
|
5607
|
+
member_lines = []
|
|
5608
|
+
for m in group.members:
|
|
5609
|
+
mc = self._read_agent_config(m.agent_path)
|
|
5610
|
+
m_name = mc.get("name", m.agent_path) if mc else m.agent_path
|
|
5611
|
+
m_desc = mc.get("description", "") if mc else ""
|
|
5612
|
+
role_label = {"owner": "群主", "admin": "管理员"}.get(m.role, "成员")
|
|
5613
|
+
line = f" - {m_name} [{m.agent_path}] ({role_label})"
|
|
5614
|
+
if m_desc:
|
|
5615
|
+
line += f" — {m_desc}"
|
|
5616
|
+
member_lines.append(line)
|
|
5617
|
+
|
|
5618
|
+
my_name = agent_cfg.get("name", agent_path) if agent_cfg else agent_path
|
|
5619
|
+
my_role = {"owner": "群主", "admin": "管理员"}.get(member.role, "成员")
|
|
5620
|
+
my_desc = agent_cfg.get("description", "") if agent_cfg else ""
|
|
5621
|
+
|
|
5622
|
+
group_context = (
|
|
5623
|
+
f"## 群聊上下文\n"
|
|
5624
|
+
f"- 群名称: {group.name}\n"
|
|
5625
|
+
f"- 群ID: {group.id}\n"
|
|
5626
|
+
f"- 群描述: {group.description}\n"
|
|
5627
|
+
f"- 当前发言者: 用户(重试任务)\n"
|
|
5628
|
+
f"- 你的身份: {my_name} ({my_role})"
|
|
5629
|
+
+ (f" — {my_desc}" if my_desc else "")
|
|
5630
|
+
+ f"\n- 群成员 ({len(group.members)}人):\n"
|
|
5631
|
+
+ "\n".join(member_lines)
|
|
5632
|
+
+ "\n\n注意:你只代表自己发言,回复时使用第一人称。"
|
|
5633
|
+
"如果消息不是跟你相关的,可以简短回复或不回复。"
|
|
5634
|
+
)
|
|
5635
|
+
|
|
5636
|
+
if agent_system_prompt:
|
|
5637
|
+
agent_system_prompt += "\n\n" + group_context
|
|
5638
|
+
else:
|
|
5639
|
+
agent_system_prompt = group_context
|
|
5640
|
+
|
|
5451
5641
|
dept_context = self._build_dept_context(group_id, agent_path)
|
|
5452
5642
|
if dept_context:
|
|
5453
|
-
|
|
5643
|
+
agent_system_prompt += "\n\n" + dept_context
|
|
5644
|
+
|
|
5645
|
+
agent_content = description
|
|
5454
5646
|
|
|
5455
5647
|
if model_chain and self.core.llm:
|
|
5456
|
-
response = await self._try_model_chain(
|
|
5648
|
+
response = await self._try_model_chain(
|
|
5649
|
+
model_chain, agent_content, session_id,
|
|
5650
|
+
agent_path=agent_path, agent_system_prompt=agent_system_prompt,
|
|
5651
|
+
)
|
|
5457
5652
|
else:
|
|
5458
5653
|
response = await self.core.process_message(agent_content, session_id)
|
|
5459
5654
|
|
package/web/ui/chat/chat_main.js
CHANGED
|
@@ -249,6 +249,20 @@ function getAgentGradient(name) {
|
|
|
249
249
|
return `linear-gradient(135deg,${palettes[idx][0]},${palettes[idx][1]})`;
|
|
250
250
|
}
|
|
251
251
|
|
|
252
|
+
/** 渲染 Agent 头像 HTML — 支持 avatar_image(图片)和 avatar_emoji(emoji)双模式 */
|
|
253
|
+
function avatarHtml(agent, size, extraStyle) {
|
|
254
|
+
if (!agent) return '';
|
|
255
|
+
const s = size || 36;
|
|
256
|
+
const style = (extraStyle || '') + 'width:' + s + 'px;height:' + s + 'px;border-radius:50%;object-fit:cover;display:block';
|
|
257
|
+
if (agent.avatar_image) {
|
|
258
|
+
return '<img src="' + escapeHtml(agent.avatar_image) + '" style="' + style + '" onerror="this.style.display=\'none\'">';
|
|
259
|
+
}
|
|
260
|
+
const emoji = agent.avatar_emoji || agent.emoji || getAgentInitials(agent.name);
|
|
261
|
+
const bg = agent.avatar_color ? agent.avatar_color + '22' : 'transparent';
|
|
262
|
+
const border = agent.avatar_color ? '2px solid ' + agent.avatar_color : 'none';
|
|
263
|
+
return '<div style="' + style + ';background:' + bg + ';border:' + border + ';display:flex;align-items:center;justify-content:center;font-size:' + Math.round(s * 0.55) + 'px">' + escapeHtml(emoji) + '</div>';
|
|
264
|
+
}
|
|
265
|
+
|
|
252
266
|
// ── State Persistence (localStorage) ──
|
|
253
267
|
const StatePersistence = {
|
|
254
268
|
PREFIX: 'myagent_',
|
|
@@ -988,9 +1002,10 @@ function renderMasterAgentCard() {
|
|
|
988
1002
|
var color = agent ? (agent.avatar_color || 'linear-gradient(135deg,#6366f1,#8b5cf6)') : 'linear-gradient(135deg,#6366f1,#8b5cf6)';
|
|
989
1003
|
var bgStyle = color.includes('gradient') ? 'background:' + color : '';
|
|
990
1004
|
var bgClass = color.includes('gradient') ? '' : getAgentColorClass('default');
|
|
1005
|
+
var avatarContent = (agent && agent.avatar_image) ? '<img src="' + escapeHtml(agent.avatar_image) + '" style="width:100%;height:100%;object-fit:cover;border-radius:12px">' : emoji;
|
|
991
1006
|
|
|
992
1007
|
el.innerHTML = '<div class="rp-master-card ' + (isActive ? 'active' : '') + '" onclick="selectAgent(\'default\')">'
|
|
993
|
-
+ '<div class="rp-master-avatar ' + bgClass + '" style="' + bgStyle + '">' +
|
|
1008
|
+
+ '<div class="rp-master-avatar ' + bgClass + '" style="' + bgStyle + '">' + avatarContent + '</div>'
|
|
994
1009
|
+ '<div class="rp-master-info">'
|
|
995
1010
|
+ '<div class="rp-master-name">' + escapeHtml(name) + '</div>'
|
|
996
1011
|
+ '<div class="rp-master-desc">' + escapeHtml(desc) + '</div>'
|
|
@@ -2456,7 +2471,7 @@ function _renderMessagesInner() {
|
|
|
2456
2471
|
// Skip standalone tool messages (now grouped into assistant parts via groupHistoryMessages)
|
|
2457
2472
|
if (msg.role === 'tool') continue;
|
|
2458
2473
|
|
|
2459
|
-
const avatar = isUser ? '
|
|
2474
|
+
const avatar = isUser ? '<span style="font-size:18px">👤</span>' : avatarHtml({avatar_image: state.currentAgent?.avatar_image, avatar_emoji: botEmoji, avatar_color: state.currentAgent?.avatar_color, name: state.currentAgent?.name}, 32, 'border-radius:8px;');
|
|
2460
2475
|
const content = renderMarkdown(msg.content);
|
|
2461
2476
|
const thoughtHtml = msg.thought ? (() => {
|
|
2462
2477
|
const isStreaming = !!msg.streaming;
|
package/web/ui/index.html
CHANGED
|
@@ -378,9 +378,10 @@ function agentCardHtml(a,deptMap){
|
|
|
378
378
|
const dept=deptMap?.[a.department]||null;
|
|
379
379
|
const emoji=a.avatar_emoji||'🤖';
|
|
380
380
|
const color=a.avatar_color||'#6366f1';
|
|
381
|
+
const avatarHtml=a.avatar_image?'<img src="'+escHtml(a.avatar_image)+'" style="width:100%;height:100%;object-fit:cover">':emoji;
|
|
381
382
|
const enabled=a.enabled!==false;
|
|
382
383
|
return `<div class="agent-card" data-name="${escHtml(a.name||'')}" data-desc="${escHtml(a.description||'')}">
|
|
383
|
-
<div class="avatar" style="background:${color}22;border:2px solid ${color}">${
|
|
384
|
+
<div class="avatar" style="background:${color}22;border:2px solid ${color};overflow:hidden">${avatarHtml}</div>
|
|
384
385
|
<div class="info">
|
|
385
386
|
<h4>${escHtml(a.name||a.path)} ${isSys?'<span class="badge badge-purple">系统</span>':''} ${!enabled?'<span class="badge badge-red">已禁用</span>':''}</h4>
|
|
386
387
|
<p>${escHtml(a.description||'无描述')}</p>
|
|
@@ -424,7 +425,30 @@ function openCreateAgentModal(){
|
|
|
424
425
|
<div class="form-group"><label>描述</label><input id="caDesc" placeholder="Agent 描述"></div>
|
|
425
426
|
</div>
|
|
426
427
|
<div class="form-row">
|
|
427
|
-
<div class="form-group"><label
|
|
428
|
+
<div class="form-group"><label>头像</label>
|
|
429
|
+
<div class="flex gap-8 items-center" style="flex-wrap:wrap">
|
|
430
|
+
<div id="caAvatarPreview" class="avatar" style="width:48px;height:48px;background:#6366f122;border:2px solid #6366f1;font-size:24px">🤖</div>
|
|
431
|
+
<div style="flex:1;min-width:200px">
|
|
432
|
+
<div class="flex gap-8 items-center">
|
|
433
|
+
<input id="caEmoji" placeholder="🤖" maxlength="4" style="width:60px" oninput="$('caAvatarPreview').textContent=this.value||'🤖'">
|
|
434
|
+
<label class="btn btn-sm" style="cursor:pointer"><input type="file" accept="image/*" hidden onchange="handleAvatarUpload(this,'ca')">
|
|
435
|
+
📷 上传图片
|
|
436
|
+
</label>
|
|
437
|
+
</div>
|
|
438
|
+
<div id="caCropArea" style="display:none;margin-top:8px">
|
|
439
|
+
<div style="position:relative;display:inline-block">
|
|
440
|
+
<img id="caCropImg" style="max-width:300px;max-height:200px;border:1px solid #ddd;cursor:crosshair" onmousedown="startCrop(event,'ca')">
|
|
441
|
+
<div id="caCropOverlay" style="position:absolute;border:2px dashed #4f46e5;background:rgba(79,70,229,0.15);display:none;pointer-events:none"></div>
|
|
442
|
+
</div>
|
|
443
|
+
<div class="flex gap-8 mt-8">
|
|
444
|
+
<button class="btn btn-sm btn-primary" onclick="confirmAvatarCrop('ca','${name||''}')">✂️ 裁剪并使用</button>
|
|
445
|
+
<button class="btn btn-sm btn-ghost" onclick="cancelAvatarCrop('ca')">取消</button>
|
|
446
|
+
</div>
|
|
447
|
+
</div>
|
|
448
|
+
<input type="hidden" id="caAvatarImage" value="">
|
|
449
|
+
</div>
|
|
450
|
+
</div>
|
|
451
|
+
</div>
|
|
428
452
|
<div class="form-group"><label>头像颜色</label><div class="flex gap-8 items-center"><input id="caColor" value="#6366f1" type="color" style="width:48px;height:34px;padding:2px"><input id="caColorText" value="#6366f1" style="flex:1" oninput="$('caColor').value=this.value"></div></div>
|
|
429
453
|
</div>
|
|
430
454
|
<div class="form-row">
|
|
@@ -484,7 +508,31 @@ async function openEditAgentModal(path){
|
|
|
484
508
|
<div class="form-group"><label>描述</label><input id="eaDesc" value="${escHtml(a.description||'')}" ${isSys?'disabled':''}></div>
|
|
485
509
|
</div>
|
|
486
510
|
<div class="form-row">
|
|
487
|
-
<div class="form-group"><label
|
|
511
|
+
<div class="form-group"><label>头像</label>
|
|
512
|
+
<div class="flex gap-8 items-center" style="flex-wrap:wrap">
|
|
513
|
+
<div id="eaAvatarPreview" class="avatar" style="width:48px;height:48px;background:${escHtml(a.avatar_color||'#6366f1')}22;border:2px solid ${escHtml(a.avatar_color||'#6366f1')};font-size:24px;overflow:hidden">${a.avatar_image?'<img src="'+escHtml(a.avatar_image)+'" style="width:100%;height:100%;object-fit:cover">':escHtml(a.avatar_emoji||'🤖')}</div>
|
|
514
|
+
<div style="flex:1;min-width:200px">
|
|
515
|
+
<div class="flex gap-8 items-center">
|
|
516
|
+
<input id="eaEmoji" value="${escHtml(a.avatar_emoji||'')}" style="width:60px" oninput="if(!$('eaAvatarImage').value){$('eaAvatarPreview').textContent=this.value||'🤖';$('eaAvatarPreview').innerHTML=''}" ${isSys?'disabled':''}>
|
|
517
|
+
<label class="btn btn-sm" style="cursor:pointer" ${isSys?'style=\"pointer-events:none;opacity:0.5\"':''}><input type="file" accept="image/*" hidden onchange="handleAvatarUpload(this,'ea')" ${isSys?'disabled':''}>
|
|
518
|
+
📷 上传图片
|
|
519
|
+
</label>
|
|
520
|
+
${a.avatar_image?'<button class="btn btn-sm btn-ghost" onclick="removeAvatarImage(\\''+escHtml(path)+'\\')">🗑️ 移除图片</button>':''}
|
|
521
|
+
</div>
|
|
522
|
+
<div id="eaCropArea" style="display:none;margin-top:8px">
|
|
523
|
+
<div style="position:relative;display:inline-block">
|
|
524
|
+
<img id="eaCropImg" style="max-width:300px;max-height:200px;border:1px solid #ddd;cursor:crosshair" onmousedown="startCrop(event,'ea')">
|
|
525
|
+
<div id="eaCropOverlay" style="position:absolute;border:2px dashed #4f46e5;background:rgba(79,70,229,0.15);display:none;pointer-events:none"></div>
|
|
526
|
+
</div>
|
|
527
|
+
<div class="flex gap-8 mt-8">
|
|
528
|
+
<button class="btn btn-sm btn-primary" onclick="confirmAvatarCrop('ea','${escHtml(path)}')">✂️ 裁剪并使用</button>
|
|
529
|
+
<button class="btn btn-sm btn-ghost" onclick="cancelAvatarCrop('ea')">取消</button>
|
|
530
|
+
</div>
|
|
531
|
+
</div>
|
|
532
|
+
<input type="hidden" id="eaAvatarImage" value="${escHtml(a.avatar_image||'')}">
|
|
533
|
+
</div>
|
|
534
|
+
</div>
|
|
535
|
+
</div>
|
|
488
536
|
<div class="form-group"><label>头像颜色</label><div class="flex gap-8 items-center"><input id="eaColor" value="${escHtml(a.avatar_color||'#6366f1')}" type="color" style="width:48px;height:34px;padding:2px" ${isSys?'disabled':''}><input id="eaColorText" value="${escHtml(a.avatar_color||'#6366f1')}" style="flex:1" oninput="$('eaColor').value=this.value" ${isSys?'disabled':''}></div></div>
|
|
489
537
|
</div>
|
|
490
538
|
<div class="form-group"><label>绑定模型</label><select id="eaModelId"><option value="">使用全局默认</option>${modelOpts}</select></div>
|
|
@@ -1897,6 +1945,63 @@ async function sysLoadPreview(){
|
|
|
1897
1945
|
if(pages[page])showPage(page);else showPage('dashboard');
|
|
1898
1946
|
})();
|
|
1899
1947
|
setInterval(()=>{api('/api/status').catch(()=>{})},30000);
|
|
1948
|
+
|
|
1949
|
+
/* ── 头像上传 + 裁剪 ── */
|
|
1950
|
+
var _cropState={prefix:'',dragging:false,sx:0,sy:0};
|
|
1951
|
+
function handleAvatarUpload(input,prefix){
|
|
1952
|
+
var file=input.files[0];if(!file)return;
|
|
1953
|
+
if(file.size>5*1024*1024){showToast('图片不能超过 5MB','danger');return}
|
|
1954
|
+
var reader=new FileReader();
|
|
1955
|
+
reader.onload=function(e){
|
|
1956
|
+
var img=$(prefix+'CropImg');img.src=e.target.result;
|
|
1957
|
+
$(prefix+'CropArea').style.display='block';
|
|
1958
|
+
$(prefix+'CropOverlay').style.display='none';
|
|
1959
|
+
_cropState.prefix=prefix;
|
|
1960
|
+
};
|
|
1961
|
+
reader.readAsDataURL(file);
|
|
1962
|
+
}
|
|
1963
|
+
function startCrop(e,prefix){
|
|
1964
|
+
_cropState={prefix:prefix,dragging:true,sx:e.offsetX,sy:e.offsetY};
|
|
1965
|
+
var overlay=$(prefix+'CropOverlay');
|
|
1966
|
+
overlay.style.display='block';
|
|
1967
|
+
overlay.style.left=e.offsetX+'px';overlay.style.top=e.offsetY+'px';
|
|
1968
|
+
overlay.style.width='0';overlay.style.height='0';
|
|
1969
|
+
document.addEventListener('mousemove',doCrop);document.addEventListener('mouseup',endCrop);
|
|
1970
|
+
}
|
|
1971
|
+
function doCrop(e){
|
|
1972
|
+
if(!_cropState.dragging)return;
|
|
1973
|
+
var img=$(_cropState.prefix+'CropImg'),rect=img.getBoundingClientRect();
|
|
1974
|
+
var overlay=$(_cropState.prefix+'CropOverlay');
|
|
1975
|
+
var x1=Math.min(_cropState.sx,e.clientX-rect.left),y1=Math.min(_cropState.sy,e.clientY-rect.top);
|
|
1976
|
+
var x2=Math.max(_cropState.sx,e.clientX-rect.left),y2=Math.max(_cropState.sy,e.clientY-rect.top);
|
|
1977
|
+
x2=Math.min(x2,rect.width);y2=Math.min(y2,rect.height);
|
|
1978
|
+
var w=x2-x1,h=y2-y1;if(w<5||h<5)return;
|
|
1979
|
+
overlay.style.left=x1+'px';overlay.style.top=y1+'px';overlay.style.width=w+'px';overlay.style.height=h+'px';
|
|
1980
|
+
}
|
|
1981
|
+
function endCrop(){_cropState.dragging=false;document.removeEventListener('mousemove',doCrop);document.removeEventListener('mouseup',endCrop);}
|
|
1982
|
+
function cancelAvatarCrop(prefix){$(prefix+'CropArea').style.display='none';}
|
|
1983
|
+
function confirmAvatarCrop(prefix,agentPath){
|
|
1984
|
+
var img=$(prefix+'CropImg'),overlay=$(prefix+'CropOverlay');
|
|
1985
|
+
var scale=img.naturalWidth/img.clientWidth;
|
|
1986
|
+
var cx=Math.round(parseFloat(overlay.style.left)*scale),cy=Math.round(parseFloat(overlay.style.top)*scale);
|
|
1987
|
+
var cw=Math.round(parseFloat(overlay.style.width)*scale),ch=Math.round(parseFloat(overlay.style.height)*scale);
|
|
1988
|
+
if(cw<10||ch<10){showToast('请拖动选择裁剪区域','danger');return}
|
|
1989
|
+
var formData=new FormData();
|
|
1990
|
+
formData.append('file',$(prefix+'CropImg').src.split(',')[1]?dataURItoBlob($(prefix+'CropImg').src):'');
|
|
1991
|
+
showToast('正在上传裁剪...','info');
|
|
1992
|
+
fetch('/api/agents/'+encodeURIComponent(agentPath)+'/avatar?crop_x='+cx+'&crop_y='+cy+'&crop_w='+cw+'&crop_h='+ch+'&size=128',{
|
|
1993
|
+
method:'POST',body:formData
|
|
1994
|
+
}).then(r=>r.json()).then(d=>{
|
|
1995
|
+
if(d.ok){$(prefix+'AvatarImage').value=d.url;$(prefix+'AvatarPreview').innerHTML='<img src="'+d.url+'" style="width:100%;height:100%;object-fit:cover">';$(prefix+'CropArea').style.display='none';showToast('头像已更新','success');}
|
|
1996
|
+
else showToast(d.error||'上传失败','danger');
|
|
1997
|
+
}).catch(e=>showToast('上传失败: '+e,'danger'));
|
|
1998
|
+
}
|
|
1999
|
+
function dataURItoBlob(dataURI){var b=atob(dataURI.split(',')[1]),a=new Uint8Array(b.length);for(var i=0;i<b.length;i++)a[i]=b.charCodeAt(i);return new Blob([a],{type:'image/png'});}
|
|
2000
|
+
async function removeAvatarImage(agentPath){
|
|
2001
|
+
var ad=await api('/api/agents/'+encodeURIComponent(agentPath));if(!ad||!ad.avatar_image)return;
|
|
2002
|
+
await api('/api/agents/'+encodeURIComponent(agentPath),{method:'PUT',body:JSON.stringify({avatar_image:''})});
|
|
2003
|
+
$('eaAvatarImage').value='';$('eaAvatarPreview').innerHTML=escHtml(ad.avatar_emoji||'🤖');showToast('已移除头像图片','success');
|
|
2004
|
+
}
|
|
1900
2005
|
</script>
|
|
1901
2006
|
</body>
|
|
1902
2007
|
</html>
|