- Add install_name, install_type, dir_category fields to all 62 plugin.json files to resolve name-mapping and skill-vs-command routing issues - Add install-skills.sh: idempotent cross-machine skill sync script - Routes skill→~/.claude/skills/<name>/, command→~/.claude/commands/<name>.md - rsync full skills/ directory (preserves multi-file skills like dev-test, req-deploy) - State file ~/.claude/.installed-skills.json tracks installed versions - Conflict detection: warns before overwriting locally modified files - --dry-run, --category, --force, --cleanup, --list flags - Add 9 new plugins migrated from local ~/.claude (agent-swarm, ai-chat, defect-analysis, executing-plans, finishing-branch, frontend-design, req-audit, req-lookback, req-retro) - Add update-plugin-meta.py helper used to bulk-update plugin.json - Fix siyuan SKILL.md: remove hardcoded server credentials, use env vars Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
538 lines
15 KiB
Markdown
538 lines
15 KiB
Markdown
---
|
||
name: ai-chat
|
||
description: AI Chat 测试与管理。发送消息测试 AI Chat 工具调用链路,管理工具开关和 Provider 配置,支持 local/staging 环境切换。
|
||
arguments: <subcommand> [args]
|
||
---
|
||
|
||
# AI Chat Skill
|
||
|
||
测试和管理 Coolbuy PaaS AI Chat 服务的 Claude Code skill。
|
||
|
||
## Quick Reference
|
||
|
||
| 命令 | 用途 |
|
||
|------|------|
|
||
| `/ai-chat send <message>` | 发送消息到 AI Chat,实时显示工具调用 + AI 回复 |
|
||
| `/ai-chat env [local\|staging]` | 切换/查看目标环境(默认 local) |
|
||
| `/ai-chat tools [category]` | 列出当前环境已注册的工具 |
|
||
| `/ai-chat config` | 查看 AI 配置(Provider、工具开关等) |
|
||
| `/ai-chat history` | 显示本次会话的历史消息 |
|
||
|
||
---
|
||
|
||
## Environment Config
|
||
|
||
两套环境,通过 `/ai-chat env` 切换:
|
||
|
||
| 环境 | Auth URL | AI URL | 登录账号 |
|
||
|------|----------|--------|----------|
|
||
| **local** (默认) | `http://localhost:7089` | `http://localhost:7092` | lining_admin / admin123 |
|
||
| **staging** | `http://39.105.150.219:7089` | `http://39.105.150.219:7092` | lining_admin / admin123 |
|
||
|
||
### 状态文件
|
||
|
||
环境状态保存在 `/tmp/ai-chat-state.json`,格式:
|
||
|
||
```json
|
||
{
|
||
"env": "local",
|
||
"token": "eyJ...",
|
||
"token_env": "local",
|
||
"history": []
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Commands
|
||
|
||
### /ai-chat env
|
||
|
||
**切换或查看当前环境。**
|
||
|
||
用法:
|
||
- `/ai-chat env` — 显示当前环境
|
||
- `/ai-chat env local` — 切换到本地环境
|
||
- `/ai-chat env staging` — 切换到 staging 环境
|
||
|
||
实现步骤:
|
||
|
||
1. 读取 `/tmp/ai-chat-state.json`(不存在则默认 `{"env":"local","history":[]}`)
|
||
2. 如果提供了参数,更新 `env` 字段并清空 `token`(环境变了 token 失效)
|
||
3. 写回状态文件
|
||
4. 输出当前环境信息表格
|
||
|
||
---
|
||
|
||
### /ai-chat send
|
||
|
||
**发送消息到 AI Chat 并实时显示流式响应。**
|
||
|
||
用法:`/ai-chat send <message>`
|
||
|
||
实现步骤:
|
||
|
||
#### Step 1: 读取状态
|
||
|
||
```bash
|
||
# 读取状态文件
|
||
cat /tmp/ai-chat-state.json 2>/dev/null || echo '{"env":"local","history":[]}'
|
||
```
|
||
|
||
确定环境变量:
|
||
- **local**: `AUTH_URL=http://localhost:7089`, `AI_URL=http://localhost:7092`
|
||
- **staging**: `AUTH_URL=http://39.105.150.219:7089`, `AI_URL=http://39.105.150.219:7092`
|
||
|
||
#### Step 2: 获取 Token
|
||
|
||
如果状态文件中没有 token 或 `token_env` 与当前 `env` 不匹配,执行登录:
|
||
|
||
```bash
|
||
curl -s -X POST "$AUTH_URL/api/v1/auth/login" \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"username":"lining_admin","password":"admin123"}'
|
||
```
|
||
|
||
从响应中提取 `access_token`:
|
||
|
||
```bash
|
||
# 响应格式
|
||
# {"access_token":"eyJ...","refresh_token":"...","token_type":"Bearer","expires_in":7200,"user_info":{...}}
|
||
```
|
||
|
||
用 `python3 -c "import json,sys; print(json.load(sys.stdin)['access_token'])"` 提取 token。
|
||
|
||
将 token 和 token_env 保存到状态文件。
|
||
|
||
#### Step 3: 构造请求并发送 SSE 流
|
||
|
||
```bash
|
||
curl -s -N -X POST "$AI_URL/api/v1/ai/chat/stream" \
|
||
-H "Authorization: Bearer $TOKEN" \
|
||
-H "Content-Type: application/json" \
|
||
-d "{\"message\":\"$MSG\",\"history\":$HISTORY}" 2>&1
|
||
```
|
||
|
||
**重要**: `history` 字段传入之前的会话历史(从状态文件读取),实现多轮对话。
|
||
|
||
#### Step 4: 解析 SSE 事件
|
||
|
||
用 Python 脚本解析 SSE 流(比 bash while read 更可靠):
|
||
|
||
```python
|
||
#!/usr/bin/env python3
|
||
"""解析 AI Chat SSE 流并格式化输出"""
|
||
import sys, json
|
||
|
||
full_content = ""
|
||
tool_calls = []
|
||
|
||
for line in sys.stdin:
|
||
line = line.strip()
|
||
if not line.startswith("data:"):
|
||
continue
|
||
|
||
data_str = line[5:].strip()
|
||
if not data_str:
|
||
continue
|
||
|
||
try:
|
||
event = json.loads(data_str)
|
||
except json.JSONDecodeError:
|
||
continue
|
||
|
||
evt_type = event.get("type", "")
|
||
|
||
if evt_type == "content":
|
||
chunk = event.get("content", "")
|
||
full_content += chunk
|
||
# 实时输出内容片段
|
||
sys.stdout.write(chunk)
|
||
sys.stdout.flush()
|
||
|
||
elif evt_type == "tool_call":
|
||
tc = event.get("tool_call", {})
|
||
tool_name = tc.get("name", "unknown")
|
||
tool_args = tc.get("arguments", {})
|
||
tool_id = tc.get("id", "")
|
||
tool_calls.append({"id": tool_id, "name": tool_name})
|
||
# 输出工具调用标记
|
||
args_str = json.dumps(tool_args, ensure_ascii=False)
|
||
if len(args_str) > 200:
|
||
args_str = args_str[:200] + "..."
|
||
print(f"\n🔧 Tool Call: {tool_name}", file=sys.stderr)
|
||
print(f" Args: {args_str}", file=sys.stderr)
|
||
|
||
elif evt_type == "tool_result":
|
||
tr = event.get("tool_result", {})
|
||
tool_name = tr.get("name", "unknown")
|
||
content = tr.get("content", "")
|
||
is_error = tr.get("is_error", False)
|
||
# 截断长结果
|
||
if len(content) > 500:
|
||
content = content[:500] + f"... ({len(content)} chars total)"
|
||
status = "❌ Error" if is_error else "✅ Result"
|
||
print(f" {status} [{tool_name}]: {content}", file=sys.stderr)
|
||
|
||
elif evt_type == "done":
|
||
usage = event.get("usage") or {}
|
||
prompt_t = usage.get("prompt_tokens", 0)
|
||
completion_t = usage.get("completion_tokens", 0)
|
||
total_t = usage.get("total_tokens", 0)
|
||
print(f"\n\n--- Done ---", file=sys.stderr)
|
||
if total_t > 0:
|
||
print(f"Tokens: {prompt_t} prompt + {completion_t} completion = {total_t} total", file=sys.stderr)
|
||
if tool_calls:
|
||
print(f"Tool calls: {len(tool_calls)} ({', '.join(tc['name'] for tc in tool_calls)})", file=sys.stderr)
|
||
|
||
elif evt_type == "error":
|
||
err = event.get("error", "unknown error")
|
||
print(f"\n❌ Error: {err}", file=sys.stderr)
|
||
|
||
# 输出换行
|
||
print()
|
||
# 将 full_content 输出到 fd 3 用于状态更新(如果 fd 3 打开)
|
||
try:
|
||
with open("/tmp/ai-chat-response.txt", "w") as f:
|
||
f.write(full_content)
|
||
except:
|
||
pass
|
||
```
|
||
|
||
#### Step 5: 更新会话历史
|
||
|
||
发送完成后,将用户消息和 AI 回复追加到状态文件的 `history` 数组中:
|
||
|
||
```json
|
||
[
|
||
{"role": "user", "content": "<用户消息>"},
|
||
{"role": "assistant", "content": "<AI 完整回复>"}
|
||
]
|
||
```
|
||
|
||
#### 完整 bash 执行流程
|
||
|
||
```bash
|
||
#!/usr/bin/env bash
|
||
set -euo pipefail
|
||
|
||
MSG="$1"
|
||
STATE_FILE="/tmp/ai-chat-state.json"
|
||
|
||
# 1. 读取状态
|
||
if [ -f "$STATE_FILE" ]; then
|
||
STATE=$(cat "$STATE_FILE")
|
||
else
|
||
STATE='{"env":"local","history":[]}'
|
||
fi
|
||
|
||
ENV=$(echo "$STATE" | python3 -c "import json,sys; print(json.load(sys.stdin).get('env','local'))")
|
||
TOKEN=$(echo "$STATE" | python3 -c "import json,sys; print(json.load(sys.stdin).get('token',''))")
|
||
TOKEN_ENV=$(echo "$STATE" | python3 -c "import json,sys; print(json.load(sys.stdin).get('token_env',''))")
|
||
HISTORY=$(echo "$STATE" | python3 -c "import json,sys; print(json.dumps(json.load(sys.stdin).get('history',[])))")
|
||
|
||
# 2. 确定 URL
|
||
if [ "$ENV" = "staging" ]; then
|
||
AUTH_URL="http://39.105.150.219:7089"
|
||
AI_URL="http://39.105.150.219:7092"
|
||
else
|
||
AUTH_URL="http://localhost:7089"
|
||
AI_URL="http://localhost:7092"
|
||
fi
|
||
|
||
# 3. 获取 token(如果需要)
|
||
if [ -z "$TOKEN" ] || [ "$TOKEN_ENV" != "$ENV" ]; then
|
||
echo "🔐 Logging in to $ENV environment..."
|
||
LOGIN_RESP=$(curl -s -X POST "$AUTH_URL/api/v1/auth/login" \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"username":"lining_admin","password":"admin123"}')
|
||
|
||
TOKEN=$(echo "$LOGIN_RESP" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('access_token',d.get('data',{}).get('access_token','')))")
|
||
|
||
if [ -z "$TOKEN" ]; then
|
||
echo "❌ Login failed: $LOGIN_RESP"
|
||
exit 1
|
||
fi
|
||
echo "✅ Login successful"
|
||
|
||
# 更新状态中的 token
|
||
STATE=$(echo "$STATE" | python3 -c "
|
||
import json,sys
|
||
s=json.load(sys.stdin)
|
||
s['token']='$TOKEN'
|
||
s['token_env']='$ENV'
|
||
print(json.dumps(s,ensure_ascii=False))
|
||
")
|
||
fi
|
||
|
||
# 4. 发送 SSE 请求并解析
|
||
echo ""
|
||
echo "📤 Sending to $ENV: $MSG"
|
||
echo "---"
|
||
|
||
# 转义消息中的特殊字符
|
||
MSG_JSON=$(python3 -c "import json; print(json.dumps('$MSG'))")
|
||
|
||
curl -s -N -X POST "$AI_URL/api/v1/ai/chat/stream" \
|
||
-H "Authorization: Bearer $TOKEN" \
|
||
-H "Content-Type: application/json" \
|
||
-d "{\"message\":$MSG_JSON,\"history\":$HISTORY}" 2>&1 | python3 -c "
|
||
import sys, json
|
||
|
||
full_content = ''
|
||
tool_calls = []
|
||
|
||
for line in sys.stdin:
|
||
line = line.strip()
|
||
if not line.startswith('data:'):
|
||
continue
|
||
data_str = line[5:].strip()
|
||
if not data_str:
|
||
continue
|
||
try:
|
||
event = json.loads(data_str)
|
||
except json.JSONDecodeError:
|
||
continue
|
||
|
||
evt_type = event.get('type', '')
|
||
|
||
if evt_type == 'content':
|
||
chunk = event.get('content', '')
|
||
full_content += chunk
|
||
sys.stdout.write(chunk)
|
||
sys.stdout.flush()
|
||
elif evt_type == 'tool_call':
|
||
tc = event.get('tool_call', {})
|
||
tool_name = tc.get('name', 'unknown')
|
||
tool_args = tc.get('arguments', {})
|
||
tool_calls.append({'name': tool_name})
|
||
args_str = json.dumps(tool_args, ensure_ascii=False)
|
||
if len(args_str) > 200:
|
||
args_str = args_str[:200] + '...'
|
||
print(f'\n🔧 Tool Call: {tool_name}', file=sys.stderr)
|
||
print(f' Args: {args_str}', file=sys.stderr)
|
||
elif evt_type == 'tool_result':
|
||
tr = event.get('tool_result', {})
|
||
tool_name = tr.get('name', 'unknown')
|
||
content = tr.get('content', '')
|
||
is_error = tr.get('is_error', False)
|
||
if len(content) > 500:
|
||
content = content[:500] + f'... ({len(content)} chars total)'
|
||
status = '❌' if is_error else '✅'
|
||
print(f' {status} [{tool_name}]: {content}', file=sys.stderr)
|
||
elif evt_type == 'done':
|
||
usage = event.get('usage') or {}
|
||
pt = usage.get('prompt_tokens', 0)
|
||
ct = usage.get('completion_tokens', 0)
|
||
tt = usage.get('total_tokens', 0)
|
||
print(f'\n\n--- Done ---', file=sys.stderr)
|
||
if tt > 0:
|
||
print(f'Tokens: {pt} prompt + {ct} completion = {tt} total', file=sys.stderr)
|
||
if tool_calls:
|
||
names = ', '.join(tc['name'] for tc in tool_calls)
|
||
print(f'Tool calls: {len(tool_calls)} ({names})', file=sys.stderr)
|
||
elif evt_type == 'error':
|
||
err = event.get('error', 'unknown error')
|
||
print(f'\n❌ Error: {err}', file=sys.stderr)
|
||
|
||
print()
|
||
with open('/tmp/ai-chat-response.txt', 'w') as f:
|
||
f.write(full_content)
|
||
"
|
||
|
||
# 5. 更新历史
|
||
RESPONSE=$(cat /tmp/ai-chat-response.txt 2>/dev/null || echo "")
|
||
python3 -c "
|
||
import json
|
||
state_file = '$STATE_FILE'
|
||
try:
|
||
with open(state_file) as f:
|
||
state = json.load(f)
|
||
except:
|
||
state = {'env': 'local', 'history': []}
|
||
|
||
msg = $MSG_JSON
|
||
resp = '''$RESPONSE'''
|
||
|
||
state['history'].append({'role': 'user', 'content': msg})
|
||
if resp:
|
||
state['history'].append({'role': 'assistant', 'content': resp})
|
||
state['token'] = '$TOKEN'
|
||
state['token_env'] = '$ENV'
|
||
|
||
with open(state_file, 'w') as f:
|
||
json.dump(state, f, ensure_ascii=False, indent=2)
|
||
"
|
||
|
||
echo ""
|
||
echo "💬 History: $(echo "$STATE" | python3 -c "import json,sys; h=json.load(sys.stdin).get('history',[]); print(len(h)//2 + 1)") messages in session"
|
||
```
|
||
|
||
**重要注意事项**:
|
||
- 上面的脚本是逻辑参考,**不要**原样执行。Claude Code 应按步骤逐一执行 bash 命令。
|
||
- 消息中的引号和特殊字符需要用 python3 json.dumps 转义。
|
||
- 如果 token 过期(401 响应),自动重新登录。
|
||
- SSE 超时设置 `--max-time 120`。
|
||
|
||
---
|
||
|
||
### /ai-chat tools
|
||
|
||
**列出当前环境已注册的工具。**
|
||
|
||
用法:
|
||
- `/ai-chat tools` — 列出所有工具
|
||
- `/ai-chat tools <category>` — 按分类过滤
|
||
|
||
实现步骤:
|
||
|
||
1. 读取状态获取环境和 token(必要时先登录)
|
||
2. 发送请求:
|
||
|
||
```bash
|
||
# 列出所有工具
|
||
curl -s "$AI_URL/api/v1/ai/tools" \
|
||
-H "Authorization: Bearer $TOKEN"
|
||
|
||
# 按分类过滤
|
||
curl -s "$AI_URL/api/v1/ai/tools?category=order" \
|
||
-H "Authorization: Bearer $TOKEN"
|
||
```
|
||
|
||
3. 响应格式:
|
||
|
||
```json
|
||
{
|
||
"tools": [
|
||
{
|
||
"name": "list_orders",
|
||
"description": "查询订单列表",
|
||
"category": "order",
|
||
"enabled": true,
|
||
"parameters": {...}
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
4. 按 category 分组,输出表格:
|
||
|
||
```
|
||
📋 AI Tools (142 total)
|
||
|
||
order (15 tools)
|
||
├── list_orders 查询订单列表
|
||
├── get_order_detail 获取订单详情
|
||
└── ...
|
||
|
||
product (12 tools)
|
||
├── list_products 查询商品列表
|
||
└── ...
|
||
```
|
||
|
||
已知工具分类:order, product, sku, inventory, task, brand, requirement, customer, dashboard, distribution, finance, discount, channel, approval, organization, feature_gap
|
||
|
||
---
|
||
|
||
### /ai-chat config
|
||
|
||
**查看 AI 服务配置。**
|
||
|
||
实现步骤:
|
||
|
||
1. 根据当前环境读取对应配置文件:
|
||
- **local**: 读取 `ai-service/api/etc/ai-api-local.yaml`
|
||
- **staging**: SSH 到 staging 读取 `/opt/coolbuy/configs/ai-api.yaml`
|
||
|
||
2. 显示关键配置项:
|
||
- AI Provider 和 Model
|
||
- 各工具分类的开关状态
|
||
- API 端口
|
||
|
||
3. 输出格式:
|
||
|
||
```
|
||
⚙️ AI Config (local)
|
||
|
||
Provider: deepseek
|
||
Model: deepseek-chat
|
||
Port: 7092
|
||
|
||
Tool Categories:
|
||
✅ order ✅ product ✅ sku
|
||
✅ inventory ✅ task ✅ brand
|
||
✅ requirement ✅ customer ✅ dashboard
|
||
✅ distribution ✅ finance ✅ discount
|
||
✅ channel ✅ approval ✅ organization
|
||
✅ feature_gap
|
||
```
|
||
|
||
---
|
||
|
||
### /ai-chat history
|
||
|
||
**显示当前会话的历史消息。**
|
||
|
||
实现步骤:
|
||
|
||
1. 读取 `/tmp/ai-chat-state.json` 的 `history` 数组
|
||
2. 按时间顺序显示:
|
||
|
||
```
|
||
💬 Chat History (3 messages)
|
||
|
||
[1] 👤 User: 你好
|
||
🤖 AI: 你好!我是 AI 助手...
|
||
|
||
[2] 👤 User: 搜索组织架构找到大客户部
|
||
🤖 AI: 我来帮你搜索... (used: search_organizations)
|
||
|
||
[3] 👤 User: 最近的订单
|
||
🤖 AI: 以下是最近的订单列表...
|
||
```
|
||
|
||
3. 如果需要清空历史:`/ai-chat history clear`
|
||
- 删除状态文件中的 history 数组,重置为空
|
||
|
||
---
|
||
|
||
## SSE Event Format Reference
|
||
|
||
AI Chat SSE 流使用 `event: message` + `data: {json}` 格式:
|
||
|
||
| type | 数据字段 | 说明 |
|
||
|------|---------|------|
|
||
| `content` | `content: "<text>"` | 增量文本内容 |
|
||
| `tool_call` | `tool_call: {id, name, arguments}` | AI 请求调用工具 |
|
||
| `tool_result` | `tool_result: {tool_call_id, name, content, is_error}` | 工具执行结果 |
|
||
| `done` | `usage: {prompt_tokens, completion_tokens, total_tokens}` (可能为 null), `finish_reason` | 流结束 |
|
||
| `error` | `error: "<message>"` | 错误 |
|
||
|
||
---
|
||
|
||
## Troubleshooting
|
||
|
||
### Token 过期 (401)
|
||
如果请求返回 401,删除缓存 token 重新登录:
|
||
```bash
|
||
# 清除 token 强制重新登录
|
||
python3 -c "
|
||
import json
|
||
with open('/tmp/ai-chat-state.json') as f: s=json.load(f)
|
||
s['token']=''
|
||
with open('/tmp/ai-chat-state.json','w') as f: json.dump(s,f)
|
||
"
|
||
```
|
||
|
||
### 连接失败
|
||
- **local**: 确认本地服务已启动 (`./scripts/start_dev.sh`)
|
||
- **staging**: 确认 staging 服务运行中 (`ssh coolbuy-staging "docker ps | grep ai-service"`)
|
||
|
||
### SSE 流中断
|
||
- 检查 AI 服务日志
|
||
- local: 查看终端输出
|
||
- staging: `ssh coolbuy-staging "docker logs coolbuy-ai-service --tail 50"`
|
||
|
||
### 消息中包含特殊字符
|
||
务必用 `python3 -c "import json; print(json.dumps(msg))"` 转义消息内容,避免 JSON 解析失败。
|