@vui-design/openclaw-plugin-feishu-progress 0.3.0 → 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 +89 -57
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,26 +1,25 @@
|
|
|
1
1
|
import { FeishuClient } from './feishu-client.js';
|
|
2
2
|
|
|
3
|
-
// ──
|
|
3
|
+
// ── 工具参数 → 语义化中文标签 ────────────────────────────────────────────────
|
|
4
4
|
|
|
5
5
|
function basename(p: string): string {
|
|
6
6
|
return p.split(/[/\\]/).filter(Boolean).pop() ?? p;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
function
|
|
9
|
+
function s(v: unknown, max = 60): string {
|
|
10
10
|
return String(v ?? '').slice(0, max).trim();
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
function buildToolLabel(toolName: string,
|
|
13
|
+
function buildToolLabel(toolName: string, args: Record<string, unknown>): string {
|
|
14
14
|
switch (toolName) {
|
|
15
15
|
case 'exec':
|
|
16
16
|
case 'bash': {
|
|
17
|
-
const cmd =
|
|
17
|
+
const cmd = s(args?.command ?? args?.cmd ?? args?.input, 150);
|
|
18
18
|
|
|
19
19
|
const brewInstall = cmd.match(/brew\s+install\s+([\w@./-]+)/);
|
|
20
20
|
if (brewInstall) return `📦 安装 ${brewInstall[1]}`;
|
|
21
21
|
const brewUninstall = cmd.match(/brew\s+uninstall\s+([\w@./-]+)/);
|
|
22
22
|
if (brewUninstall) return `🗑️ 卸载 ${brewUninstall[1]}`;
|
|
23
|
-
|
|
24
23
|
const which = cmd.match(/^(?:which|command\s+-v)\s+([\w.-]+)/);
|
|
25
24
|
if (which) return `🔍 检查 ${which[1]} 是否已安装`;
|
|
26
25
|
|
|
@@ -40,7 +39,6 @@ function buildToolLabel(toolName: string, params: Record<string, unknown>): stri
|
|
|
40
39
|
|
|
41
40
|
const mkdir = cmd.match(/^mkdir\s+(?:-[\w]+\s+)?([\w./~-]+)/);
|
|
42
41
|
if (mkdir) return `📁 创建目录 ${basename(mkdir[1])}`;
|
|
43
|
-
|
|
44
42
|
const rm = cmd.match(/^rm\s+(?:-[\w]+\s+)?([\w./~-]+)/);
|
|
45
43
|
if (rm) return `🗑️ 删除 ${basename(rm[1])}`;
|
|
46
44
|
|
|
@@ -52,57 +50,57 @@ function buildToolLabel(toolName: string, params: Record<string, unknown>): stri
|
|
|
52
50
|
}
|
|
53
51
|
|
|
54
52
|
case 'process': {
|
|
55
|
-
const cmd =
|
|
56
|
-
return cmd ? `⚡
|
|
53
|
+
const cmd = s(args?.command ?? args?.cmd, 80);
|
|
54
|
+
return cmd ? `⚡ ${cmd.slice(0, 40)}` : '⚡ 执行进程';
|
|
57
55
|
}
|
|
58
56
|
|
|
59
57
|
case 'read':
|
|
60
58
|
case 'read_file': {
|
|
61
|
-
const p =
|
|
59
|
+
const p = s(args?.path ?? args?.file_path ?? args?.filePath);
|
|
62
60
|
return p ? `📂 读取 ${basename(p)}` : '📂 读取文件';
|
|
63
61
|
}
|
|
64
62
|
|
|
65
63
|
case 'write':
|
|
66
64
|
case 'write_file':
|
|
67
65
|
case 'create_file': {
|
|
68
|
-
const p =
|
|
66
|
+
const p = s(args?.path ?? args?.file_path ?? args?.filePath);
|
|
69
67
|
return p ? `✏️ 写入 ${basename(p)}` : '✏️ 写入文件';
|
|
70
68
|
}
|
|
71
69
|
|
|
72
70
|
case 'str_replace_editor':
|
|
73
71
|
case 'edit_file': {
|
|
74
|
-
const p =
|
|
72
|
+
const p = s(args?.path ?? args?.file_path);
|
|
75
73
|
return p ? `✏️ 编辑 ${basename(p)}` : '✏️ 编辑文件';
|
|
76
74
|
}
|
|
77
75
|
|
|
78
76
|
case 'list_directory': {
|
|
79
|
-
const p =
|
|
77
|
+
const p = s(args?.path ?? args?.directory);
|
|
80
78
|
return p ? `📋 浏览 ${basename(p) || p}` : '📋 浏览目录';
|
|
81
79
|
}
|
|
82
80
|
|
|
83
81
|
case 'search':
|
|
84
82
|
case 'web_search':
|
|
85
83
|
case 'tavily_search': {
|
|
86
|
-
const q =
|
|
84
|
+
const q = s(args?.query ?? args?.q, 30);
|
|
87
85
|
return q ? `🌐 搜索「${q}」` : '🌐 网络搜索';
|
|
88
86
|
}
|
|
89
87
|
|
|
90
88
|
case 'fetch':
|
|
91
89
|
case 'web_fetch': {
|
|
92
|
-
const url =
|
|
90
|
+
const url = s(args?.url ?? args?.href);
|
|
93
91
|
try { return `🌐 访问 ${new URL(url).hostname}`; } catch { /**/ }
|
|
94
92
|
return '🌐 抓取网页';
|
|
95
93
|
}
|
|
96
94
|
|
|
97
95
|
case 'computer': {
|
|
98
|
-
const action =
|
|
96
|
+
const action = s(args?.action);
|
|
99
97
|
return action ? `🖥️ 操作电脑:${action}` : '🖥️ 操作电脑';
|
|
100
98
|
}
|
|
101
99
|
|
|
102
100
|
case 'screenshot': return '📸 截图';
|
|
103
101
|
|
|
104
102
|
case 'delete_file': {
|
|
105
|
-
const p =
|
|
103
|
+
const p = s(args?.path ?? args?.file_path);
|
|
106
104
|
return p ? `🗑️ 删除 ${basename(p)}` : '🗑️ 删除文件';
|
|
107
105
|
}
|
|
108
106
|
|
|
@@ -110,22 +108,21 @@ function buildToolLabel(toolName: string, params: Record<string, unknown>): stri
|
|
|
110
108
|
}
|
|
111
109
|
}
|
|
112
110
|
|
|
113
|
-
//
|
|
114
|
-
|
|
111
|
+
// 从 AI 输出文本中提取最末一句(作为进度提示优先显示)
|
|
115
112
|
function extractSnippet(text: string): string {
|
|
116
|
-
// 取最后一段非空文本,截取前 50 字
|
|
117
113
|
const lines = text.split(/\n+/).map(l => l.trim()).filter(Boolean);
|
|
118
|
-
|
|
119
|
-
return last.slice(0, 50);
|
|
114
|
+
return (lines[lines.length - 1] ?? '').slice(0, 50);
|
|
120
115
|
}
|
|
121
116
|
|
|
122
|
-
// ──
|
|
123
|
-
|
|
124
|
-
// agentId → 飞书裸 ID(ou_xxx / oc_xxx)
|
|
125
|
-
const sessionChatMap = new Map<string, string>();
|
|
117
|
+
// ── 主插件 ────────────────────────────────────────────────────────────────
|
|
126
118
|
|
|
127
|
-
|
|
128
|
-
|
|
119
|
+
interface RunState {
|
|
120
|
+
chatId: string;
|
|
121
|
+
assistantBuffer: string;
|
|
122
|
+
startedAt: number;
|
|
123
|
+
lastSentAt: number;
|
|
124
|
+
heartbeatTimer: ReturnType<typeof setInterval>;
|
|
125
|
+
}
|
|
129
126
|
|
|
130
127
|
export default {
|
|
131
128
|
id: 'feishu-progress',
|
|
@@ -150,51 +147,86 @@ export default {
|
|
|
150
147
|
domain: cfg.domain ?? 'feishu',
|
|
151
148
|
});
|
|
152
149
|
|
|
153
|
-
//
|
|
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 ─────────────────
|
|
154
157
|
api.on('message_received', async (_event: any, ctx: any) => {
|
|
155
158
|
if (ctx.channelId !== 'feishu') return;
|
|
156
159
|
if (!ctx.conversationId) return;
|
|
157
|
-
// OpenClaw 内部格式 "user:ou_xxx"
|
|
160
|
+
// OpenClaw 内部格式 "user:ou_xxx",剥离前缀
|
|
158
161
|
const rawId = ctx.conversationId.includes(':')
|
|
159
162
|
? ctx.conversationId.split(':').pop()!
|
|
160
163
|
: ctx.conversationId;
|
|
161
|
-
|
|
164
|
+
accountChatMap.set(ctx.accountId ?? 'default', rawId);
|
|
162
165
|
});
|
|
163
166
|
|
|
164
|
-
// ── 2. LLM
|
|
165
|
-
api.on('
|
|
166
|
-
if (!event.runId) return;
|
|
167
|
-
|
|
168
|
-
const text = texts.join('').trim();
|
|
169
|
-
if (text) runTextMap.set(event.runId, text);
|
|
170
|
-
});
|
|
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
171
|
|
|
172
|
-
|
|
173
|
-
api.on('before_tool_call', async (event: any, ctx: any) => {
|
|
174
|
-
const key = ctx.agentId ?? ctx.sessionKey ?? 'default';
|
|
175
|
-
const chatId = sessionChatMap.get(key);
|
|
172
|
+
const chatId = accountChatMap.get(ctx.agentId);
|
|
176
173
|
if (!chatId) return;
|
|
177
174
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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);
|
|
193
|
+
});
|
|
194
|
+
|
|
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;
|
|
186
205
|
}
|
|
187
206
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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;
|
|
192
223
|
}
|
|
193
|
-
});
|
|
194
224
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
225
|
+
// lifecycle:end/error → 清理
|
|
226
|
+
if (stream === 'lifecycle' && (data?.phase === 'end' || data?.phase === 'error')) {
|
|
227
|
+
clearInterval(state.heartbeatTimer);
|
|
228
|
+
runStateMap.delete(runId);
|
|
229
|
+
}
|
|
198
230
|
});
|
|
199
231
|
},
|
|
200
232
|
};
|