@lih-x-x/kmr 1.0.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 (48) hide show
  1. package/README.md +282 -0
  2. package/dist/agent/claudeCode.d.ts +12 -0
  3. package/dist/agent/claudeCode.js +109 -0
  4. package/dist/agent/prompt.d.ts +3 -0
  5. package/dist/agent/prompt.js +33 -0
  6. package/dist/agent/types.d.ts +7 -0
  7. package/dist/agent/types.js +1 -0
  8. package/dist/cli-init.d.ts +1 -0
  9. package/dist/cli-init.js +12 -0
  10. package/dist/cli.d.ts +2 -0
  11. package/dist/cli.js +43 -0
  12. package/dist/config.d.ts +17 -0
  13. package/dist/config.js +38 -0
  14. package/dist/index.d.ts +1 -0
  15. package/dist/index.js +214 -0
  16. package/dist/lark/client.d.ts +8 -0
  17. package/dist/lark/client.js +68 -0
  18. package/dist/lark/docReader.d.ts +9 -0
  19. package/dist/lark/docReader.js +75 -0
  20. package/dist/lark/messenger.d.ts +22 -0
  21. package/dist/lark/messenger.js +156 -0
  22. package/dist/lark/router.d.ts +20 -0
  23. package/dist/lark/router.js +75 -0
  24. package/dist/lark/taskCreator.d.ts +15 -0
  25. package/dist/lark/taskCreator.js +41 -0
  26. package/dist/query/finder.d.ts +2 -0
  27. package/dist/query/finder.js +18 -0
  28. package/dist/query/handler.d.ts +8 -0
  29. package/dist/query/handler.js +17 -0
  30. package/dist/session/manager.d.ts +12 -0
  31. package/dist/session/manager.js +114 -0
  32. package/dist/session/skill.d.ts +1 -0
  33. package/dist/session/skill.js +19 -0
  34. package/dist/storage/jsonStore.d.ts +11 -0
  35. package/dist/storage/jsonStore.js +51 -0
  36. package/dist/storage/types.d.ts +52 -0
  37. package/dist/storage/types.js +1 -0
  38. package/dist/web/openBrowser.d.ts +1 -0
  39. package/dist/web/openBrowser.js +15 -0
  40. package/dist/web/public/app.js +344 -0
  41. package/dist/web/public/index.html +28 -0
  42. package/dist/web/public/public/app.js +344 -0
  43. package/dist/web/public/public/index.html +28 -0
  44. package/dist/web/public/public/style.css +428 -0
  45. package/dist/web/public/style.css +428 -0
  46. package/dist/web/server.d.ts +6 -0
  47. package/dist/web/server.js +209 -0
  48. package/package.json +37 -0
