@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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.ts +148 -35
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vui-design/openclaw-plugin-feishu-progress",
3
- "version": "0.2.5",
3
+ "version": "0.3.0",
4
4
  "description": "飞书任务进度卡片插件 — 在 AI 执行复杂多步骤任务时,实时更新飞书进度卡片",
5
5
  "keywords": [
6
6
  "openclaw-plugin",
package/src/index.ts CHANGED
@@ -1,28 +1,132 @@
1
1
  import { FeishuClient } from './feishu-client.js';
2
2
 
3
- // 常见工具名称中文映射
4
- const TOOL_LABEL: Record<string, string> = {
5
- read_file: '读取文件',
6
- write_file: '写入文件',
7
- list_directory: '浏览目录',
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 toolLabel(name: string): string {
20
- return TOOL_LABEL[name] ?? name;
9
+ function str(v: unknown, max = 60): string {
10
+ return String(v ?? '').slice(0, max).trim();
21
11
  }
22
12
 
23
- // sessionKey conversationId(飞书 chat_id)
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 未配置,进度卡片功能已禁用。\n' +
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. 收到用户消息时,记录当前会话对应的飞书 chat_id ────────────────────
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 内部格式为 "user:ou_xxx" / "group:oc_xxx",需剥离前缀取裸 Feishu ID
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
- const key = ctx.accountId ?? 'default';
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
- const label = toolLabel(event.toolName);
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, `🔧 正在执行:${label}`);
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
- // ── 3. 任务结束后清理 session 映射 ───────────────────────────────────────
195
+ // ── 4. 任务结束后清理 session 映射 ─────────────────────────────────────
81
196
  api.on('agent_end', async (_event: any, ctx: any) => {
82
- const key = ctx.agentId ?? ctx.sessionKey ?? 'default';
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
  };