@vui-design/openclaw-plugin-feishu-progress 0.2.6 → 0.4.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 +190 -43
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,35 +1,128 @@
|
|
|
1
1
|
import { FeishuClient } from './feishu-client.js';
|
|
2
2
|
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
read: '读取文件',
|
|
9
|
-
write: '写入文件',
|
|
10
|
-
search: '网络搜索',
|
|
11
|
-
fetch: '抓取网页',
|
|
12
|
-
// Claude 工具名
|
|
13
|
-
read_file: '读取文件',
|
|
14
|
-
write_file: '写入文件',
|
|
15
|
-
list_directory: '浏览目录',
|
|
16
|
-
bash: '执行命令',
|
|
17
|
-
web_search: '网络搜索',
|
|
18
|
-
tavily_search: '网络搜索',
|
|
19
|
-
web_fetch: '抓取网页',
|
|
20
|
-
computer: '操作电脑',
|
|
21
|
-
screenshot: '截图',
|
|
22
|
-
str_replace_editor: '编辑文件',
|
|
23
|
-
create_file: '创建文件',
|
|
24
|
-
delete_file: '删除文件',
|
|
25
|
-
};
|
|
3
|
+
// ── 工具参数 → 语义化中文标签 ────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
function basename(p: string): string {
|
|
6
|
+
return p.split(/[/\\]/).filter(Boolean).pop() ?? p;
|
|
7
|
+
}
|
|
26
8
|
|
|
27
|
-
function
|
|
28
|
-
return
|
|
9
|
+
function s(v: unknown, max = 60): string {
|
|
10
|
+
return String(v ?? '').slice(0, max).trim();
|
|
29
11
|
}
|
|
30
12
|
|
|
31
|
-
|
|
32
|
-
|
|
13
|
+
function buildToolLabel(toolName: string, args: Record<string, unknown>): string {
|
|
14
|
+
switch (toolName) {
|
|
15
|
+
case 'exec':
|
|
16
|
+
case 'bash': {
|
|
17
|
+
const cmd = s(args?.command ?? args?.cmd ?? args?.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
|
+
const which = cmd.match(/^(?:which|command\s+-v)\s+([\w.-]+)/);
|
|
24
|
+
if (which) return `🔍 检查 ${which[1]} 是否已安装`;
|
|
25
|
+
|
|
26
|
+
const npmInstall = cmd.match(/(?:npm|pnpm|yarn)\s+(?:install|add)\s+([\w@./-]+)?/);
|
|
27
|
+
if (npmInstall) return npmInstall[1] ? `📦 安装 ${npmInstall[1]}` : '📦 安装依赖';
|
|
28
|
+
const npmRun = cmd.match(/(?:npm|pnpm|yarn)\s+run\s+([\w:.-]+)/);
|
|
29
|
+
if (npmRun) return `▶️ 运行 ${npmRun[1]}`;
|
|
30
|
+
|
|
31
|
+
const pip = cmd.match(/pip(?:\d)?\s+install\s+([\w@.-]+)/);
|
|
32
|
+
if (pip) return `📦 安装 ${pip[1]}`;
|
|
33
|
+
|
|
34
|
+
const gitClone = cmd.match(/git\s+clone\s+(?:.*\/)?([^/\s]+?)(?:\.git)?(?:\s|$)/);
|
|
35
|
+
if (gitClone) return `📥 克隆仓库 ${gitClone[1]}`;
|
|
36
|
+
if (/git\s+commit/.test(cmd)) return '💾 提交代码';
|
|
37
|
+
if (/git\s+push/.test(cmd)) return '🚀 推送代码';
|
|
38
|
+
if (/git\s+pull/.test(cmd)) return '⬇️ 拉取代码';
|
|
39
|
+
|
|
40
|
+
const mkdir = cmd.match(/^mkdir\s+(?:-[\w]+\s+)?([\w./~-]+)/);
|
|
41
|
+
if (mkdir) return `📁 创建目录 ${basename(mkdir[1])}`;
|
|
42
|
+
const rm = cmd.match(/^rm\s+(?:-[\w]+\s+)?([\w./~-]+)/);
|
|
43
|
+
if (rm) return `🗑️ 删除 ${basename(rm[1])}`;
|
|
44
|
+
|
|
45
|
+
const curlUrl = cmd.match(/(?:curl|wget)\s+.*?(https?:\/\/[^\s]+)/);
|
|
46
|
+
if (curlUrl) { try { return `🌐 请求 ${new URL(curlUrl[1]).hostname}`; } catch { /**/ } }
|
|
47
|
+
|
|
48
|
+
if (cmd) return `⚡ ${cmd.slice(0, 40)}`;
|
|
49
|
+
return '⚡ 执行命令';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
case 'process': {
|
|
53
|
+
const cmd = s(args?.command ?? args?.cmd, 80);
|
|
54
|
+
return cmd ? `⚡ ${cmd.slice(0, 40)}` : '⚡ 执行进程';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
case 'read':
|
|
58
|
+
case 'read_file': {
|
|
59
|
+
const p = s(args?.path ?? args?.file_path ?? args?.filePath);
|
|
60
|
+
return p ? `📂 读取 ${basename(p)}` : '📂 读取文件';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
case 'write':
|
|
64
|
+
case 'write_file':
|
|
65
|
+
case 'create_file': {
|
|
66
|
+
const p = s(args?.path ?? args?.file_path ?? args?.filePath);
|
|
67
|
+
return p ? `✏️ 写入 ${basename(p)}` : '✏️ 写入文件';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
case 'str_replace_editor':
|
|
71
|
+
case 'edit_file': {
|
|
72
|
+
const p = s(args?.path ?? args?.file_path);
|
|
73
|
+
return p ? `✏️ 编辑 ${basename(p)}` : '✏️ 编辑文件';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
case 'list_directory': {
|
|
77
|
+
const p = s(args?.path ?? args?.directory);
|
|
78
|
+
return p ? `📋 浏览 ${basename(p) || p}` : '📋 浏览目录';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
case 'search':
|
|
82
|
+
case 'web_search':
|
|
83
|
+
case 'tavily_search': {
|
|
84
|
+
const q = s(args?.query ?? args?.q, 30);
|
|
85
|
+
return q ? `🌐 搜索「${q}」` : '🌐 网络搜索';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
case 'fetch':
|
|
89
|
+
case 'web_fetch': {
|
|
90
|
+
const url = s(args?.url ?? args?.href);
|
|
91
|
+
try { return `🌐 访问 ${new URL(url).hostname}`; } catch { /**/ }
|
|
92
|
+
return '🌐 抓取网页';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
case 'computer': {
|
|
96
|
+
const action = s(args?.action);
|
|
97
|
+
return action ? `🖥️ 操作电脑:${action}` : '🖥️ 操作电脑';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
case 'screenshot': return '📸 截图';
|
|
101
|
+
|
|
102
|
+
case 'delete_file': {
|
|
103
|
+
const p = s(args?.path ?? args?.file_path);
|
|
104
|
+
return p ? `🗑️ 删除 ${basename(p)}` : '🗑️ 删除文件';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
default: return `🔧 ${toolName}`;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 从 AI 输出文本中提取最末一句(作为进度提示优先显示)
|
|
112
|
+
function extractSnippet(text: string): string {
|
|
113
|
+
const lines = text.split(/\n+/).map(l => l.trim()).filter(Boolean);
|
|
114
|
+
return (lines[lines.length - 1] ?? '').slice(0, 50);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── 主插件 ────────────────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
interface RunState {
|
|
120
|
+
chatId: string;
|
|
121
|
+
assistantBuffer: string;
|
|
122
|
+
startedAt: number;
|
|
123
|
+
lastSentAt: number;
|
|
124
|
+
heartbeatTimer: ReturnType<typeof setInterval>;
|
|
125
|
+
}
|
|
33
126
|
|
|
34
127
|
export default {
|
|
35
128
|
id: 'feishu-progress',
|
|
@@ -39,7 +132,7 @@ export default {
|
|
|
39
132
|
|
|
40
133
|
if (!cfg.appId || !cfg.appSecret) {
|
|
41
134
|
api.logger?.warn(
|
|
42
|
-
'[feishu-progress] appId / appSecret
|
|
135
|
+
'[feishu-progress] appId / appSecret 未配置,进度通知已禁用。\n' +
|
|
43
136
|
'请执行:\n' +
|
|
44
137
|
' openclaw config set plugins.entries.feishu-progress.config.appId "cli_xxx"\n' +
|
|
45
138
|
' openclaw config set plugins.entries.feishu-progress.config.appSecret "xxx"\n' +
|
|
@@ -54,32 +147,86 @@ export default {
|
|
|
54
147
|
domain: cfg.domain ?? 'feishu',
|
|
55
148
|
});
|
|
56
149
|
|
|
57
|
-
//
|
|
150
|
+
// accountId ("main") → 飞书裸 ID(ou_xxx / oc_xxx)
|
|
151
|
+
const accountChatMap = new Map<string, string>();
|
|
152
|
+
|
|
153
|
+
// runId → RunState(每次 agent run 的状态)
|
|
154
|
+
const runStateMap = new Map<string, RunState>();
|
|
155
|
+
|
|
156
|
+
// ── 1. 收到用户消息 → 记录 accountId 对应的飞书 chat ID ─────────────────
|
|
58
157
|
api.on('message_received', async (_event: any, ctx: any) => {
|
|
59
158
|
if (ctx.channelId !== 'feishu') return;
|
|
60
159
|
if (!ctx.conversationId) return;
|
|
61
|
-
// OpenClaw
|
|
160
|
+
// OpenClaw 内部格式 "user:ou_xxx",剥离前缀
|
|
62
161
|
const rawId = ctx.conversationId.includes(':')
|
|
63
162
|
? ctx.conversationId.split(':').pop()!
|
|
64
163
|
: ctx.conversationId;
|
|
65
|
-
|
|
164
|
+
accountChatMap.set(ctx.accountId ?? 'default', rawId);
|
|
66
165
|
});
|
|
67
166
|
|
|
68
|
-
// ── 2.
|
|
69
|
-
api.on('
|
|
70
|
-
|
|
71
|
-
|
|
167
|
+
// ── 2. LLM 调用前 → 用 runId+agentId 建立关联,并启动心跳 ─────────────
|
|
168
|
+
api.on('llm_input', async (event: any, ctx: any) => {
|
|
169
|
+
if (!event.runId || !ctx.agentId) return;
|
|
170
|
+
if (runStateMap.has(event.runId)) return; // 同一 run 只初始化一次
|
|
171
|
+
|
|
172
|
+
const chatId = accountChatMap.get(ctx.agentId);
|
|
72
173
|
if (!chatId) return;
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
174
|
+
|
|
175
|
+
const startedAt = Date.now();
|
|
176
|
+
const state: RunState = {
|
|
177
|
+
chatId,
|
|
178
|
+
assistantBuffer: '',
|
|
179
|
+
startedAt,
|
|
180
|
+
lastSentAt: startedAt,
|
|
181
|
+
heartbeatTimer: setInterval(() => {
|
|
182
|
+
if (!runStateMap.has(event.runId)) return;
|
|
183
|
+
const idleSec = (Date.now() - state.lastSentAt) / 1000;
|
|
184
|
+
if (idleSec >= 55) {
|
|
185
|
+
const elapsed = Math.round((Date.now() - state.startedAt) / 1000);
|
|
186
|
+
state.lastSentAt = Date.now();
|
|
187
|
+
client.sendText(chatId, `⏳ 处理中,已用时约 ${elapsed} 秒...`).catch(() => {});
|
|
188
|
+
}
|
|
189
|
+
}, 30_000),
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
runStateMap.set(event.runId, state);
|
|
78
193
|
});
|
|
79
194
|
|
|
80
|
-
// ── 3.
|
|
81
|
-
api.
|
|
82
|
-
|
|
195
|
+
// ── 3. 实时流事件 → 核心逻辑 ─────────────────────────────────────────────
|
|
196
|
+
api.runtime.events.onAgentEvent((evt: any) => {
|
|
197
|
+
const { runId, stream, data } = evt;
|
|
198
|
+
const state = runStateMap.get(runId);
|
|
199
|
+
if (!state) return;
|
|
200
|
+
|
|
201
|
+
// assistant 流:累积 AI 输出文本
|
|
202
|
+
if (stream === 'assistant') {
|
|
203
|
+
state.assistantBuffer += (data?.delta ?? '') as string;
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// tool:start → 推送进度消息
|
|
208
|
+
if (stream === 'tool' && data?.phase === 'start') {
|
|
209
|
+
// AI 文本优先,没有则用参数解析标签
|
|
210
|
+
const snippet = extractSnippet(state.assistantBuffer);
|
|
211
|
+
state.assistantBuffer = ''; // 每次工具调用前消费一次
|
|
212
|
+
|
|
213
|
+
const label = snippet || buildToolLabel(
|
|
214
|
+
data.name as string,
|
|
215
|
+
(data.args ?? {}) as Record<string, unknown>,
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
state.lastSentAt = Date.now();
|
|
219
|
+
client.sendText(state.chatId, label).catch((err: any) => {
|
|
220
|
+
api.logger?.warn(`[feishu-progress] 推送失败: ${err?.message}`);
|
|
221
|
+
});
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// lifecycle:end/error → 清理
|
|
226
|
+
if (stream === 'lifecycle' && (data?.phase === 'end' || data?.phase === 'error')) {
|
|
227
|
+
clearInterval(state.heartbeatTimer);
|
|
228
|
+
runStateMap.delete(runId);
|
|
229
|
+
}
|
|
83
230
|
});
|
|
84
231
|
},
|
|
85
232
|
};
|