@vui-design/openclaw-plugin-feishu-progress 0.2.5 → 0.3.0
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 +1 -1
- package/src/index.ts +148 -35
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,28 +1,132 @@
|
|
|
1
1
|
import { FeishuClient } from './feishu-client.js';
|
|
2
2
|
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
bash: '执行命令',
|
|
9
|
-
web_search: '网络搜索',
|
|
10
|
-
tavily_search: '网络搜索',
|
|
11
|
-
web_fetch: '抓取网页',
|
|
12
|
-
computer: '操作电脑',
|
|
13
|
-
screenshot: '截图',
|
|
14
|
-
str_replace_editor: '编辑文件',
|
|
15
|
-
create_file: '创建文件',
|
|
16
|
-
delete_file: '删除文件',
|
|
17
|
-
};
|
|
3
|
+
// ── 工具名称标签构建(解析实际参数,生成语义化中文描述)──────────────────────
|
|
4
|
+
|
|
5
|
+
function basename(p: string): string {
|
|
6
|
+
return p.split(/[/\\]/).filter(Boolean).pop() ?? p;
|
|
7
|
+
}
|
|
18
8
|
|
|
19
|
-
function
|
|
20
|
-
return
|
|
9
|
+
function str(v: unknown, max = 60): string {
|
|
10
|
+
return String(v ?? '').slice(0, max).trim();
|
|
21
11
|
}
|
|
22
12
|
|
|
23
|
-
|
|
13
|
+
function buildToolLabel(toolName: string, params: Record<string, unknown>): string {
|
|
14
|
+
switch (toolName) {
|
|
15
|
+
case 'exec':
|
|
16
|
+
case 'bash': {
|
|
17
|
+
const cmd = str(params?.command ?? params?.cmd ?? params?.input, 150);
|
|
18
|
+
|
|
19
|
+
const brewInstall = cmd.match(/brew\s+install\s+([\w@./-]+)/);
|
|
20
|
+
if (brewInstall) return `📦 安装 ${brewInstall[1]}`;
|
|
21
|
+
const brewUninstall = cmd.match(/brew\s+uninstall\s+([\w@./-]+)/);
|
|
22
|
+
if (brewUninstall) return `🗑️ 卸载 ${brewUninstall[1]}`;
|
|
23
|
+
|
|
24
|
+
const which = cmd.match(/^(?:which|command\s+-v)\s+([\w.-]+)/);
|
|
25
|
+
if (which) return `🔍 检查 ${which[1]} 是否已安装`;
|
|
26
|
+
|
|
27
|
+
const npmInstall = cmd.match(/(?:npm|pnpm|yarn)\s+(?:install|add)\s+([\w@./-]+)?/);
|
|
28
|
+
if (npmInstall) return npmInstall[1] ? `📦 安装 ${npmInstall[1]}` : '📦 安装依赖';
|
|
29
|
+
const npmRun = cmd.match(/(?:npm|pnpm|yarn)\s+run\s+([\w:.-]+)/);
|
|
30
|
+
if (npmRun) return `▶️ 运行 ${npmRun[1]}`;
|
|
31
|
+
|
|
32
|
+
const pip = cmd.match(/pip(?:\d)?\s+install\s+([\w@.-]+)/);
|
|
33
|
+
if (pip) return `📦 安装 ${pip[1]}`;
|
|
34
|
+
|
|
35
|
+
const gitClone = cmd.match(/git\s+clone\s+(?:.*\/)?([^/\s]+?)(?:\.git)?(?:\s|$)/);
|
|
36
|
+
if (gitClone) return `📥 克隆仓库 ${gitClone[1]}`;
|
|
37
|
+
if (/git\s+commit/.test(cmd)) return '💾 提交代码';
|
|
38
|
+
if (/git\s+push/.test(cmd)) return '🚀 推送代码';
|
|
39
|
+
if (/git\s+pull/.test(cmd)) return '⬇️ 拉取代码';
|
|
40
|
+
|
|
41
|
+
const mkdir = cmd.match(/^mkdir\s+(?:-[\w]+\s+)?([\w./~-]+)/);
|
|
42
|
+
if (mkdir) return `📁 创建目录 ${basename(mkdir[1])}`;
|
|
43
|
+
|
|
44
|
+
const rm = cmd.match(/^rm\s+(?:-[\w]+\s+)?([\w./~-]+)/);
|
|
45
|
+
if (rm) return `🗑️ 删除 ${basename(rm[1])}`;
|
|
46
|
+
|
|
47
|
+
const curlUrl = cmd.match(/(?:curl|wget)\s+.*?(https?:\/\/[^\s]+)/);
|
|
48
|
+
if (curlUrl) { try { return `🌐 请求 ${new URL(curlUrl[1]).hostname}`; } catch { /**/ } }
|
|
49
|
+
|
|
50
|
+
if (cmd) return `⚡ ${cmd.slice(0, 40)}`;
|
|
51
|
+
return '⚡ 执行命令';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
case 'process': {
|
|
55
|
+
const cmd = str(params?.command ?? params?.cmd, 80);
|
|
56
|
+
return cmd ? `⚡ 运行:${cmd.slice(0, 40)}` : '⚡ 执行进程';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
case 'read':
|
|
60
|
+
case 'read_file': {
|
|
61
|
+
const p = str(params?.path ?? params?.file_path ?? params?.filePath);
|
|
62
|
+
return p ? `📂 读取 ${basename(p)}` : '📂 读取文件';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
case 'write':
|
|
66
|
+
case 'write_file':
|
|
67
|
+
case 'create_file': {
|
|
68
|
+
const p = str(params?.path ?? params?.file_path ?? params?.filePath);
|
|
69
|
+
return p ? `✏️ 写入 ${basename(p)}` : '✏️ 写入文件';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
case 'str_replace_editor':
|
|
73
|
+
case 'edit_file': {
|
|
74
|
+
const p = str(params?.path ?? params?.file_path);
|
|
75
|
+
return p ? `✏️ 编辑 ${basename(p)}` : '✏️ 编辑文件';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
case 'list_directory': {
|
|
79
|
+
const p = str(params?.path ?? params?.directory);
|
|
80
|
+
return p ? `📋 浏览 ${basename(p) || p}` : '📋 浏览目录';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
case 'search':
|
|
84
|
+
case 'web_search':
|
|
85
|
+
case 'tavily_search': {
|
|
86
|
+
const q = str(params?.query ?? params?.q, 30);
|
|
87
|
+
return q ? `🌐 搜索「${q}」` : '🌐 网络搜索';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
case 'fetch':
|
|
91
|
+
case 'web_fetch': {
|
|
92
|
+
const url = str(params?.url ?? params?.href);
|
|
93
|
+
try { return `🌐 访问 ${new URL(url).hostname}`; } catch { /**/ }
|
|
94
|
+
return '🌐 抓取网页';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
case 'computer': {
|
|
98
|
+
const action = str(params?.action);
|
|
99
|
+
return action ? `🖥️ 操作电脑:${action}` : '🖥️ 操作电脑';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
case 'screenshot': return '📸 截图';
|
|
103
|
+
|
|
104
|
+
case 'delete_file': {
|
|
105
|
+
const p = str(params?.path ?? params?.file_path);
|
|
106
|
+
return p ? `🗑️ 删除 ${basename(p)}` : '🗑️ 删除文件';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
default: return `🔧 ${toolName}`;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ── 提取 AI 文本中最有意义的一句(用于工具调用前的说明)─────────────────────
|
|
114
|
+
|
|
115
|
+
function extractSnippet(text: string): string {
|
|
116
|
+
// 取最后一段非空文本,截取前 50 字
|
|
117
|
+
const lines = text.split(/\n+/).map(l => l.trim()).filter(Boolean);
|
|
118
|
+
const last = lines[lines.length - 1] ?? '';
|
|
119
|
+
return last.slice(0, 50);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ── 主插件逻辑 ──────────────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
// agentId → 飞书裸 ID(ou_xxx / oc_xxx)
|
|
24
125
|
const sessionChatMap = new Map<string, string>();
|
|
25
126
|
|
|
127
|
+
// runId → 最近一次 LLM turn 的 AI 文本(用于 before_tool_call 显示上下文)
|
|
128
|
+
const runTextMap = new Map<string, string>();
|
|
129
|
+
|
|
26
130
|
export default {
|
|
27
131
|
id: 'feishu-progress',
|
|
28
132
|
|
|
@@ -31,7 +135,7 @@ export default {
|
|
|
31
135
|
|
|
32
136
|
if (!cfg.appId || !cfg.appSecret) {
|
|
33
137
|
api.logger?.warn(
|
|
34
|
-
'[feishu-progress] appId / appSecret
|
|
138
|
+
'[feishu-progress] appId / appSecret 未配置,进度通知已禁用。\n' +
|
|
35
139
|
'请执行:\n' +
|
|
36
140
|
' openclaw config set plugins.entries.feishu-progress.config.appId "cli_xxx"\n' +
|
|
37
141
|
' openclaw config set plugins.entries.feishu-progress.config.appSecret "xxx"\n' +
|
|
@@ -46,42 +150,51 @@ export default {
|
|
|
46
150
|
domain: cfg.domain ?? 'feishu',
|
|
47
151
|
});
|
|
48
152
|
|
|
49
|
-
// ── 1.
|
|
153
|
+
// ── 1. 收到用户消息时,记录 agentId → 飞书 ID ────────────────────────────
|
|
50
154
|
api.on('message_received', async (_event: any, ctx: any) => {
|
|
51
|
-
api.logger.info(`[feishu-progress][DEBUG] message_received: channelId=${ctx.channelId} accountId=${ctx.accountId} conversationId=${ctx.conversationId}`);
|
|
52
155
|
if (ctx.channelId !== 'feishu') return;
|
|
53
156
|
if (!ctx.conversationId) return;
|
|
54
|
-
// OpenClaw
|
|
157
|
+
// OpenClaw 内部格式 "user:ou_xxx" / "group:oc_xxx",剥离前缀取裸 Feishu ID
|
|
55
158
|
const rawId = ctx.conversationId.includes(':')
|
|
56
159
|
? ctx.conversationId.split(':').pop()!
|
|
57
160
|
: ctx.conversationId;
|
|
58
|
-
|
|
59
|
-
sessionChatMap.set(key, rawId);
|
|
60
|
-
api.logger.info(`[feishu-progress][DEBUG] stored: key=${key} rawId=${rawId} (original=${ctx.conversationId})`);
|
|
161
|
+
sessionChatMap.set(ctx.accountId ?? 'default', rawId);
|
|
61
162
|
});
|
|
62
163
|
|
|
63
|
-
// ── 2.
|
|
164
|
+
// ── 2. LLM 完成一轮生成后,缓存 AI 文本供 before_tool_call 使用 ──────────
|
|
165
|
+
api.on('llm_output', async (event: any, _ctx: any) => {
|
|
166
|
+
if (!event.runId) return;
|
|
167
|
+
const texts: string[] = event.assistantTexts ?? [];
|
|
168
|
+
const text = texts.join('').trim();
|
|
169
|
+
if (text) runTextMap.set(event.runId, text);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// ── 3. 工具调用前,推送进度到飞书 ───────────────────────────────────────
|
|
64
173
|
api.on('before_tool_call', async (event: any, ctx: any) => {
|
|
65
|
-
api.logger.info(`[feishu-progress][DEBUG] before_tool_call: tool=${event.toolName} agentId=${ctx.agentId} sessionKey=${ctx.sessionKey}`);
|
|
66
174
|
const key = ctx.agentId ?? ctx.sessionKey ?? 'default';
|
|
67
175
|
const chatId = sessionChatMap.get(key);
|
|
68
|
-
api.logger.info(`[feishu-progress][DEBUG] lookup: key=${key} chatId=${chatId} mapSize=${sessionChatMap.size}`);
|
|
69
176
|
if (!chatId) return;
|
|
70
177
|
|
|
71
|
-
|
|
178
|
+
// AI 文本优先(取自本轮 LLM 输出的最后一句),没有则用参数解析标签
|
|
179
|
+
let label: string;
|
|
180
|
+
if (event.runId && runTextMap.has(event.runId)) {
|
|
181
|
+
const snippet = extractSnippet(runTextMap.get(event.runId)!);
|
|
182
|
+
runTextMap.delete(event.runId); // 每轮只用一次
|
|
183
|
+
label = snippet || buildToolLabel(event.toolName, event.params ?? {});
|
|
184
|
+
} else {
|
|
185
|
+
label = buildToolLabel(event.toolName, event.params ?? {});
|
|
186
|
+
}
|
|
187
|
+
|
|
72
188
|
try {
|
|
73
|
-
await client.sendText(chatId,
|
|
74
|
-
api.logger.info(`[feishu-progress][DEBUG] sendText OK: chatId=${chatId} label=${label}`);
|
|
189
|
+
await client.sendText(chatId, label);
|
|
75
190
|
} catch (err: any) {
|
|
76
191
|
api.logger?.warn(`[feishu-progress] 推送失败: ${err?.message}`);
|
|
77
192
|
}
|
|
78
193
|
});
|
|
79
194
|
|
|
80
|
-
// ──
|
|
195
|
+
// ── 4. 任务结束后清理 session 映射 ─────────────────────────────────────
|
|
81
196
|
api.on('agent_end', async (_event: any, ctx: any) => {
|
|
82
|
-
|
|
83
|
-
api.logger.info(`[feishu-progress][DEBUG] agent_end: key=${key}`);
|
|
84
|
-
sessionChatMap.delete(key);
|
|
197
|
+
sessionChatMap.delete(ctx.agentId ?? ctx.sessionKey ?? 'default');
|
|
85
198
|
});
|
|
86
199
|
},
|
|
87
200
|
};
|