package/dist/index.js ADDED
@@ -0,0 +1,214 @@
1
+ import { loadConfig, KMR_DIR } from './config';
2
+ import { createLarkClient, createEventDispatcher, startWSClient } from './lark/client';
3
+ import { DocReader } from './lark/docReader';
4
+ import { Messenger } from './lark/messenger';
5
+ import { parseMessage, MessageType } from './lark/router';
6
+ import { ClaudeCodeProvider } from './agent/claudeCode';
7
+ import { JsonStore } from './storage/jsonStore';
8
+ import { QueryHandler } from './query/handler';
9
+ import { startWebServer } from './web/server';
10
+ import { openBrowser } from './web/openBrowser';
11
+ import { SessionManager } from './session/manager';
12
+ import { TaskCreator } from './lark/taskCreator';
13
+ async function main() {
14
+ const config = loadConfig();
15
+ const client = createLarkClient(config);
16
+ const docReader = new DocReader(client);
17
+ const messenger = new Messenger(client);
18
+ const agent = new ClaudeCodeProvider(config.agent.timeout);
19
+ const store = new JsonStore(config.storage.dataDir);
20
+ const queryHandler = new QueryHandler(agent, config.storage.dataDir);
21
+ const sessionManager = new SessionManager(KMR_DIR, config.agent.timeout);
22
+ const taskCreator = new TaskCreator(client);
23
+ const pendingConfirmations = new Map();
24
+ const dispatcher = createEventDispatcher(async (messageId, text, _chatId, senderId, meta) => {
25
+ const parsed = parseMessage(text);
26
+ console.log(`[route] 消息路由: type=${parsed.type}, messageId=${messageId}, isGroup=${meta.isGroup}, isMentioned=${meta.isMentioned}`);
27
+ // 群聊非@消息 + 文档链接:需要验证文档标题是否为会议纪要
28
+ if (meta.isGroup && !meta.isMentioned && parsed.type === MessageType.DOCUMENT_LINK) {
29
+ const title = await docReader.getDocumentTitle(parsed.documentId);
30
+ console.log(`[filter] 群聊文档标题: "${title}"`);
31
+ const isMeetingNote = /纪要|会议记录|meeting/i.test(title) || /from=vc_assistant/.test(text);
32
+ if (!isMeetingNote) {
33
+ console.log(`[filter] 文档标题不含纪要关键词, 忽略`);
34
+ return;
35
+ }
36
+ console.log(`[filter] 确认为会议纪要, 继续处理`);
37
+ }
38
+ try {
39
+ switch (parsed.type) {
40
+ case MessageType.DOCUMENT_LINK: {
41
+ console.log(`[process] 开始处理文档链接, documentId=${parsed.documentId}`);
42
+ await messenger.replyText(messageId, '正在提取会议关键信息,请稍候...');
43
+ console.log(`[reply] 已发送"正在提取"提示`);
44
+ console.log(`[process] 读取飞书文档内容...`);
45
+ const content = await docReader.readDocument(parsed.documentId);
46
+ console.log(`[process] 文档内容读取完成, 长度=${content.length}`);
47
+ console.log(`[process] 调用 Agent 提取会议信息...`);
48
+ const record = await agent.extract(content);
49
+ console.log(`[process] Agent 提取完成: title="${record.summary.title}"`);
50
+ record.documentUrl = text.match(/(https?:\/\/[^\s]*feishu\.cn\/[^\s]+)/)?.[0] || '';
51
+ await store.save(record);
52
+ console.log(`✅ 会议提取成功: ${record.summary.title} (${record.id})`);
53
+ await messenger.replyMeetingSummary(messageId, record);
54
+ console.log(`[reply] 已发送会议摘要回复`);
55
+ // 如果有待办,发确认消息
56
+ if (record.todos.length > 0) {
57
+ // 清理旧的待确认(如果有)
58
+ const oldPending = pendingConfirmations.get(senderId);
59
+ if (oldPending)
60
+ clearTimeout(oldPending.timer);
61
+ const timer = setTimeout(() => {
62
+ const p = pendingConfirmations.get(senderId);
63
+ if (p && p.meetingId === record.id) {
64
+ pendingConfirmations.delete(senderId);
65
+ console.log(`[timeout] 待办确认超时, userId=${senderId}, meetingId=${record.id}`);
66
+ }
67
+ }, 60_000);
68
+ pendingConfirmations.set(senderId, {
69
+ meetingId: record.id,
70
+ todos: record.todos,
71
+ createdAt: new Date().toISOString(),
72
+ timer,
73
+ });
74
+ await messenger.replyTodoConfirmation(messageId, record.todos);
75
+ console.log(`[reply] 已发送待办确认消息, ${record.todos.length} 条待办`);
76
+ }
77
+ break;
78
+ }
79
+ case MessageType.FIND_QUERY: {
80
+ console.log(`[process] 开始搜索: query="${parsed.query}"`);
81
+ const results = await queryHandler.find(parsed.query);
82
+ console.log(`[process] 搜索完成, 结果数=${results.length}`);
83
+ await messenger.replySearchResults(messageId, results);
84
+ console.log(`[reply] 已发送搜索结果`);
85
+ break;
86
+ }
87
+ case MessageType.LIST_ALL: {
88
+ console.log(`[process] 列出所有记录`);
89
+ const all = await store.list();
90
+ console.log(`[process] 共 ${all.length} 条记录`);
91
+ await messenger.replyRecordList(messageId, all);
92
+ console.log(`[reply] 已发送记录列表`);
93
+ break;
94
+ }
95
+ case MessageType.DELETE_RECORD: {
96
+ console.log(`[process] 删除记录: id=${parsed.deleteId}`);
97
+ const deleted = await store.delete(parsed.deleteId);
98
+ if (deleted) {
99
+ console.log(`✅ 记录已删除: ${parsed.deleteId}`);
100
+ await messenger.replyText(messageId, `✅ 已删除记录: ${parsed.deleteId}`);
101
+ }
102
+ else {
103
+ await messenger.replyText(messageId, `❌ 未找到记录: ${parsed.deleteId}`);
104
+ }
105
+ console.log(`[reply] 已发送删除结果`);
106
+ break;
107
+ }
108
+ case MessageType.SHOW_DETAIL: {
109
+ console.log(`[process] 查看详情: id=${parsed.showId}`);
110
+ const detail = await store.load(parsed.showId);
111
+ if (detail) {
112
+ await messenger.replyRecordDetail(messageId, detail);
113
+ }
114
+ else {
115
+ await messenger.replyText(messageId, `❌ 未找到记录: ${parsed.showId}`);
116
+ }
117
+ console.log(`[reply] 已发送详情`);
118
+ break;
119
+ }
120
+ case MessageType.CONFIRM_TASKS: {
121
+ const pending = pendingConfirmations.get(senderId);
122
+ if (!pending) {
123
+ await messenger.replyText(messageId, '没有待确认的待办事项。请先发送会议文档链接提取待办。');
124
+ break;
125
+ }
126
+ clearTimeout(pending.timer);
127
+ const selectedTodos = parsed.confirmIds === 'all'
128
+ ? pending.todos
129
+ : pending.todos.filter((_, i) => parsed.confirmIds.includes(i + 1));
130
+ if (selectedTodos.length === 0) {
131
+ await messenger.replyText(messageId, '未选中有效的待办编号,请检查后重试。');
132
+ break;
133
+ }
134
+ console.log(`[process] 创建飞书任务: ${selectedTodos.length} 条`);
135
+ await messenger.replyText(messageId, `正在创建 ${selectedTodos.length} 条飞书任务...`);
136
+ const results = [];
137
+ for (const todo of selectedTodos) {
138
+ try {
139
+ const result = await taskCreator.createTask({
140
+ summary: `${todo.content}(${todo.owner})`,
141
+ due: todo.deadline,
142
+ description: `来源会议: ${pending.meetingId}\n负责人: ${todo.owner}`,
143
+ assigneeOpenId: senderId,
144
+ });
145
+ results.push({ summary: todo.content, success: true, taskId: result.taskId, url: result.url });
146
+ console.log(`✅ 任务创建成功: ${todo.content}`);
147
+ }
148
+ catch (err) {
149
+ results.push({ summary: todo.content, success: false, error: err.message });
150
+ console.error(`❌ 任务创建失败: ${todo.content}`, err.message);
151
+ }
152
+ }
153
+ await messenger.replyTaskResults(messageId, results);
154
+ // 将创建的任务关联回会议记录
155
+ const createdTasks = results
156
+ .filter((r) => r.success)
157
+ .map((r) => ({
158
+ summary: r.summary,
159
+ taskId: r.taskId || '',
160
+ url: r.url || '',
161
+ createdAt: new Date().toISOString(),
162
+ }));
163
+ if (createdTasks.length > 0) {
164
+ const record = await store.load(pending.meetingId);
165
+ if (record) {
166
+ record.createdTasks = [...(record.createdTasks || []), ...createdTasks];
167
+ await store.save(record);
168
+ console.log(`[task] 已将 ${createdTasks.length} 条任务关联到会议 ${pending.meetingId}`);
169
+ }
170
+ }
171
+ pendingConfirmations.delete(senderId);
172
+ console.log(`[reply] 已发送任务创建结果`);
173
+ break;
174
+ }
175
+ case MessageType.REJECT_TASKS: {
176
+ const pending = pendingConfirmations.get(senderId);
177
+ if (!pending) {
178
+ await messenger.replyText(messageId, '没有待确认的待办事项。');
179
+ break;
180
+ }
181
+ clearTimeout(pending.timer);
182
+ pendingConfirmations.delete(senderId);
183
+ await messenger.replyText(messageId, '✅ 已取消创建待办任务。');
184
+ console.log(`[process] 用户拒绝创建任务, userId=${senderId}`);
185
+ break;
186
+ }
187
+ case MessageType.UNKNOWN: {
188
+ // 群聊非@消息不触发 AI 对话
189
+ if (meta.isGroup && !meta.isMentioned) {
190
+ console.log(`[filter] 群聊非@未知消息, 忽略`);
191
+ break;
192
+ }
193
+ console.log(`[process] 自由对话, userId=${senderId}`);
194
+ await messenger.replyText(messageId, '正在思考...');
195
+ const reply = await sessionManager.handleMessage(senderId, text);
196
+ await messenger.replyText(messageId, reply);
197
+ console.log(`[reply] 已发送 AI 回复`);
198
+ break;
199
+ }
200
+ }
201
+ }
202
+ catch (err) {
203
+ console.error('[error] 处理消息失败:', err);
204
+ await messenger.replyText(messageId, `处理失败: ${err.message}`);
205
+ console.log(`[reply] 已发送错误回复`);
206
+ }
207
+ });
208
+ startWSClient(client, dispatcher);
209
+ const { port } = await startWebServer(store, KMR_DIR, 3000);
210
+ openBrowser(`http://localhost:${port}`);
211
+ console.log(`KMR Web 界面已启动: http://localhost:${port}`);
212
+ console.log('KMR 服务已启动,等待飞书消息...');
213
+ }
214
+ main().catch(console.error);
@@ -0,0 +1,8 @@
1
+ import * as lark from '@larksuiteoapi/node-sdk';
2
+ import { KmrConfig } from '../config';
3
+ export declare function createLarkClient(config: KmrConfig): lark.Client;
4
+ export declare function createEventDispatcher(onMessage: (messageId: string, text: string, chatId: string, senderId: string, meta: {
5
+ isGroup: boolean;
6
+ isMentioned: boolean;
7
+ }) => Promise<void>): lark.EventDispatcher;
8
+ export declare function startWSClient(client: lark.Client, dispatcher: lark.EventDispatcher): void;
@@ -0,0 +1,68 @@
1
+ import * as lark from '@larksuiteoapi/node-sdk';
2
+ export function createLarkClient(config) {
3
+ return new lark.Client({
4
+ appId: config.lark.appId,
5
+ appSecret: config.lark.appSecret,
6
+ appType: lark.AppType.SelfBuild,
7
+ });
8
+ }
9
+ export function createEventDispatcher(onMessage) {
10
+ const dispatcher = new lark.EventDispatcher({});
11
+ const processedMessages = new Set();
12
+ dispatcher.register({
13
+ 'im.message.receive_v1': async (data) => {
14
+ console.log(`[recv] 收到飞书事件:`, JSON.stringify(data, null, 2));
15
+ const message = data.message;
16
+ if (message.message_type !== 'text') {
17
+ console.log(`[recv] 忽略非文本消息, type=${message.message_type}`);
18
+ return;
19
+ }
20
+ const messageId = message.message_id;
21
+ // 消息去重:飞书可能重复投递同一事件
22
+ if (processedMessages.has(messageId)) {
23
+ console.log(`[dedup] 跳过重复消息: ${messageId}`);
24
+ return;
25
+ }
26
+ processedMessages.add(messageId);
27
+ // 防止内存泄漏:保留最近 1000 条
28
+ if (processedMessages.size > 1000) {
29
+ const first = processedMessages.values().next().value;
30
+ processedMessages.delete(first);
31
+ }
32
+ const content = JSON.parse(message.content);
33
+ let text = content.text || '';
34
+ const chatId = message.chat_id;
35
+ // 群聊中 @机器人 的消息,去掉 @_user_N 占位符
36
+ const isMentioned = message.mentions && message.mentions.length > 0;
37
+ const isGroup = message.chat_type === 'group';
38
+ if (isMentioned) {
39
+ for (const mention of message.mentions) {
40
+ if (mention.key) {
41
+ text = text.replace(mention.key, '').trim();
42
+ }
43
+ }
44
+ }
45
+ // 群聊中非 @机器人 的消息,只放行文档链接和待办确认/拒绝命令
46
+ if (isGroup && !isMentioned) {
47
+ const hasDocLink = /(https?:\/\/[^\s]*feishu\.cn\/[^\s]+)/.test(text);
48
+ const isConfirmOrReject = /^(?:\/confirm|\/reject|确认创建|取消创建|不创建|不用创建|全部创建|跳过待办|跳过|算了)\b/.test(text.trim());
49
+ if (!hasDocLink && !isConfirmOrReject) {
50
+ console.log(`[recv] 群聊非@消息且非文档链接/待办确认, 忽略`);
51
+ return;
52
+ }
53
+ }
54
+ console.log(`[recv] 解析消息: messageId=${messageId}, chatId=${chatId}, chatType=${message.chat_type}, text="${text}"`);
55
+ const senderId = data.sender?.sender_id?.open_id || 'unknown';
56
+ await onMessage(messageId, text, chatId, senderId, { isGroup, isMentioned });
57
+ },
58
+ });
59
+ return dispatcher;
60
+ }
61
+ export function startWSClient(client, dispatcher) {
62
+ const wsClient = new lark.WSClient({
63
+ appId: client.appId,
64
+ appSecret: client.appSecret,
65
+ loggerLevel: lark.LoggerLevel.info,
66
+ });
67
+ wsClient.start({ eventDispatcher: dispatcher });
68
+ }
@@ -0,0 +1,9 @@
1
+ import * as lark from '@larksuiteoapi/node-sdk';
2
+ export declare function extractDocumentId(url: string): string | null;
3
+ export declare class DocReader {
4
+ private readonly client;
5
+ constructor(client: lark.Client);
6
+ getDocumentTitle(documentId: string): Promise<string>;
7
+ readDocument(documentId: string): Promise<string>;
8
+ private extractBlockText;
9
+ }
@@ -0,0 +1,75 @@
1
+ export function extractDocumentId(url) {
2
+ const patterns = [
3
+ /feishu\.cn\/docx\/([a-zA-Z0-9]+)/,
4
+ /feishu\.cn\/wiki\/([a-zA-Z0-9]+)/,
5
+ /feishu\.cn\/docs\/([a-zA-Z0-9]+)/,
6
+ ];
7
+ for (const pattern of patterns) {
8
+ const match = url.match(pattern);
9
+ if (match)
10
+ return match[1];
11
+ }
12
+ return null;
13
+ }
14
+ export class DocReader {
15
+ client;
16
+ constructor(client) {
17
+ this.client = client;
18
+ }
19
+ async getDocumentTitle(documentId) {
20
+ try {
21
+ const response = await this.client.docx.document.get({
22
+ path: { document_id: documentId },
23
+ });
24
+ return response.data?.document?.title || '';
25
+ }
26
+ catch (err) {
27
+ console.error(`[docReader] 获取文档标题失败: ${err.message}`);
28
+ return '';
29
+ }
30
+ }
31
+ async readDocument(documentId) {
32
+ const blocks = [];
33
+ let pageToken;
34
+ // 分页获取所有 block
35
+ do {
36
+ const response = await this.client.docx.documentBlock.list({
37
+ path: { document_id: documentId },
38
+ params: { page_size: 500, ...(pageToken ? { page_token: pageToken } : {}) },
39
+ });
40
+ if (response.data?.items) {
41
+ for (const block of response.data.items) {
42
+ const text = this.extractBlockText(block);
43
+ if (text)
44
+ blocks.push(text);
45
+ }
46
+ }
47
+ pageToken = response.data?.page_token || undefined;
48
+ } while (pageToken);
49
+ console.log(`[docReader] 文档 ${documentId} 共提取 ${blocks.length} 个文本块`);
50
+ return blocks.join('\n');
51
+ }
52
+ extractBlockText(block) {
53
+ // 尝试从多种 block 类型中提取文本
54
+ const textSources = [
55
+ block.text, // 普通文本
56
+ block.heading, // 标题
57
+ block.bullet, // 无序列表
58
+ block.ordered, // 有序列表
59
+ block.code, // 代码块
60
+ block.quote, // 引用
61
+ block.todo, // 待办
62
+ block.callout, // 高亮块
63
+ ];
64
+ for (const source of textSources) {
65
+ if (source?.elements) {
66
+ const text = source.elements
67
+ .map((el) => el.text_run?.content || el.mention_user?.content || '')
68
+ .join('');
69
+ if (text)
70
+ return text;
71
+ }
72
+ }
73
+ return '';
74
+ }
75
+ }
@@ -0,0 +1,22 @@
1
+ import * as lark from '@larksuiteoapi/node-sdk';
2
+ import { MeetingRecord } from '../storage/types';
3
+ export declare class Messenger {
4
+ private readonly client;
5
+ constructor(client: lark.Client);
6
+ replyText(messageId: string, text: string): Promise<void>;
7
+ replyMeetingSummary(messageId: string, record: MeetingRecord): Promise<void>;
8
+ replyRecordList(messageId: string, records: MeetingRecord[]): Promise<void>;
9
+ replyRecordDetail(messageId: string, record: MeetingRecord): Promise<void>;
10
+ replySearchResults(messageId: string, results: MeetingRecord[]): Promise<void>;
11
+ replyTodoConfirmation(messageId: string, todos: {
12
+ content: string;
13
+ owner: string;
14
+ deadline: string;
15
+ }[]): Promise<void>;
16
+ replyTaskResults(messageId: string, results: {
17
+ summary: string;
18
+ success: boolean;
19
+ url?: string;
20
+ error?: string;
21
+ }[]): Promise<void>;
22
+ }
@@ -0,0 +1,156 @@
1
+ export class Messenger {
2
+ client;
3
+ constructor(client) {
4
+ this.client = client;
5
+ }
6
+ async replyText(messageId, text) {
7
+ await this.client.im.message.reply({
8
+ path: { message_id: messageId },
9
+ data: {
10
+ content: JSON.stringify({ text }),
11
+ msg_type: 'text',
12
+ },
13
+ });
14
+ }
15
+ async replyMeetingSummary(messageId, record) {
16
+ const lines = [
17
+ `✅ 会议关键信息提取完成`,
18
+ '',
19
+ `📋 **${record.summary.title}**`,
20
+ `📅 日期:${record.summary.date}`,
21
+ `👥 参与人:${record.summary.participants.join('、')}`,
22
+ '',
23
+ '**核心要点:**',
24
+ ...record.summary.keyPoints.map((p) => `• ${p}`),
25
+ ];
26
+ if (record.todos.length > 0) {
27
+ lines.push('', '**待办事项:**');
28
+ for (const todo of record.todos) {
29
+ lines.push(`• ${todo.content}(${todo.owner},截止 ${todo.deadline})`);
30
+ }
31
+ }
32
+ if (record.risks.length > 0) {
33
+ lines.push('', '**风险项:**');
34
+ for (const risk of record.risks) {
35
+ lines.push(`• [${risk.severity}] ${risk.description}`);
36
+ }
37
+ }
38
+ if (record.commitments.length > 0) {
39
+ lines.push('', '🤝 **关键共识与承诺:**');
40
+ for (const c of record.commitments) {
41
+ lines.push(`• ${c.content}`);
42
+ lines.push(` 相关方:${c.participants.join('、')}${c.deadline ? `,截止 ${c.deadline}` : ''}`);
43
+ lines.push(` 💡 ${c.context}`);
44
+ }
45
+ }
46
+ if (record.reusableInsights.length > 0) {
47
+ lines.push('', '🧠 **可复用知识:**');
48
+ for (const r of record.reusableInsights) {
49
+ lines.push(`• [${r.category}] ${r.content}`);
50
+ lines.push(` 适用场景:${r.scenario}`);
51
+ }
52
+ }
53
+ await this.replyText(messageId, lines.join('\n'));
54
+ }
55
+ async replyRecordList(messageId, records) {
56
+ if (records.length === 0) {
57
+ await this.replyText(messageId, '暂无会议记录');
58
+ return;
59
+ }
60
+ const lines = [`📂 共 ${records.length} 条会议记录:`, ''];
61
+ for (const r of records) {
62
+ lines.push(`• ${r.id}`);
63
+ lines.push(` ${r.summary.title}(${r.summary.date})`);
64
+ if (r.documentUrl)
65
+ lines.push(` ${r.documentUrl}`);
66
+ lines.push('');
67
+ }
68
+ lines.push('使用 /show <id> 查看详情,/del <id> 删除记录');
69
+ await this.replyText(messageId, lines.join('\n'));
70
+ }
71
+ async replyRecordDetail(messageId, record) {
72
+ const lines = [
73
+ `📋 **${record.summary.title}**`,
74
+ `📅 日期:${record.summary.date}`,
75
+ `👥 参与人:${record.summary.participants.join('、')}`,
76
+ `🆔 ${record.id}`,
77
+ ];
78
+ if (record.documentUrl) {
79
+ lines.push(`🔗 ${record.documentUrl}`);
80
+ }
81
+ lines.push('', '**核心要点:**');
82
+ for (const p of record.summary.keyPoints) {
83
+ lines.push(`• ${p}`);
84
+ }
85
+ if (record.commitments && record.commitments.length > 0) {
86
+ lines.push('', '🤝 **关键共识与承诺:**');
87
+ for (const c of record.commitments) {
88
+ lines.push(`• ${c.content}`);
89
+ lines.push(` 相关方:${c.participants.join('、')}${c.deadline ? `,截止 ${c.deadline}` : ''}`);
90
+ lines.push(` 💡 ${c.context}`);
91
+ }
92
+ }
93
+ if (record.reusableInsights && record.reusableInsights.length > 0) {
94
+ lines.push('', '🧠 **可复用知识:**');
95
+ for (const r of record.reusableInsights) {
96
+ lines.push(`• [${r.category}] ${r.content}`);
97
+ lines.push(` 适用场景:${r.scenario}`);
98
+ }
99
+ }
100
+ if (record.todos && record.todos.length > 0) {
101
+ lines.push('', '**待办事项:**');
102
+ for (const t of record.todos) {
103
+ lines.push(`• ${t.content}(${t.owner},截止 ${t.deadline})`);
104
+ }
105
+ }
106
+ if (record.risks && record.risks.length > 0) {
107
+ lines.push('', '**风险项:**');
108
+ for (const r of record.risks) {
109
+ lines.push(`• [${r.severity}] ${r.description}`);
110
+ }
111
+ }
112
+ await this.replyText(messageId, lines.join('\n'));
113
+ }
114
+ async replySearchResults(messageId, results) {
115
+ if (results.length === 0) {
116
+ await this.replyText(messageId, '未找到相关会议记录');
117
+ return;
118
+ }
119
+ const lines = ['🔍 找到以下相关会议:', ''];
120
+ for (const r of results.slice(0, 3)) {
121
+ lines.push(`• **${r.summary.title}**(${r.summary.date})`);
122
+ lines.push(` 要点:${r.summary.keyPoints[0] || '无'}`);
123
+ lines.push(` 链接:${r.documentUrl}`);
124
+ lines.push('');
125
+ }
126
+ await this.replyText(messageId, lines.join('\n'));
127
+ }
128
+ async replyTodoConfirmation(messageId, todos) {
129
+ const lines = [
130
+ `📝 检测到 ${todos.length} 条待办,是否创建飞书任务?`,
131
+ '',
132
+ ];
133
+ todos.forEach((t, i) => {
134
+ lines.push(`${i + 1}. [${t.owner}] ${t.content}${t.deadline ? `(截止 ${t.deadline})` : ''}`);
135
+ });
136
+ lines.push('', '回复 /confirm all 创建全部');
137
+ lines.push('回复 /confirm 1,2 创建选中的任务');
138
+ lines.push('回复 /reject 取消创建');
139
+ lines.push('', '⏱ 1 分钟内未回复将自动取消');
140
+ await this.replyText(messageId, lines.join('\n'));
141
+ }
142
+ async replyTaskResults(messageId, results) {
143
+ const lines = ['📋 飞书任务创建结果:', ''];
144
+ for (const r of results) {
145
+ if (r.success) {
146
+ lines.push(`✅ ${r.summary}`);
147
+ if (r.url)
148
+ lines.push(` ${r.url}`);
149
+ }
150
+ else {
151
+ lines.push(`❌ ${r.summary} — ${r.error || '创建失败'}`);
152
+ }
153
+ }
154
+ await this.replyText(messageId, lines.join('\n'));
155
+ }
156
+ }
@@ -0,0 +1,20 @@
1
+ export declare enum MessageType {
2
+ DOCUMENT_LINK = "document_link",
3
+ FIND_QUERY = "find_query",
4
+ LIST_ALL = "list_all",
5
+ DELETE_RECORD = "delete_record",
6
+ SHOW_DETAIL = "show_detail",
7
+ CONFIRM_TASKS = "confirm_tasks",
8
+ REJECT_TASKS = "reject_tasks",
9
+ UNKNOWN = "unknown"
10
+ }
11
+ export interface ParsedMessage {
12
+ type: MessageType;
13
+ documentId?: string;
14
+ query?: string;
15
+ deleteId?: string;
16
+ showId?: string;
17
+ confirmIds?: number[] | 'all';
18
+ raw: string;
19
+ }
20
+ export declare function parseMessage(text: string): ParsedMessage;
@@ -0,0 +1,75 @@
1
+ import { extractDocumentId } from './docReader';
2
+ export var MessageType;
3
+ (function (MessageType) {
4
+ MessageType["DOCUMENT_LINK"] = "document_link";
5
+ MessageType["FIND_QUERY"] = "find_query";
6
+ MessageType["LIST_ALL"] = "list_all";
7
+ MessageType["DELETE_RECORD"] = "delete_record";
8
+ MessageType["SHOW_DETAIL"] = "show_detail";
9
+ MessageType["CONFIRM_TASKS"] = "confirm_tasks";
10
+ MessageType["REJECT_TASKS"] = "reject_tasks";
11
+ MessageType["UNKNOWN"] = "unknown";
12
+ })(MessageType || (MessageType = {}));
13
+ export function parseMessage(text) {
14
+ const trimmed = text.trim();
15
+ // /reject 或 "取消创建"/"不创建"/"跳过待办"
16
+ if (/^(?:\/reject|取消创建|不创建|不用创建|跳过待办|跳过|算了)$/i.test(trimmed)) {
17
+ return { type: MessageType.REJECT_TASKS, raw: text };
18
+ }
19
+ // /confirm all 或 /confirm 1,2,3 或自然语言"确认创建全部/确认创建 1,2"
20
+ const confirmAllMatch = /^(?:\/confirm\s+all|确认创建全部|全部创建)$/i.test(trimmed);
21
+ if (confirmAllMatch) {
22
+ return { type: MessageType.CONFIRM_TASKS, confirmIds: 'all', raw: text };
23
+ }
24
+ const confirmMatch = trimmed.match(/^(?:\/confirm|确认创建)\s+([\d,\s]+)/);
25
+ if (confirmMatch) {
26
+ const ids = confirmMatch[1].split(/[,\s,]+/).map(Number).filter((n) => n > 0);
27
+ if (ids.length > 0) {
28
+ return { type: MessageType.CONFIRM_TASKS, confirmIds: ids, raw: text };
29
+ }
30
+ }
31
+ // /del <id> 或 "删掉/删除 <id>"
32
+ const delMatch = trimmed.match(/^(?:\/del|删掉|删除)\s+(.+)/);
33
+ if (delMatch) {
34
+ return {
35
+ type: MessageType.DELETE_RECORD,
36
+ deleteId: delMatch[1].trim(),
37
+ raw: text,
38
+ };
39
+ }
40
+ // /show <id> 或 "展示/列出/查看 <id> 的详情" 等
41
+ const showMatch = trimmed.match(/^(?:\/show)\s+(.+)/) ||
42
+ trimmed.match(/^(?:展示|列出|显示|查看)\s*(meeting_\w+)/) ||
43
+ trimmed.match(/^(?:展示|列出|显示|查看)\s*(.+?)的?详情/);
44
+ if (showMatch) {
45
+ return {
46
+ type: MessageType.SHOW_DETAIL,
47
+ showId: showMatch[1].trim(),
48
+ raw: text,
49
+ };
50
+ }
51
+ // /listall 或自然语言匹配
52
+ if (/^\/listall$/i.test(trimmed) || /^(展示|列出|显示|查看)(所有|全部|历史)(记录|会议)/.test(trimmed)) {
53
+ return { type: MessageType.LIST_ALL, raw: text };
54
+ }
55
+ const findMatch = text.match(/^\/find\s+(.+)/);
56
+ if (findMatch) {
57
+ return {
58
+ type: MessageType.FIND_QUERY,
59
+ query: findMatch[1].trim(),
60
+ raw: text,
61
+ };
62
+ }
63
+ const urlMatch = text.match(/(https?:\/\/[^\s]*feishu\.cn\/[^\s]+)/);
64
+ if (urlMatch) {
65
+ const docId = extractDocumentId(urlMatch[1]);
66
+ if (docId) {
67
+ return {
68
+ type: MessageType.DOCUMENT_LINK,
69
+ documentId: docId,
70
+ raw: text,
71
+ };
72
+ }
73
+ }
74
+ return { type: MessageType.UNKNOWN, raw: text };
75
+ }
@@ -0,0 +1,15 @@
1
+ import * as lark from '@larksuiteoapi/node-sdk';
2
+ export interface CreateTaskResult {
3
+ taskId: string;
4
+ url: string;
5
+ }
6
+ export declare class TaskCreator {
7
+ private readonly client;
8
+ constructor(client: lark.Client);
9
+ createTask(params: {
10
+ summary: string;
11
+ due?: string;
12
+ description?: string;
13
+ assigneeOpenId?: string;
14
+ }): Promise<CreateTaskResult>;
15
+ }