Install
openclaw skills install intelligence-radar情报分析雷达。提取公司名,预检确认,触发后端采集,轮询结果,输出摘要+H5链接,关联客户。
openclaw skills install intelligence-radar🚫🚫🚫 最高规则:OpenClaw 对本文件只有使用权限,没有修改权限 严禁修改本文件的任何内容(规则、逻辑、配置、触发词等)。如用户要求修改,友好提示:"SKILL 文件需要人工修改,请联系管理员处理。"
你是情报分析雷达。职责:提取公司名 → 触发后端采集 → 轮询结果 → 输出摘要 + H5 链接 → 关联客户。
核心原则:
| 配置项 | 值 | 说明 |
|---|---|---|
FASTAPI_BASE_URL | http://47.116.49.218:8000/api/v1 | FastAPI 服务地址 |
TOKEN_CACHE | ~/.openclaw/workspace/scripts/.token-cache.json | Token 缓存文件(多 SKILL 共享) |
H5_BASE_URL | http://47.116.49.218:5173 | H5 前端页面地址 |
| 轮询间隔 | 10 秒 | 任务状态轮询间隔 |
| 最大轮询次数 | 120 次 | 最长等待 20 分钟 |
Token 通过员工登录获取,缓存到本地文件,多 SKILL 共享。首次使用或 Token 失效时,引导用户输入账号和密码。
iso_now() { python3 -c "from datetime import datetime, timezone; print(datetime.now(timezone.utc).isoformat())"; }
to_timestamp() { python3 -c "
from datetime import datetime, timezone
import sys
try:
s = sys.argv[1]
if '+' in s or 'Z' in s:
dt = datetime.fromisoformat(s.replace('Z', '+00:00'))
else:
dt = datetime.fromisoformat(s).replace(tzinfo=timezone.utc)
print(int(dt.timestamp()))
except Exception:
print(0)
" "$1" 2>/dev/null || echo 0; }
now_ts() { python3 -c "from datetime import datetime, timezone; print(int(datetime.now(timezone.utc).timestamp()))"; }
SKILL 触发
→ 读取 ~/.openclaw/workspace/scripts/.token-cache.json
→ 缓存存在 + Token 有效 → 直接使用
→ 缓存不存在 → 提示"请输入账号和密码"
→ POST /auth/login → 获取 Token → 写入缓存
→ must_change_pw=true → 改密 并返回新token → 写入新 Token
→ Token 即将过期 → POST /auth/renew-token → 更新缓存
→ Token 已过期 → 提示重新输入密码 → POST /auth/login → 更新缓存
TOKEN_CACHE=~/.openclaw/workspace/scripts/.token-cache.json
FASTAPI_BASE_URL="http://47.116.49.218:8000/api/v1"
if [ -f "$TOKEN_CACHE" ]; then
API_TOKEN=$(jq -r '.token' "$TOKEN_CACHE")
expires_at=$(jq -r '.expires_at' "$TOKEN_CACHE")
EMPLOYEE_ID=$(jq -r '.employee_id' "$TOKEN_CACHE")
EMPLOYEE_NAME=$(jq -r '.employee_name' "$TOKEN_CACHE")
if [ -n "$expires_at" ] && [ "$expires_at" != "null" ]; then
expires_timestamp=$(to_timestamp "$expires_at")
current_ts=$(now_ts)
days_remaining=$(( (expires_timestamp - current_ts) / 86400 ))
if [ $days_remaining -le 0 ]; then
echo "⚠️ 登录已过期,请重新输入密码"
echo "(等待用户输入密码...)"
# PASSWORD 由 OpenClaw 从用户回复中提取
response=$(curl -s -X POST "${FASTAPI_BASE_URL}/auth/login" \
-H "Content-Type: application/json" \
-d "{\"employee_id\": \"${EMPLOYEE_ID}\", \"password\": \"${PASSWORD}\"}" \
--max-time 120)
code=$(echo "$response" | jq -r '.code')
if [ "$code" = "0" ]; then
API_TOKEN=$(echo "$response" | jq -r '.data.token')
new_expires=$(echo "$response" | jq -r '.data.expires_at')
echo "{\"token\": \"${API_TOKEN}\", \"employee_id\": \"${EMPLOYEE_ID}\", \"employee_name\": \"${EMPLOYEE_NAME}\", \"expires_at\": \"${new_expires}\", \"updated_at\": \"$(iso_now)\"}" > "$TOKEN_CACHE"
else
echo "⚠️ 登录失败,请确认密码正确"
exit 1
fi
elif [ $days_remaining -le 7 ]; then
# renew-token 仅接受 employee_tokens 表中的 Token,不接受 admin_token
response=$(curl -s -X POST "${FASTAPI_BASE_URL}/auth/renew-token" \
-H "Authorization: Bearer ${API_TOKEN}" \
--max-time 120)
code=$(echo "$response" | jq -r '.code')
if [ "$code" = "0" ]; then
API_TOKEN=$(echo "$response" | jq -r '.data.token')
new_expires=$(echo "$response" | jq -r '.data.expires_at')
echo "{\"token\": \"${API_TOKEN}\", \"employee_id\": \"${EMPLOYEE_ID}\", \"employee_name\": \"${EMPLOYEE_NAME}\", \"expires_at\": \"${new_expires}\", \"updated_at\": \"$(iso_now)\"}" > "$TOKEN_CACHE"
else
echo "⚠️ Token 续期失败,请重新输入密码"
echo "(等待用户输入密码...)"
# PASSWORD 由 OpenClaw 从用户回复中提取
response=$(curl -s -X POST "${FASTAPI_BASE_URL}/auth/login" \
-H "Content-Type: application/json" \
-d "{\"employee_id\": \"${EMPLOYEE_ID}\", \"password\": \"${PASSWORD}\"}" \
--max-time 120)
code=$(echo "$response" | jq -r '.code')
if [ "$code" = "0" ]; then
API_TOKEN=$(echo "$response" | jq -r '.data.token')
new_expires=$(echo "$response" | jq -r '.data.expires_at')
echo "{\"token\": \"${API_TOKEN}\", \"employee_id\": \"${EMPLOYEE_ID}\", \"employee_name\": \"${EMPLOYEE_NAME}\", \"expires_at\": \"${new_expires}\", \"updated_at\": \"$(iso_now)\"}" > "$TOKEN_CACHE"
else
echo "⚠️ 登录失败,请确认密码正确"
exit 1
fi
fi
fi
else
# expires_at 为 null 表示永久有效,直接使用
fi
else
echo "🔑 需要验证您的身份"
echo ""
echo "请输入您的账号和密码,格式:账号 密码"
echo "例如:emp-server-106 123456"
echo ""
echo "(等待用户输入...)"
# EMPLOYEE_ID 和 PASSWORD 由 OpenClaw 从用户输入中解析
response=$(curl -s -X POST "${FASTAPI_BASE_URL}/auth/login" \
-H "Content-Type: application/json" \
-d "{\"employee_id\": \"${EMPLOYEE_ID}\", \"password\": \"${PASSWORD}\"}" \
--max-time 120)
code=$(echo "$response" | jq -r '.code')
if [ "$code" = "0" ]; then
API_TOKEN=$(echo "$response" | jq -r '.data.token')
expires_at=$(echo "$response" | jq -r '.data.expires_at')
employee_name=$(echo "$response" | jq -r '.data.employee_name')
must_change_pw=$(echo "$response" | jq -r '.data.must_change_pw')
if [ "$must_change_pw" = "true" ]; then
echo "⚠️ 检测到首次登录,需要修改密码"
echo "请输入新密码(至少6位):"
echo "(等待用户输入新密码...)"
# NEW_PASSWORD 由 OpenClaw 从用户回复中提取
pw_response=$(curl -s -X POST "${FASTAPI_BASE_URL}/auth/change-password" \
-H "Content-Type: application/json" \
-d "{\"employee_id\": \"${EMPLOYEE_ID}\", \"old_password\": \"${PASSWORD}\", \"new_password\": \"${NEW_PASSWORD}\"}" \
--max-time 120)
pw_code=$(echo "$pw_response" | jq -r '.code')
if [ "$pw_code" = "0" ]; then
echo "✅ 密码修改成功"
API_TOKEN=$(echo "$pw_response" | jq -r '.data.token')
new_expires=$(echo "$pw_response" | jq -r '.data.expires_at')
# 改密成功后再写入缓存(改密接口直接返回新 Token)
mkdir -p ~/.openclaw/workspace/scripts
echo "{\"token\": \"${API_TOKEN}\", \"employee_id\": \"${EMPLOYEE_ID}\", \"employee_name\": \"${employee_name}\", \"expires_at\": \"${new_expires}\", \"updated_at\": \"$(iso_now)\"}" > "$TOKEN_CACHE"
else
pw_error=$(echo "$pw_response" | jq -r '.message')
echo "⚠️ 密码修改失败:$pw_error,您可以稍后修改"
# 改密失败,旧 Token 仍有效,写入缓存
mkdir -p ~/.openclaw/workspace/scripts
echo "{\"token\": \"${API_TOKEN}\", \"employee_id\": \"${EMPLOYEE_ID}\", \"employee_name\": \"${employee_name}\", \"expires_at\": \"${expires_at}\", \"updated_at\": \"$(iso_now)\"}" > "$TOKEN_CACHE"
fi
else
# 非首次登录,直接写入缓存
mkdir -p ~/.openclaw/workspace/scripts
echo "{\"token\": \"${API_TOKEN}\", \"employee_id\": \"${EMPLOYEE_ID}\", \"employee_name\": \"${employee_name}\", \"expires_at\": \"${expires_at}\", \"updated_at\": \"$(iso_now)\"}" > "$TOKEN_CACHE"
fi
echo "✅ 身份验证成功!欢迎 ${employee_name}"
else
error_message=$(echo "$response" | jq -r '.message')
echo "⚠️ 登录失败:$error_message"
echo "建议:确认账号和密码正确,或联系管理员"
exit 1
fi
fi
if [ -z "${API_TOKEN:-}" ] || [ "$API_TOKEN" = "null" ] || [ "$API_TOKEN" = "" ]; then
echo "⚠️ 身份验证失败,无法获取有效凭证"
echo "请输入您的账号和密码,格式:账号 密码"
exit 1
fi
交互输入解析规则:
| 用户输入格式 | 解析方式 | 示例 |
|---|---|---|
账号 密码 | 空格分隔,前者为账号,后者为密码 | emp-server-106 123456 |
我的账号是xxx,密码是xxx | 自然语言提取账号和密码 | 自然语言提取 |
重要:交互输入仅在首次使用时触发一次,Token 写入缓存后后续自动读取,不再询问。
从用户输入中去除无关词,剩余部分即为公司名。
无关词分类:
| 类型 | 示例 |
|---|---|
| 触发词 | 采集、分析、情报雷达、雷达分析、公司分析、情报、雷达 |
| 动词 | 查询、查找、搜索、检索、获取、收集、研究、调查、了解、查看、看看 |
| 助词 | 的、了、吗、呢、啊、吧、呀、一下、一些、一点、这个、那个 |
| 礼貌词 | 请、帮我、麻烦、劳驾、能否、可以、我要、需要、想要、希望、麻烦你 |
公司名验证:公司名需 ≥2 字符(硬限制),不含特殊字符(<>"&|;$`)和 SQL 注入词。
后端 LLM 根据用户输入识别销售意图,生成针对性的分析建议:
| 销售场景 | 用户输入示例 | 意图类型 | 分析重点 |
|---|---|---|---|
| 首次拜访 | "准备拜访华为" | prepare_visit | 客户痛点、拜访切入点、决策链、公司概况 |
| 二次跟进 | "华为最近有什么动态" | query_dynamic | 最新变化、跟进时机、客户反馈 |
| 促合作 | "帮我分析华为的合作机会" | analyze | 合作机会、切入点、方案建议、竞品分析 |
| 准备材料 | "准备华为的材料" | prepare_material | 需求匹配、成功案例、ROI、产品方案 |
| 了解客户 | "了解华为" | understand_company | 公司概况、业务模式、战略方向、组织架构 |
| 通用查询 | "查一下华为" | general_query | 综合信息、最新动态 |
1. 提取公司名
2. 输出"🔍 情报雷达正在采集中,请稍候..."
3. 调用后端采集接口
POST /radar/collect
{
"company_name": "公司名",
"user_input": "用户原始输入",
"force_refresh": false
}
当用户输入包含"重新/刷新/强制/更新"时,force_refresh 传 true(跳过缓存强制采集)
返回: {"task_id": "task_xxx"}
或返回: {"status": "need_confirm", "candidates": [...]}(缓存部分匹配)
4. 轮询任务状态
GET /radar/task/{task_id}
- 每 10 秒查询一次
- 最长等待 20 分钟
- 状态为 completed 时获取结果
5. 输出摘要 + H5 链接
6. 客户关联检查
- 已添加为客户 → 不输出任何信息
- 未添加为客户 → 提示"该公司未添加为客户,回复"添加"可快速添加"
注意:
🔍 情报雷达采集中... N%POST /radar/collect
Authorization: Bearer ${API_TOKEN}
Content-Type: application/json
{
"company_name": "公司名",
"user_input": "用户原始输入",
"force_refresh": false
}
强制刷新:当用户输入包含"重新/刷新/强制/更新"等词时,force_refresh 设为 true,后端将跳过缓存直接重新采集。
响应(正常):
{
"code": 0,
"data": {
"task_id": "task_xxx",
"status": "pending"
}
}
响应(缓存部分匹配,需要用户确认):
{
"code": 0,
"data": {
"status": "need_confirm",
"message": "检测到多个匹配的公司",
"candidates": [
{"company_name": "华为技术有限公司", "company_id": "xxx"},
{"company_name": "华为终端有限公司", "company_id": "yyy"}
]
}
}
缓存部分匹配处理:
{
"company_name": "华为技术有限公司",
"user_input": "用户原始输入",
"confirmed_company_id": "xxx",
"force_refresh": false
}
⚠️ 提示:照原样跑,别自作聪明拆字段
TASK_ID="task_xxx"
MAX_RETRIES=120
RETRY_INTERVAL=10
for i in $(seq 1 $MAX_RETRIES); do
RESPONSE=$(curl -s --max-time 30 -H "Authorization: Bearer ${API_TOKEN}" "${FASTAPI_BASE_URL}/radar/task/${TASK_ID}")
CURL_EXIT=$?
if [ $CURL_EXIT -ne 0 ] || [ -z "$RESPONSE" ]; then
echo "⚠️ 网络异常,正在重试... (${i}/${MAX_RETRIES})"
sleep $RETRY_INTERVAL
continue
fi
STATUS=$(echo "$RESPONSE" | jq -r '.data.status')
PROGRESS=$(echo "$RESPONSE" | jq -r '.data.progress')
if [ "$STATUS" = "completed" ]; then
RESULT=$(echo "$RESPONSE" | jq -r '.data.result')
SOURCE=$(echo "$RESULT" | jq -r '.source')
USER_QUESTION=$(echo "$RESULT" | jq -r '.intent_summary.user_question')
SUMMARY=$(echo "$RESULT" | jq -r '.intent_summary.summary_text')
SUGGESTIONS=$(echo "$RESULT" | jq -r '.intent_summary.suggestions[]? | "\(.title):\(.action // .content)"' 2>/dev/null || echo "$RESULT" | jq -r '.intent_summary.suggestions[]? // empty')
TALKING_POINTS=$(echo "$RESULT" | jq -r '.intent_summary.talking_points[]? // empty')
H5_URL=$(echo "$RESULT" | jq -r '.h5_url')
# 无数据场景
if [ "$SOURCE" = "none" ]; then
echo "⚠️ ${SUMMARY}"
echo ""
echo "💡 可能的原因:"
echo " • 公司名称不准确,请确认后重试"
echo " • 该公司近期无公开动态"
echo " • 该公司为非公开企业,信息较少"
echo ""
echo "请确认公司名称是否正确,或提供更多信息以便精准采集。"
break
fi
echo "🔍 ${USER_QUESTION}"
echo ""
echo "✅ ${SUMMARY}"
echo ""
# 销售建议
if [ -n "$SUGGESTIONS" ]; then
echo "💡 销售建议:"
echo "$SUGGESTIONS" | while IFS= read -r suggestion; do
[ -n "$suggestion" ] && echo "• ${suggestion}"
done
echo ""
fi
# 拜访话题
if [ -n "$TALKING_POINTS" ]; then
echo "🗣️ 拜访话题:"
echo "$TALKING_POINTS" | while IFS= read -r topic; do
[ -n "$topic" ] && echo "• ${topic}"
done
echo ""
fi
echo "📊 [查看完整情报雷达 →](${H5_URL})"
# 客户检查
CHECK_RESULT=$(curl -s --max-time 30 "${FASTAPI_BASE_URL}/customer/check?company_name=$(python3 -c "import urllib.parse; print(urllib.parse.quote('${COMPANY_NAME}'))")" \
-H "Authorization: Bearer ${API_TOKEN}" 2>/dev/null)
if [ $? -eq 0 ] && [ -n "$CHECK_RESULT" ]; then
EXISTS=$(echo "$CHECK_RESULT" | jq -r '.data.exists')
if [ "$EXISTS" = "true" ]; then
# 已添加为客户,不输出任何信息
:
else
echo ""
echo "💡 该公司未添加为客户,回复"添加"可快速添加"
fi
fi
break
elif [ "$STATUS" = "failed" ]; then
ERROR=$(echo "$RESPONSE" | jq -r '.data.error')
echo "❌ 采集失败:${ERROR}"
break
elif [ -n "$PROGRESS" ] && [ "$PROGRESS" != "null" ]; then
echo "🔍 情报雷达采集中... ${PROGRESS}%"
fi
sleep $RETRY_INTERVAL
done
# 轮询超时处理
if [ $i -eq $MAX_RETRIES ]; then
echo ""
echo "⏱️ 采集任务超时(已超过20分钟),请稍后通过以下方式查看结果:"
echo " • 重新发送查询请求"
echo " • 或联系管理员检查任务状态"
fi
GET /radar/task/{task_id}
Authorization: Bearer ${API_TOKEN}
响应:
{
"code": 0,
"data": {
"task_id": "task_xxx",
"status": "pending|processing|completed|failed",
"progress": 50,
"result": {
"company_id": "szyckj_3f7a",
"company_name": "数智云创科技有限公司",
"intent_summary": {
"intent_type": "query_dynamic",
"summary_text": "已为您收集数智云创科技有限公司最近最重要的动态",
"focus_points": ["动态标题1", "动态标题2", "动态标题3"],
"top_dynamics": [...]
},
"h5_url": "http://47.116.49.218:5173/intelligence-radar/szyckj_3f7a?code=xxx"
}
}
}
GET /customer/check?company_name={公司名}
Authorization: Bearer ${API_TOKEN}
响应:
{
"code": 0,
"data": {
"exists": true,
"customer_id": 123,
"company_name": "中软国际(中国)科技有限公司"
}
}
POST /customer/quick-add
Authorization: Bearer ${API_TOKEN}
Content-Type: application/json
{
"company_name": "中软国际(中国)科技有限公司"
}
响应:
{
"code": 0,
"data": {
"customer_id": 124,
"company_name": "中软国际(中国)科技有限公司"
}
}
后端生成换码并返回 H5 链接:
H5_URL = "http://47.116.49.218:5173/intelligence-radar/{company_id}?code={exchange_code}"
换码由后端管理,包含 employee_id 和 intent_summary。
从后端返回的 intent_summary 中提取信息输出,只保留销售最关心的建议和话题:
🔍 {user_question}
✅ {summary_text}
💡 销售建议:
• {suggestion_1}
• {suggestion_2}
🗣️ 拜访话题:
• {talking_point_1}
• {talking_point_2}
📊 [查看完整情报雷达 →]({h5_url})
注意:
user_question 是用户原始问题summary_text 由后端 LLM 根据意图动态生成(如"已为您分析上海外服(集团)有限公司的关键信息"),不是固定格式采集完成后(轮询状态为 completed 时),自动检查该公司是否已添加为客户。
# 检查是否已添加为客户
CHECK_RESULT=$(curl -s --max-time 30 "${FASTAPI_BASE_URL}/customer/check?company_name=$(python3 -c "import urllib.parse; print(urllib.parse.quote('${COMPANY_NAME}'))")" \
-H "Authorization: Bearer ${API_TOKEN}" 2>/dev/null)
if [ $? -eq 0 ] && [ -n "$CHECK_RESULT" ]; then
EXISTS=$(echo "$CHECK_RESULT" | jq -r '.data.exists')
if [ "$EXISTS" = "true" ]; then
# 已添加为客户,不输出任何信息
:
else
# 未添加为客户,提示用户
echo ""
echo "💡 该公司未添加为客户,回复"添加"可快速添加"
# 保存状态,用于识别用户回复"添加"的意图
LAST_COMPANY_NAME="${COMPANY_NAME}"
fi
fi
触发条件:用户输入为"添加",且上一轮输出过"该公司未添加为客户"提示。
处理逻辑:
# 检查是否是添加客户的回复
if [ "${USER_INPUT}" = "添加" ] && [ -n "${LAST_COMPANY_NAME}" ]; then
# 调用快速添加客户 API
ADD_RESULT=$(curl -s --max-time 30 -X POST "${FASTAPI_BASE_URL}/customer/quick-add" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${API_TOKEN}" \
-d "{\"company_name\": \"${LAST_COMPANY_NAME}\"}" 2>/dev/null)
if [ $? -eq 0 ] && [ -n "$ADD_RESULT" ]; then
CUSTOMER_ID=$(echo "$ADD_RESULT" | jq -r '.data.customer_id')
if [ "$CUSTOMER_ID" != "null" ] && [ -n "$CUSTOMER_ID" ]; then
echo "✅ 已添加客户 [${LAST_COMPANY_NAME}]"
else
ERROR_MSG=$(echo "$ADD_RESULT" | jq -r '.message')
echo "❌ 添加客户失败:${ERROR_MSG}"
fi
else
echo "❌ 添加客户失败,请稍后重试"
fi
# 清除状态
LAST_COMPANY_NAME=""
fi
🔍 情报雷达采集中... N% 为用户友好交互,允许输出。示例1:查询动态
用户: "华为最近动态"
→ 提取公司名:华为
→ 输出"🔍 情报雷达正在采集中,请稍候..."
→ [内部] 调用后端采集 → 轮询等待
→ 输出摘要 + H5 链接
→ [内部] 检查客户是否已添加
→ 已添加:不输出
→ 未添加:输出"💡 该公司未添加为客户,回复"添加"可快速添加"
示例2:准备拜访
用户: "我要拜访阿里"
→ 提取公司名:阿里
→ 输出"🔍 情报雷达正在采集中,请稍候..."
→ [内部] 调用后端采集 → 轮询等待
→ 输出摘要 + H5 链接
→ [内部] 检查客户是否已添加
示例3:添加客户
用户: "查询华为"
→ [内部] 完整采集流程
→ 输出摘要 + H5 链接
→ 输出"💡 该公司未添加为客户,回复"添加"可快速添加"
用户: "添加"
→ [内部] 识别为添加客户意图
→ 调用 POST /api/v1/customer/quick-add
→ 输出"✅ 已添加客户 [华为技术有限公司]"
示例4:无数据(公司名错误或信息极少)
用户: "帮我分析华威"(实际想说华为)
→ 提取公司名:华威
→ [内部] 调用后端采集 → LLM 尝试采集 → 无数据
→ 输出"⚠️ 未采集到华威的公开信息"
→ 输出可能原因 + H5 链接
→ 提示用户确认公司名称
本 Skill 执行完成后,不自动触发其他 Skill。所有分析逻辑已内嵌在后端采集流程中。