원클릭으로
create-group-and-send-message
Unified group creation skill supporting multiple friends and assistants, with optional first message sending.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Unified group creation skill supporting multiple friends and assistants, with optional first message sending.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
A-share (沪深) investment assistance using akshare_stock and akshare_stock_technical — fundamentals, technicals, sector context, and decision framing (not financial advice).
Use the browser to search the web, log into websites, or extract live page content using spawn_browser_interactive_task.
Search and install agent skills from ClawHub, the public skill registry.
Retrieve current user's friends and assistant profiles (friend name/signature/preferences + assistant nickname/intro/baseUrl) from customer_service APIs. Use when user asks to inspect or export social/contact profile data.
Schedule reminders and recurring tasks.
Retrieve top deeplink candidates from desktop-synced catalog.
| name | create-group-and-send-message |
| description | Unified group creation skill supporting multiple friends and assistants, with optional first message sending. |
| metadata | {"topoclaw":{"emoji":"lightning","requires":{"bins":["python"]}}} |
Use this as the only group-creation skill. It supersedes legacy create-group.
write_file to generate temporary .py files.exec with script-file paths.>nul, 2>nul, > /dev/null.release/win-unpacked/resources/....TOPO_IMEI (required): caller IMEITOPO_OWNER_IMEI (optional): owner IMEI override, default caller IMEITOPO_GROUP_NAME (optional): group name, default \u6211\u7684\u65b0\u7fa4\u7ec4TOPO_FRIEND_IMEIS (optional): JSON array or comma-separated IMEIsTOPO_TARGET_FRIEND (optional, backward-compatible): one selector (nickname/name/IMEI)TOPO_TARGET_FRIENDS (optional): JSON array or comma-separated selectors (nickname/name/IMEI)TOPO_ASSISTANT_DISPLAY_IDS (optional): JSON array or comma-separated display IDsTOPO_ASSISTANT_IDS (optional, deprecated): alias for display IDsTOPO_GROUP_MANAGER_ASSISTANT_DISPLAY_ID (optional): display ID to enable GroupManager capabilityTOPO_GROUP_MANAGER_ASSISTANT_ID (optional, deprecated): aliasTOPO_AUTO_ADD_FRIEND_TOPOCLAW (optional, default true; when friend clone is missing, trigger /api/profile/{imei} once and retry)TOPO_GROUP_MESSAGE (optional): first message contentTOPO_SEND_FIRST_MESSAGE (optional bool): defaults to true only when message existsTOPO_GROUP_SENDER (optional): first message sender label, default TopoClawCUSTOMER_SERVICE_URL (optional)VITE_MOBILE_AGENT_CUSTOMER_SERVICE_URL (recommended)POST /api/groups/create.free_discovery=true; if false, call POST /api/groups/update-config.POST /api/groups/add-assistant.group_manager_assistant_display_id is set, call POST /api/groups/update-assistant-config.POST /api/groups/send-assistant-message.GET /api/groups/{groupId} and return structured JSON.When reporting to user:
assistant_add_results[i].success == true.auto_friend_topoclaw_candidates means "resolved candidate assistants", not "added successfully".auto_friend_topoclaw_ensure_attempts records profile ensure/retry attempts for missing default clones.auto_friend_topoclaw_errors or assistant_add_results[].error.TOPO_SEND_FIRST_MESSAGE=true with empty message -> fail fastimport json
import os
import requests
caller_imei = os.getenv("TOPO_IMEI", "").strip() or os.getenv("IMEI", "").strip()
owner_imei = os.getenv("TOPO_OWNER_IMEI", "").strip() or caller_imei
group_name = os.getenv("TOPO_GROUP_NAME", "").strip() or "\u6211\u7684\u65b0\u7fa4\u7ec4"
base_url = (
os.getenv("TOPO_ACTIVE_CUSTOMER_SERVICE_URL", "").strip()
or os.getenv("CUSTOMER_SERVICE_URL", "").strip()
or os.getenv("VITE_MOBILE_AGENT_CUSTOMER_SERVICE_URL", "").strip()
).rstrip("/")
def _env_list(name: str):
raw = os.getenv(name, "").strip()
if not raw:
return []
try:
val = json.loads(raw)
if isinstance(val, list):
return [str(x).strip() for x in val if str(x).strip()]
except Exception:
pass
return [x.strip() for x in raw.split(",") if x.strip()]
def _env_bool(name: str, default: bool) -> bool:
raw = os.getenv(name, "").strip().lower()
if not raw:
return default
return raw in ("1", "true", "yes", "on")
def _norm(s: str) -> str:
return str(s or "").strip().lower()
def _normalize_base_url(url: str) -> str:
return str(url or "").strip().lower().rstrip("/")
if not owner_imei:
raise RuntimeError("Missing TOPO_IMEI/TOPO_OWNER_IMEI")
if not base_url:
raise RuntimeError("Missing customer_service URL (TOPO_ACTIVE_CUSTOMER_SERVICE_URL / CUSTOMER_SERVICE_URL / VITE_MOBILE_AGENT_CUSTOMER_SERVICE_URL)")
if not group_name.strip():
raise RuntimeError("group_name cannot be empty")
friend_imeis = _env_list("TOPO_FRIEND_IMEIS")
target_friend = os.getenv("TOPO_TARGET_FRIEND", "").strip()
target_friends = _env_list("TOPO_TARGET_FRIENDS")
assistant_display_ids = _env_list("TOPO_ASSISTANT_DISPLAY_IDS") or _env_list("TOPO_ASSISTANT_IDS")
group_manager_display_id = (
os.getenv("TOPO_GROUP_MANAGER_ASSISTANT_DISPLAY_ID", "").strip()
or os.getenv("TOPO_GROUP_MANAGER_ASSISTANT_ID", "").strip()
)
auto_add_friend_topoclaw = _env_bool("TOPO_AUTO_ADD_FRIEND_TOPOCLAW", True)
group_message = os.getenv("TOPO_GROUP_MESSAGE", "").strip()
send_env_raw = os.getenv("TOPO_SEND_FIRST_MESSAGE", "").strip()
send_first_message = _env_bool("TOPO_SEND_FIRST_MESSAGE", bool(group_message)) if send_env_raw else bool(group_message)
group_sender = os.getenv("TOPO_GROUP_SENDER", "").strip() or "TopoClaw"
if send_first_message and not group_message:
raise RuntimeError("TOPO_SEND_FIRST_MESSAGE=true but TOPO_GROUP_MESSAGE is empty")
def get(path: str, params=None):
url = f"{base_url}/{path.lstrip('/')}"
r = requests.get(url, params=params, timeout=20)
r.raise_for_status()
return r.json()
def post(path: str, body: dict):
url = f"{base_url}/{path.lstrip('/')}"
r = requests.post(url, json=body, timeout=20)
r.raise_for_status()
return r.json()
def _is_customer_topoclaw_assistant(item: dict) -> bool:
aid = str(item.get("id") or "").strip().lower()
did = str(item.get("displayId") or "").strip().lower()
name = str(item.get("name") or "").strip().lower()
bu = _normalize_base_url(item.get("baseUrl") or "")
if "customer_topoclaw" in aid or "customer_topoclaw" in did:
return True
return ("topoclaw" in aid or "topoclaw" in name) and bu != "topoclaw://relay"
def _fetch_custom_assistants(target_imei: str):
data = get("/api/custom-assistants", {"imei": target_imei})
arr = data.get("assistants") or []
return arr if isinstance(arr, list) else []
def _resolve_default_topoclaw_assistant(target_imei: str):
assistants = _fetch_custom_assistants(target_imei)
preferred, relay_fallback = [], []
for item in assistants:
if not isinstance(item, dict):
continue
if _is_customer_topoclaw_assistant(item):
preferred.append(item)
elif _normalize_base_url(item.get("baseUrl") or "") == "topoclaw://relay":
relay_fallback.append(item)
for item in (preferred or relay_fallback):
did = str(item.get("displayId") or "").strip()
if not did:
continue
return {
"owner_imei": target_imei,
"display_id": did,
"assistant_id_internal": str(item.get("id") or "").strip(),
"name": str(item.get("name") or "TopoClaw").strip() or "TopoClaw",
"base_url": str(item.get("baseUrl") or "topoclaw://relay"),
"intro": str(item.get("intro") or ""),
"avatar": str(item.get("avatar") or ""),
"capabilities": item.get("capabilities") or ["chat", "skills", "cron"],
"multi_session": bool(item.get("multiSessionEnabled", True)),
}
return None
def _ensure_default_topoclaw_via_profile(target_imei: str):
imei = str(target_imei or "").strip()
if not imei:
return False, "empty_member_imei", None
try:
url = f"{base_url}/api/profile/{imei}"
r = requests.post(url, data={}, timeout=20)
r.raise_for_status()
data = r.json()
if isinstance(data, dict) and data.get("success"):
return True, None, data
return False, f"profile_update_unsuccess: {data}", data
except Exception as e:
return False, f"ensure_default_topoclaw_via_profile_failed: {e}", None
def _resolve_assistant_by_display_id(target_imei: str, display_id: str):
did = str(display_id or "").strip()
if not did:
return None
for item in _fetch_custom_assistants(target_imei):
if not isinstance(item, dict):
continue
if str(item.get("displayId") or "").strip() != did:
continue
return {
"owner_imei": target_imei,
"display_id": did,
"assistant_id_internal": str(item.get("id") or "").strip(),
"name": str(item.get("name") or did),
"base_url": str(item.get("baseUrl") or ""),
"intro": str(item.get("intro") or ""),
"avatar": str(item.get("avatar") or ""),
"capabilities": item.get("capabilities") or ["chat"],
"multi_session": bool(item.get("multiSessionEnabled", True)),
}
return None
def _resolve_friend_selectors(selectors: list[str]):
if not selectors:
return [], []
data = get("/api/friends/list", {"imei": owner_imei})
friends = data.get("friends") or []
if not isinstance(friends, list):
friends = []
resolved, errors = [], []
for selector in selectors:
q = _norm(selector)
exact, contains = [], []
for f in friends:
if not isinstance(f, dict):
continue
imei = str(f.get("imei") or "").strip()
nickname = str(f.get("nickname") or "").strip()
candidates = [_norm(imei), _norm(nickname)]
if q in candidates:
exact.append({"imei": imei, "nickname": nickname or imei, "query": selector})
elif any(q and q in c for c in candidates if c):
contains.append({"imei": imei, "nickname": nickname or imei, "query": selector})
if len(exact) == 1:
resolved.append(exact[0])
elif len(exact) > 1:
errors.append({"query": selector, "error": "ambiguous_exact", "matches": exact})
elif len(contains) == 1:
resolved.append(contains[0])
elif len(contains) > 1:
errors.append({"query": selector, "error": "ambiguous_contains", "matches": contains})
else:
errors.append({"query": selector, "error": "not_found"})
return resolved, errors
friend_selectors = ([target_friend] if target_friend else []) + [x for x in target_friends if str(x).strip()]
resolved_friends, friend_selector_errors = _resolve_friend_selectors(friend_selectors)
member_imeis = list(dict.fromkeys(
[str(x).strip() for x in friend_imeis if str(x).strip()]
+ [str(x.get("imei") or "").strip() for x in resolved_friends if str(x.get("imei") or "").strip()]
))
assistant_spec_map = {}
assistant_display_resolve_errors = []
for did in [str(x).strip() for x in assistant_display_ids if str(x).strip()]:
spec = _resolve_assistant_by_display_id(owner_imei, did)
if spec:
assistant_spec_map[spec["display_id"]] = spec
else:
assistant_display_resolve_errors.append({"display_id": did, "error": "display_id_not_found"})
auto_friend_topoclaw_candidates = []
auto_friend_topoclaw_errors = []
auto_friend_topoclaw_ensure_attempts = []
if auto_add_friend_topoclaw:
for m_imei in list(dict.fromkeys([owner_imei, *member_imeis])):
try:
spec = _resolve_default_topoclaw_assistant(m_imei)
if not spec:
ensured, ensure_err, ensure_raw = _ensure_default_topoclaw_via_profile(m_imei)
ensure_item = {
"member_imei": m_imei,
"ensure_called": True,
"ensure_success": bool(ensured),
}
if ensure_err:
ensure_item["ensure_error"] = ensure_err
if ensure_raw is not None:
ensure_item["ensure_raw"] = ensure_raw
if ensured:
spec = _resolve_default_topoclaw_assistant(m_imei)
ensure_item["retry_success"] = bool(spec)
if spec:
ensure_item["retry_display_id"] = spec.get("display_id")
else:
ensure_item["retry_error"] = "default_topoclaw_not_found_after_ensure"
auto_friend_topoclaw_ensure_attempts.append(ensure_item)
if not spec:
auto_friend_topoclaw_errors.append({"member_imei": m_imei, "error": "default_topoclaw_not_found"})
continue
assistant_spec_map[spec["display_id"]] = spec
auto_friend_topoclaw_candidates.append({
"member_imei": m_imei,
"display_id": spec["display_id"],
"assistant_id_internal": spec["assistant_id_internal"],
})
except Exception as e:
auto_friend_topoclaw_errors.append({"member_imei": m_imei, "error": str(e)})
if group_manager_display_id and group_manager_display_id not in assistant_spec_map:
spec = _resolve_assistant_by_display_id(owner_imei, group_manager_display_id)
if spec:
assistant_spec_map[spec["display_id"]] = spec
else:
assistant_display_resolve_errors.append({"display_id": group_manager_display_id, "error": "group_manager_resolve_failed"})
assistant_display_ids_final = list(assistant_spec_map.keys())
create_res = post("/api/groups/create", {
"imei": owner_imei,
"name": group_name.strip(),
"memberImeis": member_imeis,
"assistantEnabled": bool(assistant_display_ids_final),
})
if not create_res.get("success"):
raise RuntimeError(f"Create group failed: {create_res}")
group_id = str(create_res.get("groupId") or "").strip()
if not group_id:
raise RuntimeError(f"Missing groupId from create result: {create_res}")
group = create_res.get("group") or {}
free_discovery = bool(group.get("free_discovery", False))
if not free_discovery:
cfg_res = post("/api/groups/update-config", {"imei": owner_imei, "groupId": group_id, "freeDiscovery": True})
if not cfg_res.get("success"):
raise RuntimeError(f"Enable freeDiscovery failed: {cfg_res}")
refreshed = get(f"/api/groups/{group_id}")
group = (refreshed or {}).get("group") or group
free_discovery = bool(group.get("free_discovery", False))
assistant_results = []
auto_friend_topoclaw_assistants = []
auto_candidate_set = {
(str(x.get("member_imei") or "").strip(), str(x.get("display_id") or "").strip())
for x in auto_friend_topoclaw_candidates
}
for display_id in assistant_display_ids_final:
spec = assistant_spec_map.get(display_id) or {}
internal_id = str(spec.get("assistant_id_internal") or "").strip() or display_id
item = {"displayId": display_id, "assistantId_internal": internal_id, "owner_imei": spec.get("owner_imei"), "success": False}
try:
add_res = post("/api/groups/add-assistant", {
"imei": owner_imei,
"groupId": group_id,
"assistantId": internal_id,
"creatorImei": spec.get("owner_imei") or owner_imei,
"displayId": display_id,
"baseUrl": spec.get("base_url") or "",
"name": spec.get("name") or display_id,
"capabilities": spec.get("capabilities") or ["chat"],
"intro": spec.get("intro") or "",
"avatar": spec.get("avatar") or "",
"multiSession": bool(spec.get("multi_session", True)),
})
item["success"] = bool(add_res.get("success"))
item["add_raw"] = add_res
if item["success"]:
owner_key = str(spec.get("owner_imei") or "").strip()
if (owner_key, display_id) in auto_candidate_set:
auto_friend_topoclaw_assistants.append({
"member_imei": owner_key,
"display_id": display_id,
"assistant_id_internal": internal_id,
"success": True,
})
elif (str(spec.get("owner_imei") or "").strip(), display_id) in auto_candidate_set:
auto_friend_topoclaw_errors.append({
"member_imei": str(spec.get("owner_imei") or "").strip() or None,
"display_id": display_id,
"error": "add_assistant_failed",
"raw": add_res,
})
if group_manager_display_id and display_id == group_manager_display_id and item["success"]:
gm_res = post("/api/groups/update-assistant-config", {
"imei": owner_imei,
"groupId": group_id,
"assistantId": internal_id,
"capabilities": ["chat", "group_manager"],
})
item["group_manager_config_updated"] = bool(gm_res.get("success"))
item["group_manager_update_raw"] = gm_res
except Exception as e:
item["error"] = str(e)
if (str(spec.get("owner_imei") or "").strip(), display_id) in auto_candidate_set:
auto_friend_topoclaw_errors.append({
"member_imei": str(spec.get("owner_imei") or "").strip() or None,
"display_id": display_id,
"error": str(e),
})
assistant_results.append(item)
send_raw = None
if send_first_message:
send_raw = post("/api/groups/send-assistant-message", {
"imei": owner_imei,
"groupId": group_id,
"content": group_message,
"sender": group_sender,
})
if not send_raw.get("success"):
raise RuntimeError(f"Send first group message failed: {send_raw}")
group_detail = get(f"/api/groups/{group_id}") if group_id else {}
result = {
"success": True,
"caller_imei": caller_imei or None,
"owner_imei": owner_imei,
"friend_selectors": friend_selectors,
"friend_selector_errors": friend_selector_errors,
"friend_imeis_final": member_imeis,
"assistant_display_resolve_errors": assistant_display_resolve_errors,
"auto_add_friend_topoclaw": bool(auto_add_friend_topoclaw),
"auto_friend_topoclaw_candidates": auto_friend_topoclaw_candidates,
"auto_friend_topoclaw_assistants": auto_friend_topoclaw_assistants,
"auto_friend_topoclaw_ensure_attempts": auto_friend_topoclaw_ensure_attempts,
"auto_friend_topoclaw_errors": auto_friend_topoclaw_errors,
"assistant_display_ids_final": assistant_display_ids_final,
"group_manager_assistant_display_id": group_manager_display_id or None,
"groupId": group_id,
"groupName": (group_detail.get("group") or {}).get("name") if isinstance(group_detail, dict) else group_name,
"free_discovery": free_discovery,
"create": create_res,
"assistant_add_results": assistant_results,
"send_first_message": bool(send_first_message),
"send_raw": send_raw,
"group": group_detail.get("group") if isinstance(group_detail, dict) else group_detail,
}
print(json.dumps(result, ensure_ascii=False, indent=2))