lightclawbot 1.1.1 → 1.1.2
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/dist/public/data/scripts/manifest.json +11 -0
- package/dist/public/data/scripts/preflight.9af62b39.sh +191 -0
- package/dist/public/data/scripts/preflight.sh +191 -0
- package/dist/src/upload-tool.d.ts.map +1 -1
- package/dist/src/upload-tool.js +10 -5
- package/dist/src/upload-tool.js.map +1 -1
- package/package.json +1 -1
- package/skills/lightclaw-cron/SKILL.md +2 -1
- package/dist/src/socket-handlers.d.ts +0 -21
- package/dist/src/socket-handlers.d.ts.map +0 -1
- package/dist/src/socket-handlers.js +0 -122
- package/dist/src/socket-handlers.js.map +0 -1
- package/dist/src/socket-registry.d.ts +0 -53
- package/dist/src/socket-registry.d.ts.map +0 -1
- package/dist/src/socket-registry.js +0 -111
- package/dist/src/socket-registry.js.map +0 -1
- package/skills/lightclaw-media/SKILL.md +0 -250
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# ========== lightclawbot 插件前置检查脚本 ==========
|
|
4
|
+
# 用途:快速判断 lightclawbot 插件是否可用
|
|
5
|
+
# 特点:
|
|
6
|
+
# 1. 减少文件 I/O 次数(一次读取配置文件,复用解析结果)
|
|
7
|
+
# 2. 与 OpenClaw 源码逻辑保持一致(plugins.enabled/deny/allow/entries)
|
|
8
|
+
# 3. 支持传参指定插件 ID
|
|
9
|
+
#
|
|
10
|
+
# 输出:最后一行固定为 RESULT:{...} JSON,供前端解析
|
|
11
|
+
# - 可用: RESULT:{"status":"ok","version":"x.y.z"}
|
|
12
|
+
# - 缺 openclaw:RESULT:{"status":"need_openclaw","reason":"...","desc":"..."}
|
|
13
|
+
# - 需要安装/配置:RESULT:{"status":"need_install","reason":"...","desc":"..."}
|
|
14
|
+
# - reason: 固定错误码(前端用于判断分支)
|
|
15
|
+
# - desc: 检测命令的实际输出(可选,用于排查问题)
|
|
16
|
+
#
|
|
17
|
+
# 使用方式:
|
|
18
|
+
# 本地执行: bash preflight.sh [plugin_id]
|
|
19
|
+
# CDN 执行: bash <(curl -fsSL https://your-cdn.com/preflight.sh) [plugin_id]
|
|
20
|
+
# 默认检查: lightclawbot
|
|
21
|
+
|
|
22
|
+
# ---------- 配置 ----------
|
|
23
|
+
id="${1:-lightclawbot}" # 支持传参指定插件 ID
|
|
24
|
+
cfg="$HOME/.openclaw/openclaw.json"
|
|
25
|
+
plugin_dir="$HOME/.openclaw/extensions/${id}"
|
|
26
|
+
|
|
27
|
+
# ---------- 加载 nvm 环境 ----------
|
|
28
|
+
# bash xxx.sh 启动的是非交互式 shell,不会自动 source ~/.bashrc,
|
|
29
|
+
# 如果用户通过 nvm 安装 node,脚本中的 node / openclaw 命令都找不到。
|
|
30
|
+
# 提前加载 nvm,让后续所有步骤都能使用 nvm 管理的 node。
|
|
31
|
+
export NVM_DIR="$HOME/.nvm"
|
|
32
|
+
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
|
|
33
|
+
|
|
34
|
+
# ---------- 工具函数 ----------
|
|
35
|
+
result_exit() {
|
|
36
|
+
local status="$1"
|
|
37
|
+
local reason="$2"
|
|
38
|
+
local desc="${3:-}"
|
|
39
|
+
local exit_code="${4:-1}"
|
|
40
|
+
# 转义 desc 中的特殊字符,保证输出合法 JSON
|
|
41
|
+
desc="${desc//\\/\\\\}"
|
|
42
|
+
desc="${desc//\"/\\\"}"
|
|
43
|
+
desc="${desc//$'\n'/\\n}"
|
|
44
|
+
desc="${desc//$'\r'/}"
|
|
45
|
+
if [ -n "$desc" ]; then
|
|
46
|
+
echo "RESULT:{\"status\":\"${status}\",\"reason\":\"${reason}\",\"desc\":\"${desc}\"}"
|
|
47
|
+
else
|
|
48
|
+
echo "RESULT:{\"status\":\"${status}\",\"reason\":\"${reason}\"}"
|
|
49
|
+
fi
|
|
50
|
+
exit "$exit_code"
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
result_ok() {
|
|
54
|
+
echo "RESULT:{\"status\":\"ok\",\"version\":\"${1}\"}"
|
|
55
|
+
exit 0
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# ---------- 1. 检查配置文件是否存在 ----------
|
|
59
|
+
|
|
60
|
+
if [ ! -f "$cfg" ]; then
|
|
61
|
+
result_exit "error" "config_not_found" "$(ls "$cfg" 2>&1)"
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
# ---------- 2. 验证配置文件 JSON 格式 ----------
|
|
65
|
+
# openclaw 通过即 node 可用,用 node 做一次轻量校验
|
|
66
|
+
|
|
67
|
+
json_check_output=$(node -e "JSON.parse(require('fs').readFileSync(process.argv[1],'utf8'))" "$cfg" 2>&1)
|
|
68
|
+
if [ $? -ne 0 ]; then
|
|
69
|
+
result_exit "error" "config_json_invalid" "$json_check_output"
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
# ---------- 3. 检查 openclaw 是否可用 ----------
|
|
73
|
+
|
|
74
|
+
if ! command -v openclaw &>/dev/null; then
|
|
75
|
+
oc_which_output=$(which openclaw 2>&1) || true
|
|
76
|
+
result_exit "error" "openclaw_not_available" "${oc_which_output:-openclaw not found in PATH=${PATH}}"
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
# ---------- 3a. 检查 openclaw 版本是否满足最低要求 ----------
|
|
80
|
+
|
|
81
|
+
min_version="2026.2.3"
|
|
82
|
+
# 兼容多种输出格式:
|
|
83
|
+
# "OpenClaw 2026.3.24 (cff6dc9)" → 2026.3.24
|
|
84
|
+
# "2026.2.3" → 2026.2.3
|
|
85
|
+
oc_version_output=$(openclaw -v 2>&1)
|
|
86
|
+
current_version=$(echo "$oc_version_output" | head -1 | sed 's/[()]//g' | grep -oE '[0-9]{4}\.[0-9]+\.[0-9]+' | head -1)
|
|
87
|
+
|
|
88
|
+
if [ -z "$current_version" ]; then
|
|
89
|
+
result_exit "error" "openclaw_version_unknown" "$oc_version_output"
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
# 比较版本号:将 a.b.c 转为数值后逐段比较
|
|
93
|
+
version_lt() {
|
|
94
|
+
local IFS='.'
|
|
95
|
+
local i a=($1) b=($2)
|
|
96
|
+
for ((i = 0; i < 3; i++)); do
|
|
97
|
+
if (( ${a[i]:-0} < ${b[i]:-0} )); then return 0; fi
|
|
98
|
+
if (( ${a[i]:-0} > ${b[i]:-0} )); then return 1; fi
|
|
99
|
+
done
|
|
100
|
+
return 1 # 相等时不算小于
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if version_lt "$current_version" "$min_version"; then
|
|
104
|
+
result_exit "error" "openclaw_version_too_old" "current: ${current_version}, required: >=${min_version}"
|
|
105
|
+
fi
|
|
106
|
+
|
|
107
|
+
# ---------- 3b. 检查 Gateway 是否可用 ----------
|
|
108
|
+
|
|
109
|
+
gateway_output=$(openclaw gateway health --json 2>&1)
|
|
110
|
+
if [ $? -ne 0 ]; then
|
|
111
|
+
result_exit "need_install" "gateway_not_healthy" "$gateway_output"
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
# ---------- 4. 检查插件目录是否已安装 ----------
|
|
115
|
+
|
|
116
|
+
if [ ! -d "$plugin_dir" ] || [ ! -f "$plugin_dir/package.json" ]; then
|
|
117
|
+
result_exit "need_install" "plugin_not_installed" "$(ls -la "$plugin_dir/package.json" 2>&1)"
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
# ---------- 5. 单次 node 调用完成所有配置项检查 ----------
|
|
121
|
+
# 一次启动 V8,顺序执行全部校验
|
|
122
|
+
# 通过环境变量安全传参,避免 shell 注入风险
|
|
123
|
+
# 输出格式:RESULT:{...} 与 bash 工具函数保持一致
|
|
124
|
+
|
|
125
|
+
PREFLIGHT_CFG="$cfg" PREFLIGHT_ID="$id" PREFLIGHT_PLUGIN_DIR="$plugin_dir" \
|
|
126
|
+
node -e '
|
|
127
|
+
const fs = require("fs");
|
|
128
|
+
const cfgPath = process.env.PREFLIGHT_CFG;
|
|
129
|
+
const pluginId = process.env.PREFLIGHT_ID;
|
|
130
|
+
const pluginDir = process.env.PREFLIGHT_PLUGIN_DIR;
|
|
131
|
+
|
|
132
|
+
function fail(status, reason, desc) {
|
|
133
|
+
const obj = { status, reason };
|
|
134
|
+
if (desc !== undefined) obj.desc = String(desc);
|
|
135
|
+
console.log("RESULT:" + JSON.stringify(obj));
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const cfg = JSON.parse(fs.readFileSync(cfgPath, "utf8"));
|
|
140
|
+
|
|
141
|
+
// 5a. 全局插件系统检查(plugins.enabled 默认 true,仅显式 false 禁用)
|
|
142
|
+
if (cfg.plugins?.enabled === false) {
|
|
143
|
+
fail("need_install", "plugins_globally_disabled", "plugins.enabled = " + JSON.stringify(cfg.plugins?.enabled));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 5b. deny 黑名单(优先级高于 allow,与 OpenClaw 源码一致)
|
|
147
|
+
if (Array.isArray(cfg.plugins?.deny) && cfg.plugins.deny.includes(pluginId)) {
|
|
148
|
+
fail("need_install", "plugin_in_denylist", "plugins.deny = " + JSON.stringify(cfg.plugins.deny));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 5c. plugins.entries 启用状态
|
|
152
|
+
if (cfg.plugins?.entries?.[pluginId]?.enabled !== true) {
|
|
153
|
+
fail("need_install", "plugin_not_enabled", "plugins.entries." + pluginId + ".enabled = " + JSON.stringify(cfg.plugins?.entries?.[pluginId]?.enabled));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 5d. allow 白名单(非空时必须在列表中)
|
|
157
|
+
const allow = cfg.plugins?.allow;
|
|
158
|
+
if (Array.isArray(allow) && allow.length > 0 && !allow.includes(pluginId)) {
|
|
159
|
+
fail("need_install", "plugin_not_in_allowlist", "plugins.allow = " + JSON.stringify(allow));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// 5e. plugins.installs 安装记录(可选,仅警告,默认不阻塞)
|
|
163
|
+
// 如需严格检查,取消下面的注释:
|
|
164
|
+
// if (!cfg.plugins?.installs?.[pluginId]) {
|
|
165
|
+
// fail("need_install", "plugin_install_record_missing", "plugins.installs." + pluginId + " = " + JSON.stringify(cfg.plugins?.installs?.[pluginId]));
|
|
166
|
+
// }
|
|
167
|
+
|
|
168
|
+
// 5f. channels 启用状态
|
|
169
|
+
if (cfg.channels?.[pluginId]?.enabled !== true) {
|
|
170
|
+
fail("need_install", "channel_not_enabled", "channels." + pluginId + ".enabled = " + JSON.stringify(cfg.channels?.[pluginId]?.enabled));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 5g. channels apiKeys 配置
|
|
174
|
+
const apiKeys = cfg.channels?.[pluginId]?.apiKeys;
|
|
175
|
+
if (!Array.isArray(apiKeys) || apiKeys.length === 0) {
|
|
176
|
+
fail("need_install", "apikeys_empty", "channels." + pluginId + ".apiKeys = " + JSON.stringify(apiKeys));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// 5h. 读取插件版本
|
|
180
|
+
let version = "unknown";
|
|
181
|
+
try {
|
|
182
|
+
const pkg = JSON.parse(fs.readFileSync(pluginDir + "/package.json", "utf8"));
|
|
183
|
+
version = pkg.version || "unknown";
|
|
184
|
+
} catch (e) {}
|
|
185
|
+
|
|
186
|
+
// 全部通过
|
|
187
|
+
console.log("RESULT:" + JSON.stringify({ status: "ok", version }));
|
|
188
|
+
'
|
|
189
|
+
|
|
190
|
+
# 将 node 的退出码传递给 bash
|
|
191
|
+
exit $?
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# ========== lightclawbot 插件前置检查脚本 ==========
|
|
4
|
+
# 用途:快速判断 lightclawbot 插件是否可用
|
|
5
|
+
# 特点:
|
|
6
|
+
# 1. 减少文件 I/O 次数(一次读取配置文件,复用解析结果)
|
|
7
|
+
# 2. 与 OpenClaw 源码逻辑保持一致(plugins.enabled/deny/allow/entries)
|
|
8
|
+
# 3. 支持传参指定插件 ID
|
|
9
|
+
#
|
|
10
|
+
# 输出:最后一行固定为 RESULT:{...} JSON,供前端解析
|
|
11
|
+
# - 可用: RESULT:{"status":"ok","version":"x.y.z"}
|
|
12
|
+
# - 缺 openclaw:RESULT:{"status":"need_openclaw","reason":"...","desc":"..."}
|
|
13
|
+
# - 需要安装/配置:RESULT:{"status":"need_install","reason":"...","desc":"..."}
|
|
14
|
+
# - reason: 固定错误码(前端用于判断分支)
|
|
15
|
+
# - desc: 检测命令的实际输出(可选,用于排查问题)
|
|
16
|
+
#
|
|
17
|
+
# 使用方式:
|
|
18
|
+
# 本地执行: bash preflight.sh [plugin_id]
|
|
19
|
+
# CDN 执行: bash <(curl -fsSL https://your-cdn.com/preflight.sh) [plugin_id]
|
|
20
|
+
# 默认检查: lightclawbot
|
|
21
|
+
|
|
22
|
+
# ---------- 配置 ----------
|
|
23
|
+
id="${1:-lightclawbot}" # 支持传参指定插件 ID
|
|
24
|
+
cfg="$HOME/.openclaw/openclaw.json"
|
|
25
|
+
plugin_dir="$HOME/.openclaw/extensions/${id}"
|
|
26
|
+
|
|
27
|
+
# ---------- 加载 nvm 环境 ----------
|
|
28
|
+
# bash xxx.sh 启动的是非交互式 shell,不会自动 source ~/.bashrc,
|
|
29
|
+
# 如果用户通过 nvm 安装 node,脚本中的 node / openclaw 命令都找不到。
|
|
30
|
+
# 提前加载 nvm,让后续所有步骤都能使用 nvm 管理的 node。
|
|
31
|
+
export NVM_DIR="$HOME/.nvm"
|
|
32
|
+
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
|
|
33
|
+
|
|
34
|
+
# ---------- 工具函数 ----------
|
|
35
|
+
result_exit() {
|
|
36
|
+
local status="$1"
|
|
37
|
+
local reason="$2"
|
|
38
|
+
local desc="${3:-}"
|
|
39
|
+
local exit_code="${4:-1}"
|
|
40
|
+
# 转义 desc 中的特殊字符,保证输出合法 JSON
|
|
41
|
+
desc="${desc//\\/\\\\}"
|
|
42
|
+
desc="${desc//\"/\\\"}"
|
|
43
|
+
desc="${desc//$'\n'/\\n}"
|
|
44
|
+
desc="${desc//$'\r'/}"
|
|
45
|
+
if [ -n "$desc" ]; then
|
|
46
|
+
echo "RESULT:{\"status\":\"${status}\",\"reason\":\"${reason}\",\"desc\":\"${desc}\"}"
|
|
47
|
+
else
|
|
48
|
+
echo "RESULT:{\"status\":\"${status}\",\"reason\":\"${reason}\"}"
|
|
49
|
+
fi
|
|
50
|
+
exit "$exit_code"
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
result_ok() {
|
|
54
|
+
echo "RESULT:{\"status\":\"ok\",\"version\":\"${1}\"}"
|
|
55
|
+
exit 0
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# ---------- 1. 检查配置文件是否存在 ----------
|
|
59
|
+
|
|
60
|
+
if [ ! -f "$cfg" ]; then
|
|
61
|
+
result_exit "error" "config_not_found" "$(ls "$cfg" 2>&1)"
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
# ---------- 2. 验证配置文件 JSON 格式 ----------
|
|
65
|
+
# openclaw 通过即 node 可用,用 node 做一次轻量校验
|
|
66
|
+
|
|
67
|
+
json_check_output=$(node -e "JSON.parse(require('fs').readFileSync(process.argv[1],'utf8'))" "$cfg" 2>&1)
|
|
68
|
+
if [ $? -ne 0 ]; then
|
|
69
|
+
result_exit "error" "config_json_invalid" "$json_check_output"
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
# ---------- 3. 检查 openclaw 是否可用 ----------
|
|
73
|
+
|
|
74
|
+
if ! command -v openclaw &>/dev/null; then
|
|
75
|
+
oc_which_output=$(which openclaw 2>&1) || true
|
|
76
|
+
result_exit "error" "openclaw_not_available" "${oc_which_output:-openclaw not found in PATH=${PATH}}"
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
# ---------- 3a. 检查 openclaw 版本是否满足最低要求 ----------
|
|
80
|
+
|
|
81
|
+
min_version="2026.2.3"
|
|
82
|
+
# 兼容多种输出格式:
|
|
83
|
+
# "OpenClaw 2026.3.24 (cff6dc9)" → 2026.3.24
|
|
84
|
+
# "2026.2.3" → 2026.2.3
|
|
85
|
+
oc_version_output=$(openclaw -v 2>&1)
|
|
86
|
+
current_version=$(echo "$oc_version_output" | head -1 | sed 's/[()]//g' | grep -oE '[0-9]{4}\.[0-9]+\.[0-9]+' | head -1)
|
|
87
|
+
|
|
88
|
+
if [ -z "$current_version" ]; then
|
|
89
|
+
result_exit "error" "openclaw_version_unknown" "$oc_version_output"
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
# 比较版本号:将 a.b.c 转为数值后逐段比较
|
|
93
|
+
version_lt() {
|
|
94
|
+
local IFS='.'
|
|
95
|
+
local i a=($1) b=($2)
|
|
96
|
+
for ((i = 0; i < 3; i++)); do
|
|
97
|
+
if (( ${a[i]:-0} < ${b[i]:-0} )); then return 0; fi
|
|
98
|
+
if (( ${a[i]:-0} > ${b[i]:-0} )); then return 1; fi
|
|
99
|
+
done
|
|
100
|
+
return 1 # 相等时不算小于
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if version_lt "$current_version" "$min_version"; then
|
|
104
|
+
result_exit "error" "openclaw_version_too_old" "current: ${current_version}, required: >=${min_version}"
|
|
105
|
+
fi
|
|
106
|
+
|
|
107
|
+
# ---------- 3b. 检查 Gateway 是否可用 ----------
|
|
108
|
+
|
|
109
|
+
gateway_output=$(openclaw gateway health --json 2>&1)
|
|
110
|
+
if [ $? -ne 0 ]; then
|
|
111
|
+
result_exit "need_install" "gateway_not_healthy" "$gateway_output"
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
# ---------- 4. 检查插件目录是否已安装 ----------
|
|
115
|
+
|
|
116
|
+
if [ ! -d "$plugin_dir" ] || [ ! -f "$plugin_dir/package.json" ]; then
|
|
117
|
+
result_exit "need_install" "plugin_not_installed" "$(ls -la "$plugin_dir/package.json" 2>&1)"
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
# ---------- 5. 单次 node 调用完成所有配置项检查 ----------
|
|
121
|
+
# 一次启动 V8,顺序执行全部校验
|
|
122
|
+
# 通过环境变量安全传参,避免 shell 注入风险
|
|
123
|
+
# 输出格式:RESULT:{...} 与 bash 工具函数保持一致
|
|
124
|
+
|
|
125
|
+
PREFLIGHT_CFG="$cfg" PREFLIGHT_ID="$id" PREFLIGHT_PLUGIN_DIR="$plugin_dir" \
|
|
126
|
+
node -e '
|
|
127
|
+
const fs = require("fs");
|
|
128
|
+
const cfgPath = process.env.PREFLIGHT_CFG;
|
|
129
|
+
const pluginId = process.env.PREFLIGHT_ID;
|
|
130
|
+
const pluginDir = process.env.PREFLIGHT_PLUGIN_DIR;
|
|
131
|
+
|
|
132
|
+
function fail(status, reason, desc) {
|
|
133
|
+
const obj = { status, reason };
|
|
134
|
+
if (desc !== undefined) obj.desc = String(desc);
|
|
135
|
+
console.log("RESULT:" + JSON.stringify(obj));
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const cfg = JSON.parse(fs.readFileSync(cfgPath, "utf8"));
|
|
140
|
+
|
|
141
|
+
// 5a. 全局插件系统检查(plugins.enabled 默认 true,仅显式 false 禁用)
|
|
142
|
+
if (cfg.plugins?.enabled === false) {
|
|
143
|
+
fail("need_install", "plugins_globally_disabled", "plugins.enabled = " + JSON.stringify(cfg.plugins?.enabled));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 5b. deny 黑名单(优先级高于 allow,与 OpenClaw 源码一致)
|
|
147
|
+
if (Array.isArray(cfg.plugins?.deny) && cfg.plugins.deny.includes(pluginId)) {
|
|
148
|
+
fail("need_install", "plugin_in_denylist", "plugins.deny = " + JSON.stringify(cfg.plugins.deny));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 5c. plugins.entries 启用状态
|
|
152
|
+
if (cfg.plugins?.entries?.[pluginId]?.enabled !== true) {
|
|
153
|
+
fail("need_install", "plugin_not_enabled", "plugins.entries." + pluginId + ".enabled = " + JSON.stringify(cfg.plugins?.entries?.[pluginId]?.enabled));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 5d. allow 白名单(非空时必须在列表中)
|
|
157
|
+
const allow = cfg.plugins?.allow;
|
|
158
|
+
if (Array.isArray(allow) && allow.length > 0 && !allow.includes(pluginId)) {
|
|
159
|
+
fail("need_install", "plugin_not_in_allowlist", "plugins.allow = " + JSON.stringify(allow));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// 5e. plugins.installs 安装记录(可选,仅警告,默认不阻塞)
|
|
163
|
+
// 如需严格检查,取消下面的注释:
|
|
164
|
+
// if (!cfg.plugins?.installs?.[pluginId]) {
|
|
165
|
+
// fail("need_install", "plugin_install_record_missing", "plugins.installs." + pluginId + " = " + JSON.stringify(cfg.plugins?.installs?.[pluginId]));
|
|
166
|
+
// }
|
|
167
|
+
|
|
168
|
+
// 5f. channels 启用状态
|
|
169
|
+
if (cfg.channels?.[pluginId]?.enabled !== true) {
|
|
170
|
+
fail("need_install", "channel_not_enabled", "channels." + pluginId + ".enabled = " + JSON.stringify(cfg.channels?.[pluginId]?.enabled));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 5g. channels apiKeys 配置
|
|
174
|
+
const apiKeys = cfg.channels?.[pluginId]?.apiKeys;
|
|
175
|
+
if (!Array.isArray(apiKeys) || apiKeys.length === 0) {
|
|
176
|
+
fail("need_install", "apikeys_empty", "channels." + pluginId + ".apiKeys = " + JSON.stringify(apiKeys));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// 5h. 读取插件版本
|
|
180
|
+
let version = "unknown";
|
|
181
|
+
try {
|
|
182
|
+
const pkg = JSON.parse(fs.readFileSync(pluginDir + "/package.json", "utf8"));
|
|
183
|
+
version = pkg.version || "unknown";
|
|
184
|
+
} catch (e) {}
|
|
185
|
+
|
|
186
|
+
// 全部通过
|
|
187
|
+
console.log("RESULT:" + JSON.stringify({ status: "ok", version }));
|
|
188
|
+
'
|
|
189
|
+
|
|
190
|
+
# 将 node 的退出码传递给 bash
|
|
191
|
+
exit $?
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"upload-tool.d.ts","sourceRoot":"","sources":["../../src/upload-tool.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,KAAK,EAAE,iBAAiB,EAAgB,MAAM,0BAA0B,CAAC;AAShF,eAAO,MAAM,gBAAgB,0BAA0B,CAAC;AAExD,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;CAY5B,CAAC;AAMF,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,iBAAiB,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"upload-tool.d.ts","sourceRoot":"","sources":["../../src/upload-tool.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,KAAK,EAAE,iBAAiB,EAAgB,MAAM,0BAA0B,CAAC;AAShF,eAAO,MAAM,gBAAgB,0BAA0B,CAAC;AAExD,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;CAY5B,CAAC;AAMF,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,iBAAiB,GAAG,IAAI,CA4I/D"}
|
package/dist/src/upload-tool.js
CHANGED
|
@@ -23,7 +23,7 @@ export const uploadToolSchema = {
|
|
|
23
23
|
items: { type: "string" },
|
|
24
24
|
minItems: 1,
|
|
25
25
|
maxItems: 5,
|
|
26
|
-
description: "
|
|
26
|
+
description: "需要上传的本地文件绝对路径数组,文件必须已写入磁盘,最多 5 个文件",
|
|
27
27
|
},
|
|
28
28
|
},
|
|
29
29
|
required: ["paths"],
|
|
@@ -35,14 +35,19 @@ export function registerUploadTool(api) {
|
|
|
35
35
|
// 通过闭包捕获 api.logger,tool ctx 中没有 log
|
|
36
36
|
const log = api.logger;
|
|
37
37
|
api.registerTool((ctx) => {
|
|
38
|
+
// 硬隔离:非 lightclawbot 通道时直接返回 null,框架不会暴露此工具
|
|
39
|
+
if (ctx.messageChannel !== "lightclawbot") {
|
|
40
|
+
// log.warn(`[lightclaw_upload_file] channel=${ctx.messageChannel}, skip..."`);
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
38
43
|
const defaultAccountId = ctx.agentAccountId;
|
|
39
44
|
const sessionKey = ctx.sessionKey;
|
|
45
|
+
// log.warn(`[lightclaw_upload_file] channel=${ctx.messageChannel}, contiue..."`);
|
|
40
46
|
return {
|
|
41
47
|
name: UPLOAD_TOOL_NAME,
|
|
42
|
-
description: "
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"Supports up to 5 files at once. Returns a public download URL for each uploaded file.",
|
|
48
|
+
description: "将本地文件上传到云端临时存储并返回下载链接。" +
|
|
49
|
+
"当需要把生成的文件(图片、文档、代码包等)交付给用户时,必须使用此工具。" +
|
|
50
|
+
"文件必须先写入磁盘,然后传入绝对路径。单次最多 5 个文件,超出请分批调用。",
|
|
46
51
|
parameters: uploadToolSchema,
|
|
47
52
|
async execute(_toolCallId, params) {
|
|
48
53
|
// 每次 execute 时动态解析 apiKey(多 key 模式下通过 sessionKey 直接获取)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"upload-tool.js","sourceRoot":"","sources":["../../src/upload-tool.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAElC,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AAErD,+DAA+D;AAC/D,8BAA8B;AAC9B,+DAA+D;AAE/D,MAAM,CAAC,MAAM,gBAAgB,GAAG,uBAAuB,CAAC;AAExD,MAAM,CAAC,MAAM,gBAAgB,GAAG;IAC9B,IAAI,EAAE,QAAiB;IACvB,UAAU,EAAE;QACV,KAAK,EAAE;YACL,IAAI,EAAE,OAAgB;YACtB,KAAK,EAAE,EAAE,IAAI,EAAE,QAAiB,EAAE;YAClC,QAAQ,EAAE,CAAC;YACX,QAAQ,EAAE,CAAC;YACX,WAAW,EAAE,
|
|
1
|
+
{"version":3,"file":"upload-tool.js","sourceRoot":"","sources":["../../src/upload-tool.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: lightclawbot-cron
|
|
3
|
-
description: LightClawBot
|
|
3
|
+
description: 仅限 lightclawbot 通道使用。LightClawBot 定时任务提醒,支持一次性提醒、周期性任务,可设置、查询、取消提醒。当消息来源不是 lightclawbot 时,禁止调用本技能。
|
|
4
|
+
metadata: { "openclaw": { "requires": { "config": ["channels.lightclawbot.apiKeys"] } } }
|
|
4
5
|
---
|
|
5
6
|
|
|
6
7
|
# LightClawBot 智能提醒
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* LightClaw — Socket.IO 事件处理器
|
|
3
|
-
*
|
|
4
|
-
* 将 socket 事件监听(message:private、history:request、sessions:request)
|
|
5
|
-
* 从 gateway 主逻辑中解耦。
|
|
6
|
-
*/
|
|
7
|
-
import type { Socket } from "socket.io-client";
|
|
8
|
-
import type { GatewayContext, QueuedMessage } from "./types.js";
|
|
9
|
-
export interface SocketHandlerDeps {
|
|
10
|
-
account: GatewayContext["account"];
|
|
11
|
-
botClientId: string;
|
|
12
|
-
log?: GatewayContext["log"];
|
|
13
|
-
enqueueMessage: (msg: QueuedMessage) => void;
|
|
14
|
-
/** 收到入站事件时调用,通知框架更新 lastEventAt */
|
|
15
|
-
onEvent?: () => void;
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* 绑定所有 Socket.IO 事件监听器
|
|
19
|
-
*/
|
|
20
|
-
export declare function bindSocketHandlers(socket: Socket, deps: SocketHandlerDeps): void;
|
|
21
|
-
//# sourceMappingURL=socket-handlers.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"socket-handlers.d.ts","sourceRoot":"","sources":["../../src/socket-handlers.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,KAAK,EAAE,cAAc,EAAsB,aAAa,EAAE,MAAM,YAAY,CAAC;AAcpF,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,cAAc,CAAC,SAAS,CAAC,CAAC;IACnC,WAAW,EAAE,MAAM,CAAC;IACpB,GAAG,CAAC,EAAE,cAAc,CAAC,KAAK,CAAC,CAAC;IAC5B,cAAc,EAAE,CAAC,GAAG,EAAE,aAAa,KAAK,IAAI,CAAC;IAC7C,mCAAmC;IACnC,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;CACtB;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,iBAAiB,GAAG,IAAI,CA6HhF"}
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* LightClaw — Socket.IO 事件处理器
|
|
3
|
-
*
|
|
4
|
-
* 将 socket 事件监听(message:private、history:request、sessions:request)
|
|
5
|
-
* 从 gateway 主逻辑中解耦。
|
|
6
|
-
*/
|
|
7
|
-
import { CHANNEL_KEY, EVENT_MESSAGE_PRIVATE, EVENT_HISTORY_REQUEST, EVENT_HISTORY_RESPONSE, EVENT_SESSIONS_REQUEST, EVENT_SESSIONS_RESPONSE, DEFAULT_HISTORY_LIMIT, } from "./config.js";
|
|
8
|
-
import { isDuplicate, debounceHistoryRequest, generateMsgId } from "./dedup.js";
|
|
9
|
-
import { getAssistantRuntime } from "./runtime.js";
|
|
10
|
-
import { readSessionHistoryWithCron, listSessions } from "./history/index.js";
|
|
11
|
-
/**
|
|
12
|
-
* 绑定所有 Socket.IO 事件监听器
|
|
13
|
-
*/
|
|
14
|
-
export function bindSocketHandlers(socket, deps) {
|
|
15
|
-
const { account, botClientId, log, enqueueMessage, onEvent } = deps;
|
|
16
|
-
// ---- 接收用户消息 ----
|
|
17
|
-
socket.on(EVENT_MESSAGE_PRIVATE, (data, ack) => {
|
|
18
|
-
ack?.();
|
|
19
|
-
log?.info(`[${CHANNEL_KEY}] Received private message: ${JSON.stringify(data)},botClientId:${botClientId}`);
|
|
20
|
-
// 回环防御:过滤自身发出的消息
|
|
21
|
-
if (data.from === botClientId)
|
|
22
|
-
return;
|
|
23
|
-
// 跳过控制消息(typing / stream 信号),只处理真实用户消息
|
|
24
|
-
if (data.kind && data.kind !== "text")
|
|
25
|
-
return;
|
|
26
|
-
// 无内容跳过
|
|
27
|
-
const hasContent = data.content?.trim();
|
|
28
|
-
const hasFiles = data.files && data.files.length > 0;
|
|
29
|
-
if (!hasContent && !hasFiles)
|
|
30
|
-
return;
|
|
31
|
-
// 去重
|
|
32
|
-
if (isDuplicate(data.msgId))
|
|
33
|
-
return;
|
|
34
|
-
// 通知框架收到入站事件(更新 lastEventAt,防止 stale-socket 误判)
|
|
35
|
-
onEvent?.();
|
|
36
|
-
log?.info(`[${CHANNEL_KEY}] Message from ${data.from}: "${(data.content || "").slice(0, 60)}" files=${data.files?.length ?? 0}`);
|
|
37
|
-
enqueueMessage({
|
|
38
|
-
senderId: data.from,
|
|
39
|
-
text: data.content || "",
|
|
40
|
-
messageId: data.msgId,
|
|
41
|
-
files: data.files ?? [],
|
|
42
|
-
timestamp: data.timestamp,
|
|
43
|
-
});
|
|
44
|
-
});
|
|
45
|
-
// ---- 历史消息请求 ----
|
|
46
|
-
socket.on(EVENT_HISTORY_REQUEST, (data, ack) => {
|
|
47
|
-
ack?.();
|
|
48
|
-
if (!data?.from) {
|
|
49
|
-
log?.warn(`[${CHANNEL_KEY}] History request missing userId, ignoring`);
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
// 回环防御:忽略 bot 自身发出的请求
|
|
53
|
-
if (data.from === botClientId)
|
|
54
|
-
return;
|
|
55
|
-
// 去重:如果有 msgId,走消息级去重
|
|
56
|
-
if (data.msgId && isDuplicate(data.msgId)) {
|
|
57
|
-
log?.warn(`[${CHANNEL_KEY}] Duplicate history request (msgId), ignoring`);
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
// 防抖:同一用户高频请求只处理最后一条
|
|
61
|
-
debounceHistoryRequest(data.from, () => {
|
|
62
|
-
// 通知框架收到入站事件(更新 lastEventAt,防止 stale-socket 误判)
|
|
63
|
-
onEvent?.();
|
|
64
|
-
try {
|
|
65
|
-
const pluginRuntime = getAssistantRuntime();
|
|
66
|
-
const currentCfg = pluginRuntime.config.loadConfig();
|
|
67
|
-
const route = pluginRuntime.channel.routing.resolveAgentRoute({
|
|
68
|
-
cfg: currentCfg,
|
|
69
|
-
channel: CHANNEL_KEY,
|
|
70
|
-
accountId: account.accountId,
|
|
71
|
-
peer: { kind: "direct", id: data.from },
|
|
72
|
-
});
|
|
73
|
-
const sessionKey = route?.sessionKey ?? `${CHANNEL_KEY}:dm:${data.from}`;
|
|
74
|
-
const messages = readSessionHistoryWithCron(sessionKey, {
|
|
75
|
-
limit: data.limit ?? DEFAULT_HISTORY_LIMIT,
|
|
76
|
-
chatOnly: data.chatOnly ?? true,
|
|
77
|
-
includeCron: true,
|
|
78
|
-
});
|
|
79
|
-
log?.info(`[${CHANNEL_KEY}] History request: userId=${data.from} sessionKey=${sessionKey} found=${messages.length}`);
|
|
80
|
-
socket.emit(EVENT_HISTORY_RESPONSE, {
|
|
81
|
-
msgId: generateMsgId(),
|
|
82
|
-
from: botClientId,
|
|
83
|
-
to: data.from,
|
|
84
|
-
sessionKey,
|
|
85
|
-
messages: messages.filter(msg => !!msg.content.trim() || (msg.files && msg.files.length > 0)),
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
catch (err) {
|
|
89
|
-
log?.error(`[${CHANNEL_KEY}] History request error: ${err}`);
|
|
90
|
-
socket.emit(EVENT_HISTORY_RESPONSE, {
|
|
91
|
-
msgId: generateMsgId(),
|
|
92
|
-
from: botClientId,
|
|
93
|
-
to: data.from,
|
|
94
|
-
sessionKey: "",
|
|
95
|
-
messages: [],
|
|
96
|
-
error: err instanceof Error ? err.message : String(err),
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
// ---- 会话列表请求 ----
|
|
102
|
-
socket.on(EVENT_SESSIONS_REQUEST, (data) => {
|
|
103
|
-
// 通知框架收到入站事件(更新 lastEventAt,防止 stale-socket 误判)
|
|
104
|
-
onEvent?.();
|
|
105
|
-
try {
|
|
106
|
-
const sessions = listSessions();
|
|
107
|
-
socket.emit(EVENT_SESSIONS_RESPONSE, {
|
|
108
|
-
requestId: data.requestId,
|
|
109
|
-
sessions,
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
catch (err) {
|
|
113
|
-
log?.error(`[${CHANNEL_KEY}] Sessions list error: ${err}`);
|
|
114
|
-
socket.emit(EVENT_SESSIONS_RESPONSE, {
|
|
115
|
-
requestId: data.requestId,
|
|
116
|
-
sessions: [],
|
|
117
|
-
error: err instanceof Error ? err.message : String(err),
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
//# sourceMappingURL=socket-handlers.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"socket-handlers.js","sourceRoot":"","sources":["../../src/socket-handlers.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* LightClaw — Socket 注册表
|
|
3
|
-
*
|
|
4
|
-
* 让 gateway 启动时注册 socket 实例,
|
|
5
|
-
* outbound 在需要发送消息时可以通过 WS 连接直接发送,
|
|
6
|
-
* 无需走 REST API。
|
|
7
|
-
*
|
|
8
|
-
* 断线缓冲:
|
|
9
|
-
* 当 socket 暂时断开(Socket.IO 自动重连中)时,outbound 消息
|
|
10
|
-
* 会被缓冲在 pendingMessages 队列中,重连后自动 flush 发送。
|
|
11
|
-
* 仅在 gateway 彻底销毁(cleanup)时才删除 entry。
|
|
12
|
-
*/
|
|
13
|
-
import type { Socket } from "socket.io-client";
|
|
14
|
-
import type { PrivateMessageData } from "./types.js";
|
|
15
|
-
interface SocketEntry {
|
|
16
|
-
socket: Socket;
|
|
17
|
-
botClientId: string;
|
|
18
|
-
/** 断线期间缓冲的待发消息 */
|
|
19
|
-
pendingMessages: PrivateMessageData[];
|
|
20
|
-
}
|
|
21
|
-
/** 注册 socket(gateway 首次连接时调用) */
|
|
22
|
-
export declare function registerSocket(accountId: string, socket: Socket, botClientId: string): void;
|
|
23
|
-
/**
|
|
24
|
-
* 注销 socket(gateway 彻底销毁时调用)。
|
|
25
|
-
* 注意:普通断线重连不应调用此函数,只在 cleanup 时调用。
|
|
26
|
-
*/
|
|
27
|
-
export declare function unregisterSocket(accountId: string): void;
|
|
28
|
-
/** 获取可用的 socket(仅在 connected 时返回) */
|
|
29
|
-
export declare function getSocket(accountId: string): Pick<SocketEntry, "socket" | "botClientId"> | undefined;
|
|
30
|
-
/** 检查 account 是否有注册的 entry(不管是否 connected) */
|
|
31
|
-
export declare function hasEntry(accountId: string): boolean;
|
|
32
|
-
/** 获取 botClientId(不管 socket 是否 connected) */
|
|
33
|
-
export declare function getBotClientId(accountId: string): string | undefined;
|
|
34
|
-
/**
|
|
35
|
-
* 缓冲一条消息(socket 断开期间由 outbound 调用)。
|
|
36
|
-
* 返回 true 表示成功缓冲,false 表示该 account 没有注册的 entry。
|
|
37
|
-
*/
|
|
38
|
-
export declare function bufferMessage(accountId: string, message: PrivateMessageData): boolean;
|
|
39
|
-
/**
|
|
40
|
-
* flush 所有缓冲消息(重连成功后由 gateway 调用)。
|
|
41
|
-
* 返回发送成功 / 失败的计数。
|
|
42
|
-
*/
|
|
43
|
-
export declare function flushPendingMessages(accountId: string, log?: {
|
|
44
|
-
info: (msg: string) => void;
|
|
45
|
-
warn: (msg: string) => void;
|
|
46
|
-
}): {
|
|
47
|
-
sent: number;
|
|
48
|
-
failed: number;
|
|
49
|
-
};
|
|
50
|
-
/** 获取缓冲队列长度(调试/监控用) */
|
|
51
|
-
export declare function getPendingCount(accountId: string): number;
|
|
52
|
-
export {};
|
|
53
|
-
//# sourceMappingURL=socket-registry.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"socket-registry.d.ts","sourceRoot":"","sources":["../../src/socket-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAOrD,UAAU,WAAW;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,kBAAkB;IAClB,eAAe,EAAE,kBAAkB,EAAE,CAAC;CACvC;AASD,iCAAiC;AACjC,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,IAAI,CAS3F;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAExD;AAMD,qCAAqC;AACrC,wBAAgB,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC,WAAW,EAAE,QAAQ,GAAG,aAAa,CAAC,GAAG,SAAS,CAIpG;AAED,8CAA8C;AAC9C,wBAAgB,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAEnD;AAED,6CAA6C;AAC7C,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAEpE;AAMD;;;GAGG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAUrF;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,MAAM,EACjB,GAAG,CAAC,EAAE;IAAE,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAAC,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE,GACjE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CA8BlC;AAED,uBAAuB;AACvB,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAEzD"}
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* LightClaw — Socket 注册表
|
|
3
|
-
*
|
|
4
|
-
* 让 gateway 启动时注册 socket 实例,
|
|
5
|
-
* outbound 在需要发送消息时可以通过 WS 连接直接发送,
|
|
6
|
-
* 无需走 REST API。
|
|
7
|
-
*
|
|
8
|
-
* 断线缓冲:
|
|
9
|
-
* 当 socket 暂时断开(Socket.IO 自动重连中)时,outbound 消息
|
|
10
|
-
* 会被缓冲在 pendingMessages 队列中,重连后自动 flush 发送。
|
|
11
|
-
* 仅在 gateway 彻底销毁(cleanup)时才删除 entry。
|
|
12
|
-
*/
|
|
13
|
-
import { MAX_PENDING_MESSAGES, EVENT_MESSAGE_PRIVATE } from "./config.js";
|
|
14
|
-
/** accountId → SocketEntry */
|
|
15
|
-
const registry = new Map();
|
|
16
|
-
// ============================================================
|
|
17
|
-
// 注册 / 注销
|
|
18
|
-
// ============================================================
|
|
19
|
-
/** 注册 socket(gateway 首次连接时调用) */
|
|
20
|
-
export function registerSocket(accountId, socket, botClientId) {
|
|
21
|
-
const existing = registry.get(accountId);
|
|
22
|
-
if (existing) {
|
|
23
|
-
// 重连场景:更新 socket 引用,保留 pending 队列
|
|
24
|
-
existing.socket = socket;
|
|
25
|
-
existing.botClientId = botClientId;
|
|
26
|
-
}
|
|
27
|
-
else {
|
|
28
|
-
registry.set(accountId, { socket, botClientId, pendingMessages: [] });
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* 注销 socket(gateway 彻底销毁时调用)。
|
|
33
|
-
* 注意:普通断线重连不应调用此函数,只在 cleanup 时调用。
|
|
34
|
-
*/
|
|
35
|
-
export function unregisterSocket(accountId) {
|
|
36
|
-
registry.delete(accountId);
|
|
37
|
-
}
|
|
38
|
-
// ============================================================
|
|
39
|
-
// 查询
|
|
40
|
-
// ============================================================
|
|
41
|
-
/** 获取可用的 socket(仅在 connected 时返回) */
|
|
42
|
-
export function getSocket(accountId) {
|
|
43
|
-
const entry = registry.get(accountId);
|
|
44
|
-
if (entry && entry.socket.connected)
|
|
45
|
-
return entry;
|
|
46
|
-
return undefined;
|
|
47
|
-
}
|
|
48
|
-
/** 检查 account 是否有注册的 entry(不管是否 connected) */
|
|
49
|
-
export function hasEntry(accountId) {
|
|
50
|
-
return registry.has(accountId);
|
|
51
|
-
}
|
|
52
|
-
/** 获取 botClientId(不管 socket 是否 connected) */
|
|
53
|
-
export function getBotClientId(accountId) {
|
|
54
|
-
return registry.get(accountId)?.botClientId;
|
|
55
|
-
}
|
|
56
|
-
// ============================================================
|
|
57
|
-
// 断线缓冲
|
|
58
|
-
// ============================================================
|
|
59
|
-
/**
|
|
60
|
-
* 缓冲一条消息(socket 断开期间由 outbound 调用)。
|
|
61
|
-
* 返回 true 表示成功缓冲,false 表示该 account 没有注册的 entry。
|
|
62
|
-
*/
|
|
63
|
-
export function bufferMessage(accountId, message) {
|
|
64
|
-
const entry = registry.get(accountId);
|
|
65
|
-
if (!entry)
|
|
66
|
-
return false;
|
|
67
|
-
if (entry.pendingMessages.length >= MAX_PENDING_MESSAGES) {
|
|
68
|
-
// 队列满了,丢弃最早的消息
|
|
69
|
-
entry.pendingMessages.shift();
|
|
70
|
-
}
|
|
71
|
-
entry.pendingMessages.push(message);
|
|
72
|
-
return true;
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* flush 所有缓冲消息(重连成功后由 gateway 调用)。
|
|
76
|
-
* 返回发送成功 / 失败的计数。
|
|
77
|
-
*/
|
|
78
|
-
export function flushPendingMessages(accountId, log) {
|
|
79
|
-
const entry = registry.get(accountId);
|
|
80
|
-
if (!entry)
|
|
81
|
-
return { sent: 0, failed: 0 };
|
|
82
|
-
const pending = entry.pendingMessages.splice(0); // 取出全部并清空
|
|
83
|
-
if (pending.length === 0)
|
|
84
|
-
return { sent: 0, failed: 0 };
|
|
85
|
-
let sent = 0;
|
|
86
|
-
let failed = 0;
|
|
87
|
-
for (const msg of pending) {
|
|
88
|
-
if (!entry.socket.connected) {
|
|
89
|
-
// socket 又断了,把剩余消息放回去
|
|
90
|
-
entry.pendingMessages.unshift(...pending.slice(sent + failed));
|
|
91
|
-
break;
|
|
92
|
-
}
|
|
93
|
-
try {
|
|
94
|
-
entry.socket.emit(EVENT_MESSAGE_PRIVATE, msg);
|
|
95
|
-
sent++;
|
|
96
|
-
}
|
|
97
|
-
catch {
|
|
98
|
-
failed++;
|
|
99
|
-
log?.warn(`[socket-registry] Failed to flush buffered message: msgId=${msg.msgId}`);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
if (sent > 0 || failed > 0) {
|
|
103
|
-
log?.info(`[socket-registry] Flushed pending messages: sent=${sent}, failed=${failed}`);
|
|
104
|
-
}
|
|
105
|
-
return { sent, failed };
|
|
106
|
-
}
|
|
107
|
-
/** 获取缓冲队列长度(调试/监控用) */
|
|
108
|
-
export function getPendingCount(accountId) {
|
|
109
|
-
return registry.get(accountId)?.pendingMessages.length ?? 0;
|
|
110
|
-
}
|
|
111
|
-
//# sourceMappingURL=socket-registry.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"socket-registry.js","sourceRoot":"","sources":["../../src/socket-registry.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,250 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: lightclawbot-media
|
|
3
|
-
description: LightClawBot 文件收发能力。用户发来的文件自动下载并保存,AI 生成的文件通过 lightclaw_upload_file 上传后以标准 Markdown 链接返回给用户。禁止使用其他存储工具。
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# LightClawBot 文件上传与下载
|
|
7
|
-
|
|
8
|
-
让 AI 帮用户处理文件的上传、下载和分享,通过 LightClawBot 通道投递。
|
|
9
|
-
|
|
10
|
-
---
|
|
11
|
-
|
|
12
|
-
## ⛔ 最重要的规则(读三遍)
|
|
13
|
-
|
|
14
|
-
> 1. **上传文件必须使用 `lightclaw_upload_file` 工具,禁止使用其他任何存储工具!**
|
|
15
|
-
> 3. **返回文件给用户时,必须使用标准 Markdown 链接格式:`[文件名](下载链接)`**
|
|
16
|
-
> - ❌ 错误:`下载链接: https://xxx`
|
|
17
|
-
> - ❌ 错误:`📎 文件下载链接: https://xxx`
|
|
18
|
-
> - ✅ 正确:`[report.pdf](https://lightai.cloud.tencent.com/drive/preview?filePath=2026-03-15/report.pdf)`
|
|
19
|
-
|
|
20
|
-
---
|
|
21
|
-
|
|
22
|
-
## 🔧 可用工具
|
|
23
|
-
|
|
24
|
-
本技能使用以下两个专属工具,**不要使用其他任何文件/存储工具**:
|
|
25
|
-
|
|
26
|
-
| 工具名 | 用途 | 何时使用 |
|
|
27
|
-
|--------|------|----------|
|
|
28
|
-
| `lightclaw_upload_file` | 上传本地文件到云端,获取公网下载链接 | AI 生成了文件需要分享给用户时 |
|
|
29
|
-
|
|
30
|
-
---
|
|
31
|
-
|
|
32
|
-
## 📤 上传文件(lightclaw_upload_file)
|
|
33
|
-
|
|
34
|
-
### 功能
|
|
35
|
-
|
|
36
|
-
将本地文件上传到云端存储,返回公网可访问的下载链接。支持批量上传(最多 5 个文件)。
|
|
37
|
-
|
|
38
|
-
### 参数
|
|
39
|
-
|
|
40
|
-
```json
|
|
41
|
-
{
|
|
42
|
-
"paths": ["/absolute/path/to/file1.pdf", "/absolute/path/to/file2.xlsx"]
|
|
43
|
-
}
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
| 参数 | 类型 | 必填 | 说明 |
|
|
47
|
-
|------|------|------|------|
|
|
48
|
-
| `paths` | string[] | ✅ | 本地文件绝对路径数组,最多 5 个 |
|
|
49
|
-
|
|
50
|
-
### ⚠️ 关键约束
|
|
51
|
-
|
|
52
|
-
1. **必须使用绝对路径**(以 `/` 开头),禁止使用相对路径
|
|
53
|
-
- ❌ `./output/report.pdf`
|
|
54
|
-
- ✅ `/Users/xxx/.openclaw/workspace/output/report.pdf`
|
|
55
|
-
2. **文件必须存在**:上传前确认文件已生成并写入磁盘
|
|
56
|
-
3. **单次最多 5 个文件**:超出需分批上传
|
|
57
|
-
|
|
58
|
-
### 使用示例
|
|
59
|
-
|
|
60
|
-
**上传单个文件**:
|
|
61
|
-
```json
|
|
62
|
-
{
|
|
63
|
-
"paths": ["/Users/xxx/.openclaw/workspace/report.pdf"]
|
|
64
|
-
}
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
**批量上传**:
|
|
68
|
-
```json
|
|
69
|
-
{
|
|
70
|
-
"paths": [
|
|
71
|
-
"/Users/xxx/.openclaw/workspace/chart1.png",
|
|
72
|
-
"/Users/xxx/.openclaw/workspace/chart2.png",
|
|
73
|
-
"/Users/xxx/.openclaw/workspace/summary.xlsx"
|
|
74
|
-
]
|
|
75
|
-
}
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
---
|
|
79
|
-
|
|
80
|
-
## 📋 返回文件给用户的格式(最重要)
|
|
81
|
-
|
|
82
|
-
### 🚨🚨🚨 必须使用标准 Markdown 链接格式
|
|
83
|
-
|
|
84
|
-
> **当需要将文件/文档/图片链接返回给用户时,必须使用标准 Markdown 链接格式:**
|
|
85
|
-
>
|
|
86
|
-
> ```
|
|
87
|
-
> [<文件名>](<下载链接>)
|
|
88
|
-
> ```
|
|
89
|
-
|
|
90
|
-
### ✅ 正确示例
|
|
91
|
-
|
|
92
|
-
**单个文件**:
|
|
93
|
-
|
|
94
|
-
```
|
|
95
|
-
好的,报告已生成,请点击下载:
|
|
96
|
-
|
|
97
|
-
[report.pdf](https://lightai.cloud.tencent.com/drive/preview?filePath=2026-03-15/report.pdf)
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
**多个文件**:
|
|
101
|
-
|
|
102
|
-
```
|
|
103
|
-
所有文件已准备好:
|
|
104
|
-
|
|
105
|
-
- [数据分析报告.pdf](https://lightai.cloud.tencent.com/drive/preview?filePath=2026-03-15/数据分析报告.pdf)
|
|
106
|
-
- [原始数据.xlsx](https://lightai.cloud.tencent.com/drive/preview?filePath=2026-03-15/原始数据.xlsx)
|
|
107
|
-
- [趋势图.png](https://lightai.cloud.tencent.com/drive/preview?filePath=2026-03-15/趋势图.png)
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
**附带说明文字**:
|
|
111
|
-
|
|
112
|
-
```
|
|
113
|
-
✅ 周报已生成完毕!
|
|
114
|
-
|
|
115
|
-
[本周工作周报.pdf](https://lightai.cloud.tencent.com/drive/preview?filePath=2026-03-15/本周工作周报.pdf)
|
|
116
|
-
|
|
117
|
-
报告包含了本周的进度汇总和下周计划。
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
### ❌ 错误示例(绝对不要这样做)
|
|
121
|
-
|
|
122
|
-
❌ **裸链接**:
|
|
123
|
-
```
|
|
124
|
-
下载链接: https://lightai.cloud.tencent.com/drive/preview?filePath=2026-03-15/report.pdf
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
❌ **带 emoji 前缀的裸链接**:
|
|
128
|
-
```
|
|
129
|
-
📎 文件下载链接: https://lightai.cloud.tencent.com/drive/preview?filePath=2026-03-15/report.pdf
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
❌ **使用代码块包裹链接**:
|
|
133
|
-
```
|
|
134
|
-
`https://lightai.cloud.tencent.com/drive/preview?filePath=2026-03-15/report.pdf`
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
❌ **只返回路径,没有文件名**:
|
|
138
|
-
```
|
|
139
|
-
[下载](https://lightai.cloud.tencent.com/drive/preview?filePath=2026-03-15/report.pdf)
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
---
|
|
143
|
-
|
|
144
|
-
## 📸 接收用户文件
|
|
145
|
-
|
|
146
|
-
用户通过 LightClawBot 发来的文件(图片、文档等)**自动下载到本地**。文件信息在上下文的【Attachments】字段中,包含:
|
|
147
|
-
|
|
148
|
-
| 字段 | 说明 |
|
|
149
|
-
|------|------|
|
|
150
|
-
| `name` | 文件名 |
|
|
151
|
-
| `mimeType` | 文件 MIME 类型 |
|
|
152
|
-
| `url` | 文件的公网下载链接 |
|
|
153
|
-
|
|
154
|
-
本地文件路径在上下文的 `MediaPath` / `MediaPaths` 字段中。
|
|
155
|
-
|
|
156
|
-
### 处理用户文件的流程
|
|
157
|
-
|
|
158
|
-
1. **查看上下文**:从 Attachments 获取文件信息
|
|
159
|
-
2. **本地处理**:使用 `MediaPath` 中的本地路径读取和处理文件
|
|
160
|
-
3. **如需返回处理后的文件**:用 `lightclaw_upload_file` 上传 → 用 Markdown 链接返回
|
|
161
|
-
|
|
162
|
-
---
|
|
163
|
-
|
|
164
|
-
## 🎯 完整使用场景
|
|
165
|
-
|
|
166
|
-
### 场景 1:用户要求生成报告
|
|
167
|
-
|
|
168
|
-
**用户**:帮我生成一份数据分析报告
|
|
169
|
-
|
|
170
|
-
**AI 执行步骤**:
|
|
171
|
-
1. 生成报告文件并保存到本地(如 `/Users/xxx/.openclaw/workspace/数据分析报告.pdf`)
|
|
172
|
-
2. 调用 `lightclaw_upload_file` 上传:
|
|
173
|
-
```json
|
|
174
|
-
{ "paths": ["/Users/xxx/.openclaw/workspace/数据分析报告.pdf"] }
|
|
175
|
-
```
|
|
176
|
-
3. 获取到下载链接后,以 Markdown 格式回复:
|
|
177
|
-
```
|
|
178
|
-
✅ 报告已生成:
|
|
179
|
-
|
|
180
|
-
[数据分析报告.pdf](https://lightai.cloud.tencent.com/drive/preview?filePath=2026-03-15/数据分析报告.pdf)
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
### 场景 2:用户要求处理发来的文件
|
|
184
|
-
|
|
185
|
-
**用户**:帮我把这个 Excel 转成 PDF(附带了 data.xlsx)
|
|
186
|
-
|
|
187
|
-
**AI 执行步骤**:
|
|
188
|
-
1. 从上下文 `MediaPath` 获取本地文件路径
|
|
189
|
-
2. 读取并处理 Excel 文件,转换为 PDF
|
|
190
|
-
3. 调用 `lightclaw_upload_file` 上传 PDF
|
|
191
|
-
4. 以 Markdown 格式回复:
|
|
192
|
-
```
|
|
193
|
-
✅ 已将 Excel 转为 PDF:
|
|
194
|
-
|
|
195
|
-
[data.pdf](https://lightai.cloud.tencent.com/drive/preview?filePath=2026-03-15/data.pdf)
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
### 场景 3:用户要求获取之前上传的文件
|
|
199
|
-
|
|
200
|
-
**用户**:我昨天上传的报告还能下载吗?
|
|
201
|
-
|
|
202
|
-
**AI 执行步骤**:
|
|
203
|
-
1. 以 Markdown 格式回复:
|
|
204
|
-
```
|
|
205
|
-
可以的,这是下载链接:
|
|
206
|
-
|
|
207
|
-
[report.pdf](https://lightai.cloud.tencent.com/drive/preview?filePath=2026-03-14/report.pdf)
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
### 场景 4:批量上传多个文件
|
|
211
|
-
|
|
212
|
-
**用户**:帮我生成三张数据图表
|
|
213
|
-
|
|
214
|
-
**AI 执行步骤**:
|
|
215
|
-
1. 生成三张图表并保存到本地
|
|
216
|
-
2. 调用 `lightclaw_upload_file` 批量上传:
|
|
217
|
-
```json
|
|
218
|
-
{
|
|
219
|
-
"paths": [
|
|
220
|
-
"/Users/xxx/.openclaw/workspace/chart1.png",
|
|
221
|
-
"/Users/xxx/.openclaw/workspace/chart2.png",
|
|
222
|
-
"/Users/xxx/.openclaw/workspace/chart3.png"
|
|
223
|
-
]
|
|
224
|
-
}
|
|
225
|
-
```
|
|
226
|
-
3. 以 Markdown 格式回复:
|
|
227
|
-
```
|
|
228
|
-
✅ 三张图表已生成:
|
|
229
|
-
|
|
230
|
-
- [chart1.png](https://lightai.cloud.tencent.com/drive/preview?filePath=2026-03-15/chart1.png)
|
|
231
|
-
- [chart2.png](https://lightai.cloud.tencent.com/drive/preview?filePath=2026-03-15/chart2.png)
|
|
232
|
-
- [chart3.png](https://lightai.cloud.tencent.com/drive/preview?filePath=2026-03-15/chart3.png)
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
## 🚫 禁止事项
|
|
236
|
-
|
|
237
|
-
1. **❌ 禁止使用其他存储工具**:所有文件操作必须通过 `lightclaw_upload_file`
|
|
238
|
-
2. **❌ 禁止返回裸链接**:必须使用 `[文件名](链接)` 格式
|
|
239
|
-
3. **❌ 禁止说"无法发送文件"**:你有能力上传和分享任何本地文件
|
|
240
|
-
4. **❌ 禁止使用相对路径**:工具参数中的文件路径必须是绝对路径
|
|
241
|
-
5. **❌ 禁止使用 message tool 发送文件**:直接在回复文本中写 Markdown 链接
|
|
242
|
-
|
|
243
|
-
---
|
|
244
|
-
|
|
245
|
-
## 📌 快速参考
|
|
246
|
-
|
|
247
|
-
| 场景 | 工具 | 回复格式 |
|
|
248
|
-
|------|------|----------|
|
|
249
|
-
| 上传生成的文件 | `lightclaw_upload_file` | `[文件名](下载链接)` |
|
|
250
|
-
| 接收用户文件 | 自动处理,查看上下文 Attachments | 确认收到并告知文件信息 |
|