@vui-design/openclaw-plugin-feishu-progress 0.1.6 → 0.2.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vui-design/openclaw-plugin-feishu-progress",
3
- "version": "0.1.6",
3
+ "version": "0.2.0",
4
4
  "description": "飞书任务进度卡片插件 — 在 AI 执行复杂多步骤任务时,实时更新飞书进度卡片",
5
5
  "keywords": [
6
6
  "openclaw-plugin",
@@ -65,6 +65,27 @@ export class FeishuClient {
65
65
  return data.data.message_id;
66
66
  }
67
67
 
68
+ async sendText(chatId: string, text: string): Promise<string> {
69
+ const token = await this.getToken();
70
+ const res = await fetch(
71
+ `${this.baseUrl}/open-apis/im/v1/messages?receive_id_type=chat_id`,
72
+ {
73
+ method: 'POST',
74
+ headers: {
75
+ Authorization: `Bearer ${token}`,
76
+ 'Content-Type': 'application/json',
77
+ },
78
+ body: JSON.stringify({
79
+ receive_id: chatId,
80
+ msg_type: 'text',
81
+ content: JSON.stringify({ text }),
82
+ }),
83
+ }
84
+ );
85
+ const data = (await res.json()) as { data: { message_id: string } };
86
+ return data.data.message_id;
87
+ }
88
+
68
89
  async updateCard(messageId: string, card: object): Promise<void> {
69
90
  const token = await this.getToken();
70
91
  await fetch(`${this.baseUrl}/open-apis/im/v1/messages/${messageId}`, {
package/src/index.ts CHANGED
@@ -1,43 +1,34 @@
1
1
  import { FeishuClient } from './feishu-client.js';
2
- import { buildProgressCard } from './card-builder.js';
3
- import { sessionStore } from './session-store.js';
4
- import type { StepEntry } from './session-store.js';
5
2
 
6
- // ── 注入到系统提示的 Skill 说明 ──────────────────────────────────────────────
7
- const SKILL_PROMPT = `
8
- ## 任务进度报告规范(report_progress 工具)
9
-
10
- 当你需要执行包含 **3 个或以上独立工具调用** 的复杂任务时,必须遵守以下规范:
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
+ };
11
18
 
12
- 1. **任务最开始**:调用 \`report_progress\`,提供完整的 \`allSteps\` 步骤列表、\`stepIndex: 1\`、当前步骤描述
13
- 2. **每个主要步骤开始前**:调用 \`report_progress\` 更新 \`stepIndex\` 和 \`step\`
14
- 3. **所有步骤完成后**:调用 \`report_progress\` 并设置 \`done: true\`
19
+ function toolLabel(name: string): string {
20
+ return TOOL_LABEL[name] ?? name;
21
+ }
15
22
 
16
- 规则:
17
- - \`allSteps\` 只在第一次调用时传入,后续调用省略
18
- - \`step\` 应简洁,10 字以内
19
- - 不要等待 \`report_progress\` 的返回结果影响主任务,它只是通知机制
20
- `.trim();
23
+ // sessionKey → conversationId(飞书 chat_id)
24
+ const sessionChatMap = new Map<string, string>();
21
25
 
22
- // ── 插件定义 ─────────────────────────────────────────────────────────────────
23
26
  export default {
24
27
  id: 'feishu-progress',
25
28
 
26
29
  register(api: any) {
27
- const cfg = api.config ?? {};
28
- const client = new FeishuClient({
29
- appId: cfg.appId,
30
- appSecret: cfg.appSecret,
31
- domain: cfg.domain ?? 'feishu',
32
- });
33
-
34
- // 将 Skill 说明注入系统提示(before_prompt_build 是稳定的生命周期钩子)
35
- api.on('before_prompt_build', async (event: any) => {
36
- event.appendSystemSection?.('feishu-progress-skill', SKILL_PROMPT);
37
- });
30
+ const cfg = api.pluginConfig ?? {};
38
31
 
39
- // 注册 report_progress 工具
40
- // 凭据未配置时跳过工具注册,避免运行时 crash
41
32
  if (!cfg.appId || !cfg.appSecret) {
42
33
  api.logger?.warn(
43
34
  '[feishu-progress] appId / appSecret 未配置,进度卡片功能已禁用。\n' +
@@ -49,126 +40,38 @@ export default {
49
40
  return;
50
41
  }
51
42
 
52
- api.registerTool((ctx: any) => ({
53
- name: 'report_progress',
54
- description:
55
- '向飞书发送或更新任务进度卡片。在每个关键步骤开始前调用,让用户实时了解任务进展。',
56
- inputSchema: {
57
- type: 'object',
58
- properties: {
59
- step: {
60
- type: 'string',
61
- description: '当前正在执行的步骤描述(简洁,10 字以内)',
62
- },
63
- stepIndex: {
64
- type: 'number',
65
- description: '当前第几步(从 1 开始)',
66
- },
67
- totalSteps: {
68
- type: 'number',
69
- description: '本次任务的总步骤数',
70
- },
71
- allSteps: {
72
- type: 'array',
73
- items: { type: 'string' },
74
- description: '所有步骤名称列表,仅首次调用时传入',
75
- },
76
- done: {
77
- type: 'boolean',
78
- description: '是否所有步骤已全部完成,完成时设为 true',
79
- },
80
- },
81
- required: ['step', 'stepIndex', 'totalSteps'],
82
- },
83
-
84
- execute: async (input: {
85
- step: string;
86
- stepIndex: number;
87
- totalSteps: number;
88
- allSteps?: string[];
89
- done?: boolean;
90
- }) => {
91
- // 优先取飞书 chat_id(兼容不同版本的 ctx 字段名)
92
- const chatId: string | undefined =
93
- ctx.channelMeta?.chatId ??
94
- ctx.channelMeta?.chat_id ??
95
- ctx.channelContext?.chatId ??
96
- ctx.chatId;
97
-
98
- if (!chatId) {
99
- return { ok: false, reason: '无法获取飞书 chat_id,跳过进度更新' };
100
- }
101
-
102
- // minSteps 未达到时静默跳过
103
- const minSteps: number = cfg.minSteps ?? 3;
104
- if (input.totalSteps < minSteps) {
105
- return { ok: true, skipped: true };
106
- }
107
-
108
- const sessionKey = `${chatId}:${ctx.sessionKey ?? ctx.runId ?? 'default'}`;
109
- const existing = sessionStore.get(sessionKey);
110
-
111
- // 构造步骤状态列表
112
- let steps: StepEntry[];
113
- if (!existing) {
114
- // 首次调用:用 allSteps 或自动生成占位标签
115
- const labels: string[] =
116
- input.allSteps ??
117
- Array.from({ length: input.totalSteps }, (_, i) =>
118
- i + 1 === input.stepIndex ? input.step : `步骤 ${i + 1}`
119
- );
120
- steps = labels.map((label, i) => ({
121
- label,
122
- status:
123
- i + 1 < input.stepIndex
124
- ? 'done'
125
- : i + 1 === input.stepIndex
126
- ? 'active'
127
- : 'pending',
128
- }));
129
- } else {
130
- // 后续调用:更新已有步骤状态
131
- steps = existing.steps.map((s, i) => ({
132
- ...s,
133
- status:
134
- i + 1 < input.stepIndex
135
- ? 'done'
136
- : i + 1 === input.stepIndex && !input.done
137
- ? 'active'
138
- : 'pending',
139
- }));
140
- }
141
-
142
- // done=true 时所有步骤标为 done
143
- if (input.done) {
144
- steps = steps.map((s) => ({ ...s, status: 'done' as const }));
145
- }
146
-
147
- const startTime = existing?.startTime ?? Date.now();
148
- const card = buildProgressCard(
149
- { stepIndex: input.stepIndex, totalSteps: input.totalSteps, done: input.done },
150
- { steps, startTime }
151
- );
43
+ const client = new FeishuClient({
44
+ appId: cfg.appId,
45
+ appSecret: cfg.appSecret,
46
+ domain: cfg.domain ?? 'feishu',
47
+ });
152
48
 
153
- try {
154
- if (!existing) {
155
- const msgId = await client.sendCard(chatId, card);
156
- sessionStore.set(sessionKey, { msgId, chatId, startTime, steps });
157
- } else {
158
- await client.updateCard(existing.msgId, card);
159
- sessionStore.set(sessionKey, { ...existing, steps });
160
- }
49
+ // ── 1. 收到用户消息时,记录当前会话对应的飞书 chat_id ────────────────────
50
+ api.on('message_received', async (_event: any, ctx: any) => {
51
+ if (ctx.channelId !== 'feishu') return;
52
+ if (!ctx.conversationId) return;
53
+ const key = ctx.sessionKey ?? ctx.accountId ?? 'default';
54
+ sessionChatMap.set(key, ctx.conversationId);
55
+ });
161
56
 
162
- // 任务完成后 30s 清理 session,避免内存泄漏
163
- if (input.done) {
164
- setTimeout(() => sessionStore.delete(sessionKey), 30_000);
165
- }
57
+ // ── 2. 每次工具调用前,自动推送进度到飞书 ──────────────────────────────
58
+ api.on('before_tool_call', async (event: any, ctx: any) => {
59
+ const key = ctx.sessionKey ?? 'default';
60
+ const chatId = sessionChatMap.get(key);
61
+ if (!chatId) return;
62
+
63
+ const label = toolLabel(event.toolName);
64
+ try {
65
+ await client.sendText(chatId, `🔧 正在执行:${label}`);
66
+ } catch (err: any) {
67
+ api.logger?.warn(`[feishu-progress] 推送失败: ${err?.message}`);
68
+ }
69
+ });
166
70
 
167
- return { ok: true };
168
- } catch (err: any) {
169
- return { ok: false, reason: err?.message ?? String(err) };
170
- }
171
- },
172
- }));
71
+ // ── 3. 任务结束后清理 session 映射 ───────────────────────────────────────
72
+ api.on('agent_end', async (_event: any, ctx: any) => {
73
+ const key = ctx.sessionKey ?? 'default';
74
+ sessionChatMap.delete(key);
75
+ });
173
76
  },
174
77
  };