一键导入
send-owner-clone-message
Send a friend or group message through the same human message channel, but with the current TopoClaw owner's digital clone identity.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Send a friend or group message through the same human message channel, but with the current TopoClaw owner's digital clone identity.
用 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.
Unified group creation skill supporting multiple friends and assistants, with optional first message sending.
Schedule reminders and recurring tasks.
| name | send-owner-clone-message |
| description | Send a friend or group message through the same human message channel, but with the current TopoClaw owner's digital clone identity. |
| metadata | {"topoclaw":{"emoji":"💬","requires":{"bins":["python"]}}} |
Use this skill when the user asks TopoClaw to send a message to a friend or group as the current owner's digital clone.
This skill intentionally uses the same customer_service message routes as normal user sending:
POST /api/friends/send-messagePOST /api/groups/send-messageTOPO_IMEI (required): current caller IMEI (owner IMEI of this TopoClaw)
target_type (required): friend or grouptarget_id (required):
content (optional): message textmessage_type (optional): text or image (default text)image_base64 (optional): base64 image payload (without data:image/...;base64, prefix)sender_label (optional): clone display name; default 我的数字分身TOPO_TARGET_TYPETOPO_TARGET_IDTOPO_MESSAGE_CONTENTTOPO_MESSAGE_TYPETOPO_MESSAGE_IMAGE_BASE64TOPO_SENDER_LABELTOPO_FILE_BASE64TOPO_IMAGE_BASE64FILE_BASE64IMAGE_BASE64CUSTOMER_SERVICE_URL (optional): service base URL overrideVITE_MOBILE_AGENT_CUSTOMER_SERVICE_URL (recommended): default customer_service base URL from TopoDesktop .env.local
TopoDesktop/.env.local127.0.0.1Structured JSON:
successtarget_typetarget_idowner_imeisender_labelmessage_typehas_imagecontent_previewraw (server response)import os
import json
import requests
owner_imei = os.getenv("TOPO_IMEI", "").strip() or os.getenv("IMEI", "").strip()
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("/")
# Editable inputs(优先环境变量自动填充,减少首次调用重试)
target_type = os.getenv("TOPO_TARGET_TYPE", "").strip().lower()
target_id = os.getenv("TOPO_TARGET_ID", "").strip()
content = os.getenv("TOPO_MESSAGE_CONTENT", "").strip()
message_type = os.getenv("TOPO_MESSAGE_TYPE", "").strip().lower() or "text"
image_base64 = (
os.getenv("TOPO_MESSAGE_IMAGE_BASE64", "").strip()
or os.getenv("TOPO_FILE_BASE64", "").strip()
or os.getenv("TOPO_IMAGE_BASE64", "").strip()
or os.getenv("FILE_BASE64", "").strip()
or os.getenv("IMAGE_BASE64", "").strip()
)
sender_label = os.getenv("TOPO_SENDER_LABEL", "").strip() or "我的数字分身"
if image_base64.startswith("data:") and "," in image_base64:
# Allow both pure base64 and data URL.
image_base64 = image_base64.split(",", 1)[1].strip()
if not owner_imei:
raise RuntimeError("Missing TOPO_IMEI (current caller 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 target_type not in ("friend", "group"):
raise RuntimeError("target_type must be 'friend' or 'group' (set TOPO_TARGET_TYPE)")
if not target_id:
raise RuntimeError("target_id is required (set TOPO_TARGET_ID)")
if message_type not in ("text", "image"):
raise RuntimeError("message_type must be 'text' or 'image' (set TOPO_MESSAGE_TYPE)")
if image_base64 and message_type == "text":
message_type = "image"
if not content and not image_base64:
raise RuntimeError("either content or image_base64 is required (set TOPO_MESSAGE_CONTENT / TOPO_MESSAGE_IMAGE_BASE64)")
if message_type == "image" and not image_base64:
raise RuntimeError("image message requires image_base64 (set TOPO_MESSAGE_IMAGE_BASE64)")
if message_type == "image" and not content:
# customer_service schemas require content(str); give a safe placeholder when image-only.
content = "[图片]"
common_clone_fields = {
"senderLabel": sender_label,
"isCloneReply": True,
"cloneOwnerImei": owner_imei,
"cloneOrigin": "digital_clone",
}
if target_type == "friend":
url = f"{base_url}/api/friends/send-message"
payload = {
"imei": owner_imei,
"targetImei": target_id,
"content": content,
"message_type": message_type,
**({"imageBase64": image_base64} if image_base64 else {}),
**common_clone_fields,
}
else:
# Group mode: tolerate passing group name as target_id.
if not str(target_id).startswith("group_"):
try:
groups_resp = requests.get(
f"{base_url}/api/groups/list",
params={"imei": owner_imei},
timeout=20,
)
groups_resp.raise_for_status()
groups_raw = groups_resp.json()
groups = groups_raw.get("groups") or []
by_name = {
str(g.get("name") or "").strip(): str(g.get("id") or "").strip()
for g in groups
if str(g.get("name") or "").strip() and str(g.get("id") or "").strip()
}
target_id = by_name.get(target_id, target_id)
except Exception:
# Keep original target_id and let server-side validation report the real cause.
pass
url = f"{base_url}/api/groups/send-message"
payload = {
"imei": owner_imei,
"groupId": target_id,
"content": content,
"message_type": message_type,
**({"imageBase64": image_base64} if image_base64 else {}),
"skipServerAssistantDispatch": False,
# 群里展示字段与单聊字段都传,兼容不同端解析
"sender": sender_label,
**common_clone_fields,
}
resp = requests.post(url, json=payload, timeout=20)
if resp.status_code >= 400:
detail = ""
try:
detail = str(resp.json())
except Exception:
detail = resp.text
raise RuntimeError(f"send owner clone message failed: HTTP {resp.status_code}, detail={detail}")
raw = resp.json()
if not raw.get("success"):
raise RuntimeError(f"send owner clone message failed: {raw}")
result = {
"success": True,
"target_type": target_type,
"target_id": target_id,
"owner_imei": owner_imei,
"sender_label": sender_label,
"message_type": message_type,
"has_image": bool(image_base64),
"content_preview": content[:80],
"raw": raw,
}
print(json.dumps(result, ensure_ascii=False, indent=2))