myagent-ai 1.10.0 → 1.10.1
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/agents/main_agent.py +66 -4
- package/core/context_builder.py +131 -16
- package/departments/manager.py +60 -5
- package/groups/manager.py +5 -4
- package/package.json +1 -1
- package/web/__pycache__/api_server.cpython-312.pyc +0 -0
- package/web/api_server.py +140 -60
- package/web/ui/chat/chat.css +9 -0
- package/web/ui/chat/chat_main.js +119 -30
- package/web/ui/chat/flow_engine.js +153 -73
- package/web/ui/chat/groupchat.js +7 -4
- package/web/ui/index.html +75 -14
package/agents/main_agent.py
CHANGED
|
@@ -323,12 +323,29 @@ class MainAgent(BaseAgent):
|
|
|
323
323
|
|
|
324
324
|
conversation_history = list(context.conversation_history or [])
|
|
325
325
|
|
|
326
|
+
# 从 DB 加载历史对话(如果 conversation_history 为空且 memory 可用)
|
|
327
|
+
if self.memory and not conversation_history:
|
|
328
|
+
try:
|
|
329
|
+
db_history = self.memory.get_conversation(
|
|
330
|
+
session_id=context.session_id,
|
|
331
|
+
limit=100,
|
|
332
|
+
)
|
|
333
|
+
if db_history:
|
|
334
|
+
conversation_history = [
|
|
335
|
+
Message(role=entry.role, content=entry.content)
|
|
336
|
+
for entry in db_history
|
|
337
|
+
]
|
|
338
|
+
logger.info(f"[{task_id}] 从 DB 加载了 {len(conversation_history)} 条历史对话")
|
|
339
|
+
except Exception as e:
|
|
340
|
+
logger.warning(f"[{task_id}] 加载历史对话失败: {e}")
|
|
341
|
+
|
|
326
342
|
# 保存用户消息到短期记忆
|
|
327
343
|
if self.memory:
|
|
328
344
|
self.memory.add_short_term(
|
|
329
345
|
session_id=context.session_id,
|
|
330
346
|
role="user",
|
|
331
347
|
content=context.user_message,
|
|
348
|
+
key="user_input",
|
|
332
349
|
)
|
|
333
350
|
|
|
334
351
|
# 加载相关记忆 (recall from previous round or initial load)
|
|
@@ -391,7 +408,6 @@ class MainAgent(BaseAgent):
|
|
|
391
408
|
+ self.SYSTEM_PROMPT.split("\n", 1)[1]
|
|
392
409
|
)
|
|
393
410
|
system_content = _prompt_with_placeholder.replace(_CONTEXT_PLACEHOLDER, context_xml)
|
|
394
|
-
print(system_content)
|
|
395
411
|
# Step 3: 调用 LLM
|
|
396
412
|
messages = [Message(role="system", content=system_content)]
|
|
397
413
|
|
|
@@ -420,6 +436,16 @@ class MainAgent(BaseAgent):
|
|
|
420
436
|
llm_raw = response.content
|
|
421
437
|
logger.debug(f"[{task_id}] LLM 输出 (前500字): {llm_raw[:500]}")
|
|
422
438
|
|
|
439
|
+
# 保存 LLM 原始输出到短期记忆(用于回溯和审计)
|
|
440
|
+
if self.memory:
|
|
441
|
+
self.memory.add_short_term(
|
|
442
|
+
session_id=context.session_id,
|
|
443
|
+
role="assistant",
|
|
444
|
+
content=llm_raw,
|
|
445
|
+
key="llm_output",
|
|
446
|
+
importance=0.3,
|
|
447
|
+
)
|
|
448
|
+
|
|
423
449
|
# Step 4: 解析结构化输出
|
|
424
450
|
parsed = parse_output(llm_raw)
|
|
425
451
|
|
|
@@ -667,11 +693,30 @@ class MainAgent(BaseAgent):
|
|
|
667
693
|
)
|
|
668
694
|
|
|
669
695
|
# 发送工具结果事件
|
|
696
|
+
# 提取实际输出:SkillResult 有 output/message/data,ExecResult 有 stdout/stderr
|
|
697
|
+
def _extract_tool_output(tr):
|
|
698
|
+
"""从工具结果中提取实际输出文本"""
|
|
699
|
+
out = tr.get("output", "")
|
|
700
|
+
if out:
|
|
701
|
+
return out
|
|
702
|
+
out = tr.get("message", "")
|
|
703
|
+
if out:
|
|
704
|
+
return out
|
|
705
|
+
out = tr.get("stdout", "")
|
|
706
|
+
if out:
|
|
707
|
+
return out
|
|
708
|
+
data = tr.get("data")
|
|
709
|
+
if data is not None:
|
|
710
|
+
return str(data) if not isinstance(data, str) else data
|
|
711
|
+
return tr.get("error", "")
|
|
712
|
+
|
|
713
|
+
tool_output_text = _extract_tool_output(tool_result)
|
|
714
|
+
|
|
670
715
|
await self._emit_v2_event(
|
|
671
716
|
"v2_tool_result",
|
|
672
717
|
{"tool": {"toolname": tool_name}, "result": {
|
|
673
718
|
"success": tool_result.get("success", False),
|
|
674
|
-
"output": truncate_str(
|
|
719
|
+
"output": truncate_str(tool_output_text, 3000),
|
|
675
720
|
"error": truncate_str(tool_result.get("error", ""), 1000),
|
|
676
721
|
"timed_out": tool_result.get("timed_out", False),
|
|
677
722
|
}},
|
|
@@ -682,7 +727,7 @@ class MainAgent(BaseAgent):
|
|
|
682
727
|
"title": f"工具结果: {tool_name}",
|
|
683
728
|
"tool_name": tool_name,
|
|
684
729
|
"success": tool_result.get("success", False),
|
|
685
|
-
"summary": truncate_str(
|
|
730
|
+
"summary": truncate_str(tool_output_text, 500),
|
|
686
731
|
"result": tool_result,
|
|
687
732
|
})
|
|
688
733
|
|
|
@@ -693,7 +738,7 @@ class MainAgent(BaseAgent):
|
|
|
693
738
|
elif should_callback:
|
|
694
739
|
need_callback = True
|
|
695
740
|
|
|
696
|
-
output_str =
|
|
741
|
+
output_str = tool_output_text
|
|
697
742
|
tool_outputs_parts.append(
|
|
698
743
|
f"### {before_call}\n"
|
|
699
744
|
f"**工具**: {tool_name}\n"
|
|
@@ -710,6 +755,23 @@ class MainAgent(BaseAgent):
|
|
|
710
755
|
content=f"[工具 {tool_name} 执行完成] {'成功' if tool_result.get('success') else '失败'}",
|
|
711
756
|
))
|
|
712
757
|
|
|
758
|
+
# 保存工具调用到短期记忆
|
|
759
|
+
if self.memory:
|
|
760
|
+
self.memory.add_short_term(
|
|
761
|
+
session_id=context.session_id,
|
|
762
|
+
role="assistant",
|
|
763
|
+
content=f"调用工具: {tool_name}\n参数: {truncate_str(parms, 1000)}",
|
|
764
|
+
key="tool_call",
|
|
765
|
+
importance=0.4,
|
|
766
|
+
)
|
|
767
|
+
self.memory.add_short_term(
|
|
768
|
+
session_id=context.session_id,
|
|
769
|
+
role="tool",
|
|
770
|
+
content=f"[{tool_name}] {'成功' if tool_result.get('success') else '失败'}\n{truncate_str(output_str, 5000)}",
|
|
771
|
+
key="tool_result",
|
|
772
|
+
importance=0.4,
|
|
773
|
+
)
|
|
774
|
+
|
|
713
775
|
all_tool_outputs = "\n".join(tool_outputs_parts)
|
|
714
776
|
|
|
715
777
|
# Step 12: 工具执行完毕后,根据 finish 标志决定是否回调 LLM
|
package/core/context_builder.py
CHANGED
|
@@ -118,10 +118,11 @@ class ContextBuilder:
|
|
|
118
118
|
kb_query = get_knowledge.strip() if get_knowledge else query
|
|
119
119
|
|
|
120
120
|
sections: List[str] = [
|
|
121
|
+
self._build_datetime(),
|
|
121
122
|
self._build_whomi(agent_name, agent_description, agent_override_prompt),
|
|
122
123
|
self._build_memory(query, session_id),
|
|
123
124
|
self._build_knowledge(kb_query),
|
|
124
|
-
self._build_recent_dialog(conversation_history, self.max_dialog_chars),
|
|
125
|
+
self._build_recent_dialog(conversation_history, self.max_dialog_chars, session_id),
|
|
125
126
|
self._build_user_input(user_typed_text, user_voice_text),
|
|
126
127
|
self._build_task_plan(task_plan),
|
|
127
128
|
self._build_tools(self.skill_registry),
|
|
@@ -140,6 +141,24 @@ class ContextBuilder:
|
|
|
140
141
|
# 各段落构建方法
|
|
141
142
|
# =========================================================================
|
|
142
143
|
|
|
144
|
+
def _build_datetime(self) -> str:
|
|
145
|
+
"""
|
|
146
|
+
构建 <datetime> 段落 —— 当前日期时间(精确到秒)。
|
|
147
|
+
让 LLM 知道当前时间,以便给出与时间相关的回答。
|
|
148
|
+
"""
|
|
149
|
+
from datetime import datetime
|
|
150
|
+
now = datetime.now()
|
|
151
|
+
weekdays = ["星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日"]
|
|
152
|
+
date_str = now.strftime("%Y年%m月%d日")
|
|
153
|
+
time_str = now.strftime("%H:%M:%S")
|
|
154
|
+
weekday = weekdays[now.weekday()]
|
|
155
|
+
return (
|
|
156
|
+
f"<datetime>\n"
|
|
157
|
+
f"当前时间: {date_str} {weekday} {time_str}\n"
|
|
158
|
+
f"时间戳: {now.timestamp()}\n"
|
|
159
|
+
f"</datetime>"
|
|
160
|
+
)
|
|
161
|
+
|
|
143
162
|
def _build_whomi(
|
|
144
163
|
self,
|
|
145
164
|
agent_name: str,
|
|
@@ -275,16 +294,18 @@ class ContextBuilder:
|
|
|
275
294
|
self,
|
|
276
295
|
conversation_history: List["Message"],
|
|
277
296
|
max_chars: int,
|
|
297
|
+
session_id: str = "",
|
|
278
298
|
) -> str:
|
|
279
299
|
"""
|
|
280
300
|
构建 <resentdialog> 段落 —— 近期对话历史。
|
|
281
301
|
|
|
282
|
-
|
|
283
|
-
|
|
302
|
+
将对话格式化为带角色标签的文本。当历史过长时,将较早的消息
|
|
303
|
+
压缩为摘要,保留近期消息完整呈现,总字符数不超过 max_chars。
|
|
284
304
|
|
|
285
305
|
Args:
|
|
286
306
|
conversation_history: 对话历史消息列表
|
|
287
307
|
max_chars: 最大字符数限制
|
|
308
|
+
session_id: 会话 ID(用于 MemoryManager 摘要)
|
|
288
309
|
|
|
289
310
|
Returns:
|
|
290
311
|
<resentdialog> XML 段落字符串
|
|
@@ -300,32 +321,126 @@ class ContextBuilder:
|
|
|
300
321
|
"tool": "工具",
|
|
301
322
|
}
|
|
302
323
|
|
|
303
|
-
#
|
|
304
|
-
|
|
324
|
+
# 过滤空消息并格式化
|
|
325
|
+
filtered_msgs: List[tuple] = []
|
|
305
326
|
for msg in conversation_history:
|
|
306
327
|
role = getattr(msg, "role", "user")
|
|
307
328
|
content = getattr(msg, "content", "")
|
|
308
329
|
if not content.strip():
|
|
309
330
|
continue
|
|
310
|
-
|
|
311
331
|
label = role_labels.get(role, role)
|
|
312
|
-
|
|
332
|
+
filtered_msgs.append((label, content.strip()))
|
|
333
|
+
|
|
334
|
+
if not filtered_msgs:
|
|
335
|
+
return "<resentdialog>\n(无对话历史)\n</resentdialog>"
|
|
336
|
+
|
|
337
|
+
# 当消息超过阈值时,将旧消息压缩为摘要
|
|
338
|
+
SUMMARY_THRESHOLD = 30 # 超过30条时启用摘要
|
|
339
|
+
RECENT_KEEP = 15 # 保留最近15条完整消息
|
|
340
|
+
SUMMARY_BUDGET = 3000 # 摘要最大字符数
|
|
341
|
+
|
|
342
|
+
prefix_text = ""
|
|
343
|
+
recent_msgs = filtered_msgs
|
|
344
|
+
|
|
345
|
+
if len(filtered_msgs) > SUMMARY_THRESHOLD:
|
|
346
|
+
old_msgs = filtered_msgs[:-RECENT_KEEP]
|
|
347
|
+
recent_msgs = filtered_msgs[-RECENT_KEEP:]
|
|
348
|
+
prefix_text = self._build_dialog_summary(old_msgs, SUMMARY_BUDGET)
|
|
349
|
+
|
|
350
|
+
# 格式化近期消息
|
|
351
|
+
formatted_lines: List[str] = []
|
|
352
|
+
if prefix_text:
|
|
353
|
+
formatted_lines.append(prefix_text)
|
|
354
|
+
formatted_lines.append("") # 空行分隔
|
|
355
|
+
|
|
356
|
+
for label, content in recent_msgs:
|
|
357
|
+
formatted_lines.append(f"[{label}] {_xml_escape(content)}")
|
|
313
358
|
|
|
314
359
|
dialog_text = "\n".join(formatted_lines)
|
|
315
360
|
|
|
316
|
-
#
|
|
361
|
+
# Token 预算裁剪:超预算时从最早的消息开始移除
|
|
317
362
|
if len(dialog_text) > max_chars:
|
|
318
|
-
#
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
363
|
+
# 保留摘要前缀,裁剪近期消息部分
|
|
364
|
+
if prefix_text:
|
|
365
|
+
# 先尝试只裁剪近期消息
|
|
366
|
+
recent_budget = max_chars - len(prefix_text) - 100
|
|
367
|
+
if recent_budget < 500:
|
|
368
|
+
# 摘要本身太长,整体裁剪
|
|
369
|
+
dialog_text = dialog_text[-max_chars:]
|
|
370
|
+
else:
|
|
371
|
+
recent_part = "\n".join(formatted_lines[len(formatted_lines) - len(recent_msgs):])
|
|
372
|
+
if len(recent_part) > recent_budget:
|
|
373
|
+
recent_part = self._trim_messages_from_start(recent_part, recent_budget)
|
|
374
|
+
dialog_text = prefix_text + "\n\n" + recent_part
|
|
375
|
+
else:
|
|
376
|
+
dialog_text = self._trim_messages_from_start(dialog_text, max_chars)
|
|
377
|
+
dialog_text = "(... 前面的对话已被裁剪 ...)\n" + dialog_text
|
|
326
378
|
|
|
327
379
|
return f"<resentdialog>\n{dialog_text}\n</resentdialog>"
|
|
328
380
|
|
|
381
|
+
def _build_dialog_summary(self, old_msgs: List[tuple], max_chars: int) -> str:
|
|
382
|
+
"""
|
|
383
|
+
将旧消息列表压缩为摘要文本。
|
|
384
|
+
|
|
385
|
+
策略: 提取每条消息的第一行作为要点,保留关键信息的同时大幅压缩篇幅。
|
|
386
|
+
如果有 MemoryManager,也可以调用 LLM 生成摘要(未来扩展)。
|
|
387
|
+
|
|
388
|
+
Args:
|
|
389
|
+
old_msgs: (label, content) 元组列表
|
|
390
|
+
max_chars: 摘要最大字符数
|
|
391
|
+
|
|
392
|
+
Returns:
|
|
393
|
+
摘要文本字符串
|
|
394
|
+
"""
|
|
395
|
+
if not old_msgs:
|
|
396
|
+
return ""
|
|
397
|
+
|
|
398
|
+
summary_parts: List[str] = ["[历史对话摘要]"]
|
|
399
|
+
for label, content in old_msgs:
|
|
400
|
+
# 提取第一行或前100字符作为要点
|
|
401
|
+
first_line = content.split("\n")[0].strip()
|
|
402
|
+
if len(first_line) > 100:
|
|
403
|
+
first_line = first_line[:100] + "..."
|
|
404
|
+
summary_parts.append(f"- [{label}] {first_line}")
|
|
405
|
+
|
|
406
|
+
summary_text = "\n".join(summary_parts)
|
|
407
|
+
|
|
408
|
+
# 摘要本身也要限制长度
|
|
409
|
+
if len(summary_text) > max_chars:
|
|
410
|
+
summary_text = summary_text[:max_chars] + "\n... (更多历史已省略)"
|
|
411
|
+
|
|
412
|
+
return summary_text
|
|
413
|
+
|
|
414
|
+
def _trim_messages_from_start(self, text: str, max_chars: int) -> str:
|
|
415
|
+
"""
|
|
416
|
+
从文本开头裁剪消息,保留尾部(最新消息优先)。
|
|
417
|
+
|
|
418
|
+
按行(即按消息)为单位裁剪,避免在消息中间截断。
|
|
419
|
+
|
|
420
|
+
Args:
|
|
421
|
+
text: 格式化后的对话文本
|
|
422
|
+
max_chars: 最大保留字符数
|
|
423
|
+
|
|
424
|
+
Returns:
|
|
425
|
+
裁剪后的文本
|
|
426
|
+
"""
|
|
427
|
+
if len(text) <= max_chars:
|
|
428
|
+
return text
|
|
429
|
+
|
|
430
|
+
lines = text.split("\n")
|
|
431
|
+
result_lines: List[str] = []
|
|
432
|
+
total = 0
|
|
433
|
+
|
|
434
|
+
# 从后往前添加行,保留最新的消息
|
|
435
|
+
for line in reversed(lines):
|
|
436
|
+
if total + len(line) + 1 > max_chars:
|
|
437
|
+
break
|
|
438
|
+
result_lines.append(line)
|
|
439
|
+
total += len(line) + 1
|
|
440
|
+
|
|
441
|
+
result_lines.reverse()
|
|
442
|
+
return "\n".join(result_lines)
|
|
443
|
+
|
|
329
444
|
def _build_user_input(
|
|
330
445
|
self,
|
|
331
446
|
user_typed_text: str,
|
package/departments/manager.py
CHANGED
|
@@ -203,16 +203,22 @@ class DepartmentManager:
|
|
|
203
203
|
if not name:
|
|
204
204
|
return {"ok": False, "message": "部门名称不能为空"}
|
|
205
205
|
|
|
206
|
+
# 清理部门名称:移除空格和特殊字符,保留中英文、数字、下划线、连字符
|
|
207
|
+
import re
|
|
208
|
+
clean_name = re.sub(r'[^\w\u4e00-\u9fff-]', '', name)
|
|
209
|
+
if not clean_name:
|
|
210
|
+
return {"ok": False, "message": "部门名称包含无效字符"}
|
|
211
|
+
|
|
206
212
|
# 安全校验
|
|
207
213
|
if not self._validate_path(parent):
|
|
208
214
|
return {"ok": False, "message": "非法的父部门路径"}
|
|
209
|
-
if not self._validate_path(
|
|
215
|
+
if not self._validate_path(clean_name):
|
|
210
216
|
return {"ok": False, "message": "非法的部门名称"}
|
|
211
|
-
if "/" in
|
|
217
|
+
if "/" in clean_name or "\\" in clean_name:
|
|
212
218
|
return {"ok": False, "message": "部门名称不能包含 / 或 \\"}
|
|
213
219
|
|
|
214
|
-
#
|
|
215
|
-
dept_path = f"{parent}/{
|
|
220
|
+
# 构建完整路径(使用清理后的名称作为目录名)
|
|
221
|
+
dept_path = f"{parent}/{clean_name}" if parent else clean_name
|
|
216
222
|
|
|
217
223
|
# 检查是否已存在
|
|
218
224
|
dept_dir = self._dept_dir(dept_path)
|
|
@@ -243,13 +249,14 @@ class DepartmentManager:
|
|
|
243
249
|
encoding="utf-8",
|
|
244
250
|
)
|
|
245
251
|
|
|
246
|
-
#
|
|
252
|
+
# 自动创建群聊(部门群不带默认 owner,成员由 assign_agent 添加)
|
|
247
253
|
chat_group_id = ""
|
|
248
254
|
if self._group_manager:
|
|
249
255
|
try:
|
|
250
256
|
group_name = f"部门: {name}"
|
|
251
257
|
group = self._group_manager.create_group(
|
|
252
258
|
name=group_name,
|
|
259
|
+
owner="", # 部门群无默认 owner
|
|
253
260
|
description=description or f"{name} 部门群聊",
|
|
254
261
|
avatar_emoji=emoji or "🏢",
|
|
255
262
|
)
|
|
@@ -559,6 +566,20 @@ class DepartmentManager:
|
|
|
559
566
|
logger.warning(
|
|
560
567
|
f"添加 {agent} 到群聊失败 ({chat_group_id}): {e}"
|
|
561
568
|
)
|
|
569
|
+
# 如果部门群没有 owner,将第一个 agent 设为 owner
|
|
570
|
+
if not meta.get("head") and self._group_manager:
|
|
571
|
+
try:
|
|
572
|
+
group = self._group_manager.get_group(chat_group_id)
|
|
573
|
+
if group and not group.owner:
|
|
574
|
+
group.owner = agent
|
|
575
|
+
self._group_manager.add_member(
|
|
576
|
+
chat_group_id, agent, role="owner"
|
|
577
|
+
)
|
|
578
|
+
meta["head"] = agent
|
|
579
|
+
except Exception as e:
|
|
580
|
+
logger.warning(
|
|
581
|
+
f"设置部门群 owner 失败 ({chat_group_id}): {e}"
|
|
582
|
+
)
|
|
562
583
|
msg = f"已添加 {len(agents)} 个成员"
|
|
563
584
|
elif action == "remove":
|
|
564
585
|
removed = [a for a in agents if a in current_agents]
|
|
@@ -638,6 +659,40 @@ class DepartmentManager:
|
|
|
638
659
|
logger.info(f"部门 {path} 负责人已{action}: {head or '无'}")
|
|
639
660
|
return {"ok": True, "meta": meta, "message": f"已{action}负责人"}
|
|
640
661
|
|
|
662
|
+
def update_dept_meta(
|
|
663
|
+
self,
|
|
664
|
+
path: str,
|
|
665
|
+
description: str = None,
|
|
666
|
+
head: str = None,
|
|
667
|
+
) -> Dict[str, Any]:
|
|
668
|
+
"""
|
|
669
|
+
更新部门元数据(描述、负责人等),不修改 dept.md。
|
|
670
|
+
|
|
671
|
+
Args:
|
|
672
|
+
path: 部门路径
|
|
673
|
+
description: 新描述(None=不修改)
|
|
674
|
+
head: 负责人 Agent 路径(None=不修改)
|
|
675
|
+
|
|
676
|
+
Returns:
|
|
677
|
+
{ok, meta, message}
|
|
678
|
+
"""
|
|
679
|
+
if not self._validate_path(path):
|
|
680
|
+
return {"ok": False, "message": "非法路径"}
|
|
681
|
+
|
|
682
|
+
meta = self._load_meta(path)
|
|
683
|
+
if not meta:
|
|
684
|
+
return {"ok": False, "message": f"部门不存在: {path}"}
|
|
685
|
+
|
|
686
|
+
if description is not None:
|
|
687
|
+
meta["description"] = description
|
|
688
|
+
if head is not None:
|
|
689
|
+
meta["head"] = head
|
|
690
|
+
meta["updated_at"] = timestamp()
|
|
691
|
+
self._save_meta(path, meta)
|
|
692
|
+
|
|
693
|
+
logger.info(f"部门 {path} 元数据已更新")
|
|
694
|
+
return {"ok": True, "meta": meta, "message": "已更新"}
|
|
695
|
+
|
|
641
696
|
# ==========================================================================
|
|
642
697
|
# 部门介绍(dept.md)
|
|
643
698
|
# ==========================================================================
|
package/groups/manager.py
CHANGED
|
@@ -323,14 +323,15 @@ class GroupManager:
|
|
|
323
323
|
owner=owner,
|
|
324
324
|
)
|
|
325
325
|
|
|
326
|
-
#
|
|
327
|
-
|
|
328
|
-
|
|
326
|
+
# 添加创建者为群主(仅当 owner 非空时)
|
|
327
|
+
if owner:
|
|
328
|
+
owner_member = GroupMember(agent_path=owner, role="owner")
|
|
329
|
+
group.add_member(owner_member)
|
|
329
330
|
|
|
330
331
|
# 添加额外成员
|
|
331
332
|
if member_paths:
|
|
332
333
|
for mp in member_paths:
|
|
333
|
-
if mp != owner:
|
|
334
|
+
if mp and mp != owner:
|
|
334
335
|
group.add_member(GroupMember(agent_path=mp, role="member"))
|
|
335
336
|
|
|
336
337
|
self._groups[group.id] = group
|
package/package.json
CHANGED
|
Binary file
|