@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 +1 -1
- package/src/feishu-client.ts +21 -0
- package/src/index.ts +51 -148
package/package.json
CHANGED
package/src/feishu-client.ts
CHANGED
|
@@ -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
|
-
//
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
19
|
+
function toolLabel(name: string): string {
|
|
20
|
+
return TOOL_LABEL[name] ?? name;
|
|
21
|
+
}
|
|
15
22
|
|
|
16
|
-
|
|
17
|
-
|
|
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.
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
};
|