@bluevs/vhcli 0.5.7 → 0.5.9
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/package.json +9 -8
- package/skill/SKILL.md +383 -0
- package/skill/docs/agent.md +43 -0
- package/skill/docs/auth.md +47 -0
- package/skill/docs/company.md +47 -0
- package/skill/docs/google.md +34 -0
- package/skill/docs/material-report.md +191 -0
- package/skill/docs/project.md +435 -0
- package/skill/docs/tiktok.md +749 -0
- package/skill/scripts/install-skill.sh +126 -0
- package/skill/scripts/vh-auth-card.cjs +228 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# install-skill.sh — vhcli + skill 双向依赖检测与安装
|
|
3
|
+
#
|
|
4
|
+
# 两种入口场景:
|
|
5
|
+
# A) 已安装 skill,检测 vhcli CLI 是否就绪
|
|
6
|
+
# B) 已安装 vhcli CLI,检测 skill 是否同步到 workspace
|
|
7
|
+
#
|
|
8
|
+
# 用法: bash skills/vhcli/scripts/install-skill.sh
|
|
9
|
+
|
|
10
|
+
set -euo pipefail
|
|
11
|
+
|
|
12
|
+
WORKSPACE="${OPENCLAW_WORKSPACE:-$HOME/.openclaw/workspace}"
|
|
13
|
+
SKILL_DIR="$WORKSPACE/skills/vhcli"
|
|
14
|
+
NPM_ROOT="$(npm root -g 2>/dev/null || echo '')"
|
|
15
|
+
PKG_DIR="${NPM_ROOT:+$NPM_ROOT/@bluevs/vhcli}"
|
|
16
|
+
PKG_SKILL_DIR="${PKG_DIR:+$PKG_DIR/skill}"
|
|
17
|
+
VERSION_FILE="$SKILL_DIR/.installed_version"
|
|
18
|
+
|
|
19
|
+
# ============================================================
|
|
20
|
+
# 方向 A:skill 已存在 → 确保 vhcli CLI 可用
|
|
21
|
+
# ============================================================
|
|
22
|
+
check_cli() {
|
|
23
|
+
if command -v vhcli &>/dev/null; then
|
|
24
|
+
echo "✅ vhcli CLI 已安装 ($(vhcli --help 2>&1 | head -1 || echo 'ok'))"
|
|
25
|
+
return 0
|
|
26
|
+
else
|
|
27
|
+
echo "⚠️ vhcli CLI 未安装"
|
|
28
|
+
echo " 请执行: npm install -g @bluevs/vhcli"
|
|
29
|
+
echo ""
|
|
30
|
+
echo " 自动安装? (执行中...)"
|
|
31
|
+
npm install -g @bluevs/vhcli 2>&1 | tail -3
|
|
32
|
+
if command -v vhcli &>/dev/null; then
|
|
33
|
+
echo "✅ vhcli CLI 安装成功"
|
|
34
|
+
# 安装后刷新路径
|
|
35
|
+
NPM_ROOT="$(npm root -g 2>/dev/null || echo '')"
|
|
36
|
+
PKG_DIR="${NPM_ROOT:+$NPM_ROOT/@bluevs/vhcli}"
|
|
37
|
+
PKG_SKILL_DIR="${PKG_DIR:+$PKG_DIR/skill}"
|
|
38
|
+
return 0
|
|
39
|
+
else
|
|
40
|
+
echo "❌ 安装失败,请手动执行: npm install -g @bluevs/vhcli"
|
|
41
|
+
return 1
|
|
42
|
+
fi
|
|
43
|
+
fi
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# ============================================================
|
|
47
|
+
# 方向 B:vhcli CLI 已存在 → 确保 skill 同步到 workspace
|
|
48
|
+
# ============================================================
|
|
49
|
+
sync_skill() {
|
|
50
|
+
# 检查 npm 包是否含 skill 目录
|
|
51
|
+
if [ -z "$PKG_SKILL_DIR" ] || [ ! -d "$PKG_SKILL_DIR" ]; then
|
|
52
|
+
echo "ℹ️ 当前 vhcli 版本未包含 skill/ 目录,跳过自动同步"
|
|
53
|
+
# 检查本地 skill 是否已存在(手动维护场景)
|
|
54
|
+
if [ -f "$SKILL_DIR/SKILL.md" ]; then
|
|
55
|
+
echo "✅ 本地 skill 已存在(手动维护模式)"
|
|
56
|
+
else
|
|
57
|
+
echo "⚠️ 本地 skill 不存在,且 npm 包无 skill 目录"
|
|
58
|
+
echo " 请手动将 skill 文件放入: $SKILL_DIR/"
|
|
59
|
+
fi
|
|
60
|
+
return 0
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
# 获取 npm 包版本
|
|
64
|
+
PKG_VERSION=$(node -p "require('$PKG_DIR/package.json').version" 2>/dev/null || echo "unknown")
|
|
65
|
+
|
|
66
|
+
# 检查本地版本
|
|
67
|
+
if [ -f "$VERSION_FILE" ]; then
|
|
68
|
+
LOCAL_VERSION=$(cat "$VERSION_FILE")
|
|
69
|
+
if [ "$LOCAL_VERSION" = "$PKG_VERSION" ]; then
|
|
70
|
+
echo "✅ vhcli skill 已是最新版本 ($PKG_VERSION),跳过同步"
|
|
71
|
+
return 0
|
|
72
|
+
fi
|
|
73
|
+
echo "🔄 升级 vhcli skill: $LOCAL_VERSION → $PKG_VERSION"
|
|
74
|
+
else
|
|
75
|
+
if [ -f "$SKILL_DIR/SKILL.md" ]; then
|
|
76
|
+
echo "🔄 同步 npm skill v$PKG_VERSION → 本地 (首次版本标记)"
|
|
77
|
+
else
|
|
78
|
+
echo "📦 首次安装 vhcli skill v$PKG_VERSION (from npm)"
|
|
79
|
+
fi
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
# 同步
|
|
83
|
+
mkdir -p "$SKILL_DIR"
|
|
84
|
+
|
|
85
|
+
# SKILL.md + docs/ → 直接覆盖(取最新文档)
|
|
86
|
+
cp -f "$PKG_SKILL_DIR/SKILL.md" "$SKILL_DIR/SKILL.md"
|
|
87
|
+
if [ -d "$PKG_SKILL_DIR/docs" ]; then
|
|
88
|
+
mkdir -p "$SKILL_DIR/docs"
|
|
89
|
+
cp -f "$PKG_SKILL_DIR/docs/"*.md "$SKILL_DIR/docs/" 2>/dev/null || true
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
# scripts/ → 只添加新文件,不覆盖本地定制
|
|
93
|
+
if [ -d "$PKG_SKILL_DIR/scripts" ]; then
|
|
94
|
+
mkdir -p "$SKILL_DIR/scripts"
|
|
95
|
+
for f in "$PKG_SKILL_DIR/scripts/"*; do
|
|
96
|
+
[ -f "$f" ] || continue
|
|
97
|
+
fname=$(basename "$f")
|
|
98
|
+
if [ ! -f "$SKILL_DIR/scripts/$fname" ]; then
|
|
99
|
+
cp "$f" "$SKILL_DIR/scripts/$fname"
|
|
100
|
+
echo " + scripts/$fname (new)"
|
|
101
|
+
else
|
|
102
|
+
echo " ~ scripts/$fname (kept local version)"
|
|
103
|
+
fi
|
|
104
|
+
done
|
|
105
|
+
fi
|
|
106
|
+
|
|
107
|
+
# 记录版本
|
|
108
|
+
echo "$PKG_VERSION" > "$VERSION_FILE"
|
|
109
|
+
echo "✅ vhcli skill v$PKG_VERSION 同步完成"
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
# ============================================================
|
|
113
|
+
# 主流程:双向检测
|
|
114
|
+
# ============================================================
|
|
115
|
+
echo "=== VH Skill 环境检测 ==="
|
|
116
|
+
echo ""
|
|
117
|
+
|
|
118
|
+
# A: 确保 CLI 可用
|
|
119
|
+
check_cli || exit 1
|
|
120
|
+
echo ""
|
|
121
|
+
|
|
122
|
+
# B: 确保 skill 同步
|
|
123
|
+
sync_skill
|
|
124
|
+
|
|
125
|
+
echo ""
|
|
126
|
+
echo "=== 检测完成 ==="
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* vh-auth-card.cjs — AI营销平台授权飞书交互卡片
|
|
4
|
+
*
|
|
5
|
+
* 用法:
|
|
6
|
+
* node vh-auth-card.cjs [target]
|
|
7
|
+
*
|
|
8
|
+
* target: 飞书 open_id (ou_xxx) 或 chat_id (oc_xxx)
|
|
9
|
+
*
|
|
10
|
+
* 流程:
|
|
11
|
+
* 1. spawn vhcli auth login → 解析 device_code + 授权 URL
|
|
12
|
+
* 2. 发送蓝色飞书卡片(带按钮 + 验证码)
|
|
13
|
+
* 3. 等待 auth login 子进程退出
|
|
14
|
+
* - exit 0 → 授权成功 → patch 绿卡
|
|
15
|
+
* - exit 非0 → 授权失败 → patch 红卡
|
|
16
|
+
* - 超时 120s → patch 橙卡
|
|
17
|
+
*/
|
|
18
|
+
const { spawn, execSync } = require('node:child_process');
|
|
19
|
+
const { readFileSync } = require('node:fs');
|
|
20
|
+
|
|
21
|
+
// --- 飞书 SDK ---
|
|
22
|
+
const larkSdkPath = (() => {
|
|
23
|
+
const paths = [
|
|
24
|
+
'/root/.openclaw/npm/node_modules/@larksuiteoapi/node-sdk',
|
|
25
|
+
'/app/dist/extensions/feishu/node_modules/@larksuiteoapi/node-sdk',
|
|
26
|
+
];
|
|
27
|
+
for (const p of paths) {
|
|
28
|
+
try { require.resolve(p); return p; } catch {}
|
|
29
|
+
}
|
|
30
|
+
throw new Error('Cannot find @larksuiteoapi/node-sdk');
|
|
31
|
+
})();
|
|
32
|
+
const lark = require(larkSdkPath);
|
|
33
|
+
|
|
34
|
+
// --- 配置 ---
|
|
35
|
+
function loadCreds() {
|
|
36
|
+
const cfg = JSON.parse(readFileSync(process.env.HOME + '/.openclaw/openclaw.json', 'utf8'));
|
|
37
|
+
const f = cfg.channels.feishu;
|
|
38
|
+
return { appId: f.appId, appSecret: f.appSecret };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// --- 卡片构建 ---
|
|
42
|
+
function buildCard({ title, headerTpl, status, code, url }) {
|
|
43
|
+
const elements = [{ tag: 'markdown', content: status }];
|
|
44
|
+
if (code) elements.push({ tag: 'markdown', content: `**验证码:** \`${code}\`` });
|
|
45
|
+
if (url) elements.push({
|
|
46
|
+
tag: 'button',
|
|
47
|
+
text: { tag: 'plain_text', content: '🔗 打开授权页面' },
|
|
48
|
+
type: 'primary',
|
|
49
|
+
behaviors: [{ type: 'open_url', default_url: url, pc_url: url, ios_url: url, android_url: url }],
|
|
50
|
+
});
|
|
51
|
+
return {
|
|
52
|
+
schema: '2.0',
|
|
53
|
+
config: { update_multi: true },
|
|
54
|
+
header: { title: { tag: 'plain_text', content: title }, template: headerTpl },
|
|
55
|
+
body: { elements },
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// --- Target 解析 ---
|
|
60
|
+
function resolveTarget() {
|
|
61
|
+
// 优先级:argv > VH_AUTH_TARGET > OPENCLAW_CHAT_ID
|
|
62
|
+
const raw = process.argv[2] || process.env.VH_AUTH_TARGET || process.env.OPENCLAW_CHAT_ID;
|
|
63
|
+
if (!raw) return null;
|
|
64
|
+
|
|
65
|
+
// 处理各种格式
|
|
66
|
+
if (raw.startsWith('oc_')) {
|
|
67
|
+
return { id: raw, type: 'chat_id' };
|
|
68
|
+
}
|
|
69
|
+
if (raw.startsWith('user:')) {
|
|
70
|
+
// 私聊 chat_id 格式 "user:ou_xxx" → 取 open_id
|
|
71
|
+
return { id: raw.slice(5), type: 'open_id' };
|
|
72
|
+
}
|
|
73
|
+
if (raw.startsWith('ou_')) {
|
|
74
|
+
return { id: raw, type: 'open_id' };
|
|
75
|
+
}
|
|
76
|
+
// 其他格式尝试作为 chat_id
|
|
77
|
+
return { id: raw, type: 'chat_id' };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// --- 主流程 ---
|
|
81
|
+
async function main() {
|
|
82
|
+
const resolved = resolveTarget();
|
|
83
|
+
if (!resolved) {
|
|
84
|
+
console.error('Usage: node vh-auth-card.cjs [target]');
|
|
85
|
+
console.error(' target: open_id(ou_xxx) / chat_id(oc_xxx) / user:ou_xxx');
|
|
86
|
+
console.error(' Also reads from env: VH_AUTH_TARGET or OPENCLAW_CHAT_ID');
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const target = resolved.id;
|
|
91
|
+
const toType = resolved.type;
|
|
92
|
+
const client = new lark.Client({ ...loadCreds(), disableTokenCache: false });
|
|
93
|
+
const TIMEOUT_MS = 120000;
|
|
94
|
+
|
|
95
|
+
// Step 1: spawn vhcli auth login, 解析输出
|
|
96
|
+
console.log('Starting vhcli auth login...');
|
|
97
|
+
|
|
98
|
+
const { url, code, exitPromise, child } = await new Promise((resolve, reject) => {
|
|
99
|
+
const child = spawn('vhcli', ['auth', 'login'], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
100
|
+
let output = '';
|
|
101
|
+
let resolved = false;
|
|
102
|
+
|
|
103
|
+
const onData = (chunk) => {
|
|
104
|
+
output += chunk.toString();
|
|
105
|
+
// 尝试解析 URL 和 code
|
|
106
|
+
const urlMatch = output.match(/https?:\/\/[^\s]+/);
|
|
107
|
+
const codeMatch = output.match(/Your code:\s*(\S+)/i) || output.match(/code[::]\s*(\S+)/i);
|
|
108
|
+
if (urlMatch && codeMatch && !resolved) {
|
|
109
|
+
resolved = true;
|
|
110
|
+
const exitPromise = new Promise(res => child.on('close', res));
|
|
111
|
+
resolve({ url: urlMatch[0], code: codeMatch[1], exitPromise, child });
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
child.stdout.on('data', onData);
|
|
116
|
+
child.stderr.on('data', onData);
|
|
117
|
+
|
|
118
|
+
child.on('close', (exitCode) => {
|
|
119
|
+
if (!resolved) {
|
|
120
|
+
reject(new Error(`vhcli auth login exited (code ${exitCode}) before outputting auth info:\n${output}`));
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// 5s 内没解析出来就失败
|
|
125
|
+
setTimeout(() => {
|
|
126
|
+
if (!resolved) {
|
|
127
|
+
child.kill();
|
|
128
|
+
reject(new Error('Timeout parsing auth login output:\n' + output));
|
|
129
|
+
}
|
|
130
|
+
}, 10000);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
console.log(`URL: ${url}`);
|
|
134
|
+
console.log(`Code: ${code}`);
|
|
135
|
+
|
|
136
|
+
// Step 2: 先发文字通知
|
|
137
|
+
await client.im.message.create({
|
|
138
|
+
params: { receive_id_type: toType },
|
|
139
|
+
data: {
|
|
140
|
+
receive_id: target,
|
|
141
|
+
msg_type: 'text',
|
|
142
|
+
content: JSON.stringify({ text: '请先完成AI营销平台授权' }),
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
console.log('Text sent');
|
|
146
|
+
|
|
147
|
+
// Step 3: 发蓝色卡片
|
|
148
|
+
const resp = await client.im.message.create({
|
|
149
|
+
params: { receive_id_type: toType },
|
|
150
|
+
data: {
|
|
151
|
+
receive_id: target,
|
|
152
|
+
msg_type: 'interactive',
|
|
153
|
+
content: JSON.stringify(buildCard({
|
|
154
|
+
title: '🔐 AI营销平台授权',
|
|
155
|
+
headerTpl: 'blue',
|
|
156
|
+
status: '⏳ 请点击下方按钮完成授权',
|
|
157
|
+
code,
|
|
158
|
+
url,
|
|
159
|
+
})),
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const messageId = resp.data.message_id;
|
|
164
|
+
console.log('Card sent: ' + messageId);
|
|
165
|
+
|
|
166
|
+
// Step 3: 等待 auth login 子进程退出 or 超时
|
|
167
|
+
const timeoutPromise = new Promise(res => setTimeout(() => res('TIMEOUT'), TIMEOUT_MS));
|
|
168
|
+
const result = await Promise.race([
|
|
169
|
+
exitPromise.then(code => code === 0 ? 'SUCCESS' : 'FAILED'),
|
|
170
|
+
timeoutPromise,
|
|
171
|
+
]);
|
|
172
|
+
|
|
173
|
+
// Step 4: patch 卡片
|
|
174
|
+
if (result === 'SUCCESS') {
|
|
175
|
+
await client.im.message.patch({
|
|
176
|
+
path: { message_id: messageId },
|
|
177
|
+
data: {
|
|
178
|
+
content: JSON.stringify(buildCard({
|
|
179
|
+
title: '✅ AI营销平台授权成功',
|
|
180
|
+
headerTpl: 'green',
|
|
181
|
+
status: '可以开始使用了',
|
|
182
|
+
})),
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
console.log('AUTH_SUCCESS');
|
|
186
|
+
// 通过 openclaw agent 注入消息唤醒 session(fire-and-forget)
|
|
187
|
+
const notify = spawn('openclaw', [
|
|
188
|
+
'agent',
|
|
189
|
+
'--message', '[system] VH平台授权已完成,请继续执行用户之前的请求',
|
|
190
|
+
'--channel', 'feishu',
|
|
191
|
+
'--deliver',
|
|
192
|
+
'--timeout', '60',
|
|
193
|
+
], { stdio: 'ignore', detached: true });
|
|
194
|
+
notify.unref();
|
|
195
|
+
} else if (result === 'TIMEOUT') {
|
|
196
|
+
child.kill();
|
|
197
|
+
await client.im.message.patch({
|
|
198
|
+
path: { message_id: messageId },
|
|
199
|
+
data: {
|
|
200
|
+
content: JSON.stringify(buildCard({
|
|
201
|
+
title: '⏰ 授权超时',
|
|
202
|
+
headerTpl: 'orange',
|
|
203
|
+
status: '授权等待超时(120s),请重新发起',
|
|
204
|
+
})),
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
console.log('AUTH_TIMEOUT');
|
|
208
|
+
} else {
|
|
209
|
+
await client.im.message.patch({
|
|
210
|
+
path: { message_id: messageId },
|
|
211
|
+
data: {
|
|
212
|
+
content: JSON.stringify(buildCard({
|
|
213
|
+
title: '❌ 授权失败',
|
|
214
|
+
headerTpl: 'red',
|
|
215
|
+
status: '授权过程出错,请重试',
|
|
216
|
+
})),
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
console.log('AUTH_FAILED');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
process.exit(result === 'SUCCESS' ? 0 : 1);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
main().catch(e => {
|
|
226
|
+
console.error(e.message || e);
|
|
227
|
+
process.exit(1);
|
|
228
|
+
});
|