@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/README.md ADDED
@@ -0,0 +1,282 @@
1
+ # KMR(Key Meetings Record)
2
+
3
+ 关键会议记录服务 — 从飞书会议纪要中提取关键信息并持久化保存,帮助你在遗忘后通过只言片语快速找回历史会议。
4
+
5
+ ## 功能
6
+
7
+ - **会议纪要提取**:发送飞书云文档链接给机器人,自动提取会议摘要、待办事项、风险项、关键共识、可复用知识
8
+ - **飞书任务创建**:提取待办后自动发送确认消息,回复 `/confirm` 创建飞书任务,`/reject` 取消,1 分钟超时自动取消
9
+ - **AI 对话**:发送非指令文本即可与 AI 自由对话,基于 acpx claude 持久化 session,按用户隔离
10
+ - **历史会议查询**:通过 `/find` 指令用自然语言搜索历史会议记录
11
+ - **记录管理**:`/listall` 列出所有记录,`/show <id>` 查看详情,`/del <id>` 删除记录
12
+ - **群聊智能识别**:群聊中非 @消息自动识别会议纪要文档链接(通过文档标题判断),@机器人可使用所有指令和 AI 对话
13
+ - **本地持久化**:所有数据以 JSON 文件存储在 `~/.kmr/` 目录下,支持 grep 检索
14
+ - **Web 管理界面**:服务启动时自动打开浏览器,查看会议记录、AI 会话、管理配置
15
+
16
+ ## 核心理念:不是记录员,而是分析师
17
+
18
+ KMR 不做流水账式的会议记录搬运。每条提取结果都围绕两个核心目标:
19
+
20
+ ### 1. 溯源追责 — 关键共识与承诺
21
+
22
+ > 谁答应了什么?什么时候完成?定下了什么约定?
23
+
24
+ 会议中达成的承诺、约定、决议是最容易被遗忘也最容易被否认的。KMR 会提取每一条共识,标注相关方和截止日期,并由 Agent 分析达成背景——事后有人不认账时,一搜即得。
25
+
26
+ ```json
27
+ {
28
+ "content": "前端重构方案采用渐进式迁移,不做大爆炸重写",
29
+ "participants": ["张三", "李四"],
30
+ "deadline": "2026-06-01",
31
+ "context": "因为当前版本仍在高频迭代,大规模重写会阻塞业务需求至少两个月,团队评估后一致选择渐进方案以降低风险"
32
+ }
33
+ ```
34
+
35
+ ### 2. 知识复用 — 可提效的工具与方法
36
+
37
+ > 会上提到的工具用法、方法论、经验教训,下次用得上吗?
38
+
39
+ 有价值的信息往往在会议中一闪而过。KMR 会按 `tool`(工具用法)、`methodology`(方法论)、`lesson`(经验教训)、`best-practice`(最佳实践)分类提取,标注适用场景,形成可检索的知识库。
40
+
41
+ ```json
42
+ {
43
+ "category": "tool",
44
+ "content": "用 pnpm patch 可以临时修复第三方包的 bug,不用 fork 仓库",
45
+ "scenario": "依赖包有 bug 但官方还没发版时,用 pnpm patch 打补丁可以快速解决而不引入维护负担",
46
+ "source": "王五"
47
+ }
48
+ ```
49
+
50
+ ## 前置条件
51
+
52
+ - Node.js >= 18
53
+ - [acpx](https://www.npmjs.com/package/acpx) 已全局安装(`npm install -g acpx@latest`)
54
+ - Claude Code 已安装并可通过 acpx 调用
55
+ - 飞书自建应用,开启以下权限:
56
+ - `im:message:receive_v1`(接收消息事件)
57
+ - `im:message`(发送消息)
58
+ - `docs:document.content:read`(读取文档内容)
59
+ - `docs:document:readonly`(读取文档元信息,用于标题识别)
60
+ - `task:task:write`(创建飞书任务)
61
+
62
+ ## 快速开始
63
+
64
+ ### 方式一:全局安装(推荐)
65
+
66
+ ```bash
67
+ npm install -g kmr
68
+ kmr init # 初始化配置
69
+ # 编辑 ~/.kmr/config.json 填入飞书凭证
70
+ kmr # 启动服务
71
+ ```
72
+
73
+ ### 方式二:克隆源码
74
+
75
+ ```bash
76
+ git clone <repo-url> && cd kmr
77
+ npm install
78
+ npm run init # 初始化配置
79
+ npm run dev # 启动服务
80
+ ```
81
+
82
+ ### 配置文件
83
+
84
+ 编辑 `~/.kmr/config.json`:
85
+
86
+ ```json
87
+ {
88
+ "lark": {
89
+ "appId": "你的飞书应用 App ID",
90
+ "appSecret": "你的飞书应用 App Secret"
91
+ },
92
+ "agent": {
93
+ "provider": "claude-code",
94
+ "timeout": 120000
95
+ },
96
+ "storage": {
97
+ "dataDir": "~/.kmr/data/meetings"
98
+ }
99
+ }
100
+ ```
101
+
102
+ 配置完成后运行 `kmr` 或 `npm run dev` 启动服务。
103
+
104
+ ## 使用方式
105
+
106
+ ### 提取会议纪要
107
+
108
+ 在飞书中向机器人发送会议云文档链接:
109
+
110
+ ```
111
+ https://xxx.feishu.cn/docx/abc123
112
+ ```
113
+
114
+ 机器人会自动读取文档内容,提取结构化信息并回复摘要。
115
+
116
+ ### 搜索历史会议
117
+
118
+ ```
119
+ /find 关于用户增长的会议
120
+ /find 上次讨论技术架构的内容
121
+ /find 处理A事件的文档
122
+ ```
123
+
124
+ 机器人会返回最相关的 3 条会议记录及原始文档链接。
125
+
126
+ ### 管理会议记录
127
+
128
+ ```
129
+ /listall # 列出所有记录(ID + 标题 + 链接)
130
+ 展示所有记录 # 自然语言同样支持
131
+
132
+ /show meeting_1714380000 # 查看某条记录的完整详情
133
+ 查看 meeting_1714380000 详情 # 自然语言同样支持
134
+
135
+ /del meeting_1714380000 # 删除某条记录
136
+ 删掉 meeting_1714380000 # 自然语言同样支持
137
+ ```
138
+
139
+ ### 创建飞书任务
140
+
141
+ 会议纪要提取后,如果包含待办事项,机器人会自动发送确认消息:
142
+
143
+ ```
144
+ 📝 检测到 3 条待办,是否创建飞书任务?
145
+
146
+ 1. [张三] 完成前端重构方案(截止 2026-05-15)
147
+ 2. [李四] 提交性能测试报告(截止 2026-05-10)
148
+ 3. [王五] 更新 API 文档(截止 2026-05-12)
149
+
150
+ 回复 /confirm all 创建全部
151
+ 回复 /confirm 1,2 创建选中的任务
152
+ 回复 /reject 取消创建
153
+
154
+ ⏱ 1 分钟内未回复将自动取消
155
+ ```
156
+
157
+ 创建的任务会自动设置截止时间,并将确认者设为任务负责人。任务信息同步回写到会议记录中。
158
+
159
+ ### AI 自由对话
160
+
161
+ 发送非指令文本即可与 AI 对话,每个用户拥有独立的持久化会话:
162
+
163
+ ```
164
+ 你好,帮我总结一下最近的技术方案
165
+ ```
166
+
167
+ 会话基于 acpx claude session,上下文在多轮对话间保持。
168
+
169
+ ### 群聊使用
170
+
171
+ **@机器人**:可使用所有指令和 AI 对话,与私聊体验一致。
172
+
173
+ ```
174
+ @KMR /listall
175
+ @KMR /find 技术方案评审
176
+ @KMR https://xxx.feishu.cn/docx/abc123
177
+ ```
178
+
179
+ **无需@**:以下场景机器人会自动响应:
180
+ - 发送会议纪要文档链接(通过文档标题中的"纪要/会议记录"等关键词自动识别)
181
+ - 回复 `/confirm`、`/reject` 确认或取消创建任务
182
+
183
+ 其他消息机器人不会响应,不会打扰群聊。
184
+
185
+ ### Web 管理界面
186
+
187
+ 服务启动后会自动打开浏览器,访问 `http://localhost:3000`。
188
+
189
+ - **会议记录**:查看所有已提取的会议,点击展开查看详情(含共识、知识、已创建任务等)
190
+ - **AI 会话**:查看各用户的对话历史,支持删除
191
+ - **设置**:点击右上角齿轮图标,配置飞书 App ID/Secret、Agent 参数等
192
+
193
+ ## 项目结构
194
+
195
+ ```
196
+ kmr/
197
+ ├── src/
198
+ │ ├── index.ts # 服务入口
199
+ │ ├── config.ts # 配置管理(~/.kmr/config.json)
200
+ │ ├── cli-init.ts # kmr init 命令
201
+ │ ├── lark/
202
+ │ │ ├── client.ts # 飞书长连接客户端(消息去重、群聊过滤)
203
+ │ │ ├── docReader.ts # 飞书文档内容读取(含标题获取)
204
+ │ │ ├── messenger.ts # 飞书消息回复(摘要、确认、任务结果等)
205
+ │ │ ├── router.ts # 消息路由(指令解析、自然语言匹配)
206
+ │ │ └── taskCreator.ts # 飞书任务创建(Task API v2)
207
+ │ ├── agent/
208
+ │ │ ├── types.ts # AgentProvider 接口
209
+ │ │ ├── claudeCode.ts # Claude Code 实现(acpx)
210
+ │ │ └── prompt.ts # Prompt 模板
211
+ │ ├── session/
212
+ │ │ ├── manager.ts # AI 会话管理(per-user acpx session)
213
+ │ │ └── skill.ts # 会话角色设定模板
214
+ │ ├── query/
215
+ │ │ ├── finder.ts # grep 匹配
216
+ │ │ └── handler.ts # /find 编排
217
+ │ ├── web/
218
+ │ │ ├── server.ts # HTTP 服务器 + REST API
219
+ │ │ ├── openBrowser.ts # 自动打开浏览器
220
+ │ │ └── public/ # 前端静态文件
221
+ │ │ ├── index.html
222
+ │ │ ├── style.css
223
+ │ │ └── app.js
224
+ │ └── storage/
225
+ │ ├── types.ts # 数据类型定义
226
+ │ └── jsonStore.ts # JSON 文件存储
227
+ ├── tests/ # 测试文件
228
+ ├── package.json
229
+ └── tsconfig.json
230
+ ```
231
+
232
+ ## 数据存储
233
+
234
+ 会议记录存储在 `~/.kmr/data/meetings/` 下,每个会议一个 JSON 文件:
235
+
236
+ ```
237
+ ~/.kmr/
238
+ ├── config.json
239
+ ├── data/
240
+ │ └── meetings/
241
+ │ ├── 2026-04-29_meeting_1714380000.json
242
+ │ └── 2026-04-30_meeting_1714466400.json
243
+ └── sessions/
244
+ └── <userId>/
245
+ ├── skill.md # AI 角色设定
246
+ └── summary.md # 对话历史
247
+ ```
248
+
249
+ 每条会议记录包含:会议摘要、关键共识与承诺(含 Agent 背景分析)、可复用知识(工具/方法/经验)、待办事项、风险项、项目关联、已创建的飞书任务、原始文档文本。
250
+
251
+ ## 开发
252
+
253
+ ```bash
254
+ # 运行测试
255
+ npm test
256
+
257
+ # 监听模式
258
+ npm run test:watch
259
+
260
+ # 类型检查
261
+ npx tsc --noEmit
262
+
263
+ # 构建
264
+ npm run build
265
+ ```
266
+
267
+ ## 架构扩展
268
+
269
+ Agent 模块通过 `AgentProvider` 接口抽象,当前仅实现 Claude Code。后续可扩展其他 Agent(如 Kiro、Gemini 等),只需实现该接口:
270
+
271
+ ```typescript
272
+ interface AgentProvider {
273
+ name: string;
274
+ extract(content: string): Promise<MeetingRecord>;
275
+ searchKeywords(query: string): Promise<string[]>;
276
+ rankResults(query: string, candidates: MeetingRecord[]): Promise<MeetingRecord[]>;
277
+ }
278
+ ```
279
+
280
+ ## 许可
281
+
282
+ ISC
@@ -0,0 +1,12 @@
1
+ import { AgentProvider } from './types';
2
+ import { MeetingRecord } from '../storage/types';
3
+ export declare class ClaudeCodeProvider implements AgentProvider {
4
+ private readonly timeout;
5
+ name: string;
6
+ constructor(timeout?: number);
7
+ extract(content: string): Promise<MeetingRecord>;
8
+ searchKeywords(query: string): Promise<string[]>;
9
+ rankResults(query: string, candidates: MeetingRecord[]): Promise<MeetingRecord[]>;
10
+ private callAcpx;
11
+ private extractJson;
12
+ }
@@ -0,0 +1,109 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ import { EXTRACT_PROMPT, SEARCH_KEYWORDS_PROMPT, RANK_RESULTS_PROMPT } from './prompt';
4
+ const execFileAsync = promisify(execFile);
5
+ export class ClaudeCodeProvider {
6
+ timeout;
7
+ name = 'claude-code';
8
+ constructor(timeout = 120000) {
9
+ this.timeout = timeout;
10
+ }
11
+ async extract(content) {
12
+ const prompt = EXTRACT_PROMPT + content;
13
+ //打印出要发送给acpx的prompt,方便调试
14
+ console.log(`[ClaudeCodeProvider] 提取信息的 prompt:\n${prompt}\n--- End of Prompt ---`);
15
+ const output = await this.callAcpx(prompt);
16
+ const parsed = JSON.parse(output);
17
+ return {
18
+ id: `meeting_${Date.now()}`,
19
+ documentUrl: '',
20
+ extractedAt: new Date().toISOString(),
21
+ summary: parsed.summary,
22
+ todos: parsed.todos || [],
23
+ risks: parsed.risks || [],
24
+ projectRelations: parsed.projectRelations || [],
25
+ commitments: parsed.commitments || [],
26
+ reusableInsights: parsed.reusableInsights || [],
27
+ rawContent: content,
28
+ };
29
+ }
30
+ async searchKeywords(query) {
31
+ const prompt = SEARCH_KEYWORDS_PROMPT + query;
32
+ const output = await this.callAcpx(prompt);
33
+ return JSON.parse(output);
34
+ }
35
+ async rankResults(query, candidates) {
36
+ if (candidates.length <= 1)
37
+ return candidates;
38
+ const summaries = candidates.map((c) => ({
39
+ id: c.id,
40
+ title: c.summary.title,
41
+ keyPoints: c.summary.keyPoints,
42
+ }));
43
+ const prompt = RANK_RESULTS_PROMPT + query + '\n\n候选会议:\n' + JSON.stringify(summaries, null, 2);
44
+ const output = await this.callAcpx(prompt);
45
+ const rankedIds = JSON.parse(output);
46
+ const indexed = new Map(candidates.map((c) => [c.id, c]));
47
+ return rankedIds.map((id) => indexed.get(id)).filter(Boolean);
48
+ }
49
+ async callAcpx(prompt) {
50
+ try {
51
+ const { stdout } = await execFileAsync('acpx', ['--allowed-tools', '', 'claude', 'exec', prompt], {
52
+ timeout: this.timeout,
53
+ maxBuffer: 1024 * 1024,
54
+ });
55
+ return this.extractJson(stdout);
56
+ }
57
+ catch (err) {
58
+ if (err.killed) {
59
+ throw new Error(`acpx 调用超时 (${this.timeout}ms)`);
60
+ }
61
+ throw new Error(`acpx 调用失败: ${err.message}`);
62
+ }
63
+ }
64
+ extractJson(output) {
65
+ // acpx 输出包含 [client]、[tool]、[thinking] 等元信息前缀行
66
+ // 需要过滤掉这些,只提取 JSON 内容
67
+ const lines = output.split('\n');
68
+ // 策略1:找到第一个以 { 或 [ 开头的行,取从那行到最后一个 } 或 ] 的内容
69
+ let jsonStart = -1;
70
+ let jsonEnd = -1;
71
+ for (let i = 0; i < lines.length; i++) {
72
+ const trimmed = lines[i].trim();
73
+ if (jsonStart === -1 && (trimmed.startsWith('{') || trimmed.startsWith('['))) {
74
+ jsonStart = i;
75
+ }
76
+ if (trimmed.endsWith('}') || trimmed.endsWith(']')) {
77
+ jsonEnd = i;
78
+ }
79
+ }
80
+ if (jsonStart !== -1 && jsonEnd >= jsonStart) {
81
+ const candidate = lines.slice(jsonStart, jsonEnd + 1).join('\n').trim();
82
+ try {
83
+ JSON.parse(candidate);
84
+ return candidate;
85
+ }
86
+ catch {
87
+ // fall through
88
+ }
89
+ }
90
+ // 策略2:过滤掉已知的 acpx 元信息行
91
+ const filtered = lines
92
+ .filter((line) => {
93
+ const t = line.trim();
94
+ return t.length > 0 &&
95
+ !t.startsWith('[client]') &&
96
+ !t.startsWith('[tool]') &&
97
+ !t.startsWith('[thinking]') &&
98
+ !t.startsWith('[done]') &&
99
+ !t.startsWith('[error]') &&
100
+ !t.startsWith('[warn]') &&
101
+ !t.startsWith('[info]');
102
+ })
103
+ .join('\n')
104
+ .trim();
105
+ if (filtered.length > 0)
106
+ return filtered;
107
+ return output.trim();
108
+ }
109
+ }
@@ -0,0 +1,3 @@
1
+ export declare const EXTRACT_PROMPT = "\u4F60\u662F\u4E00\u4E2A\u9AD8\u7EA7\u4F1A\u8BAE\u5206\u6790\u5E08\uFF0C\u4E0D\u662F\u8BB0\u5F55\u5458\u3002\u4F60\u7684\u4EFB\u52A1\u662F\u4ECE\u4F1A\u8BAE\u7EAA\u8981\u4E2D\u63D0\u70BC\u771F\u6B63\u6709\u4EF7\u503C\u7684\u4FE1\u606F\uFF0C\u800C\u4E0D\u662F\u6D41\u6C34\u8D26\u5F0F\u590D\u8FF0\u3002\n\n\u4F60\u9700\u8981\u5E26\u7740\u4E24\u4E2A\u6838\u5FC3\u76EE\u6807\u5206\u6790\u4F1A\u8BAE\u5185\u5BB9\uFF1A\n1. **\u6EAF\u6E90\u8FFD\u8D23**\uFF1A\u63D0\u53D6\u6240\u6709\u660E\u786E\u7684\u627F\u8BFA\u3001\u7EA6\u5B9A\u3001\u51B3\u8BAE\u2014\u2014\u8C01\u7B54\u5E94\u4E86\u4EC0\u4E48\u3001\u4EC0\u4E48\u65F6\u5019\u5B8C\u6210\u3001\u8FBE\u6210\u4E86\u4EC0\u4E48\u5171\u8BC6\u3002\u5177\u4F53\u7684\u4EBA\u548C\u5177\u4F53\u7684\u65F6\u95F4\u662F\u6838\u5FC3\u951A\u70B9\uFF0C\u8FD9\u4E9B\u4FE1\u606F\u7528\u4E8E\u4E8B\u540E\u5FEB\u901F\u6EAF\u6E90\u3002\n2. **\u77E5\u8BC6\u590D\u7528**\uFF1A\u63D0\u53D6\u4F1A\u8BAE\u4E2D\u63D0\u5230\u7684\u5DE5\u5177\u7528\u6CD5\u3001\u65B9\u6CD5\u8BBA\u3001\u7ECF\u9A8C\u6559\u8BAD\u3001\u6700\u4F73\u5B9E\u8DF5\u2014\u2014\u8FD9\u4E9B\u53EF\u4EE5\u63D0\u5347\u5DE5\u4F5C\u6548\u7387\u7684\u53EF\u590D\u7528\u77E5\u8BC6\u3002\n\n\u5206\u6790\u8981\u6C42\uFF1A\n- participants\uFF1A\u5FC5\u987B\u5217\u51FA\u7EAA\u8981\u6587\u6863\u4E2D\u8BB0\u5F55\u7684\u53C2\u4E0E\u8BA8\u8BBA\u548C\u51B3\u7B56\u6216\u8005\u88AB\u63D0\u53CA\u7684\u6838\u5FC3\u6210\u5458\u540D\u5B57\n- keyPoints\uFF1A\u53EA\u4FDD\u7559\u6700\u5173\u952E\u7684 3-8 \u4E2A\u51B3\u7B56\u6027\u8981\u70B9\uFF0C\u7F57\u5217\u7B80\u8981\u8BA8\u8BBA\u8FC7\u7A0B\n- todos\uFF1A\u53EA\u63D0\u53D6\u6709\u660E\u786E\u8D1F\u8D23\u4EBA\u548C\u53EF\u6267\u884C\u5185\u5BB9\u7684\u5F85\u529E\uFF0C\u4E0D\u8981\u6A21\u7CCA\u7684\"\u540E\u7EED\u8DDF\u8FDB\"\n- risks\uFF1A\u53EA\u63D0\u53D6\u771F\u6B63\u7684\u98CE\u9669\u548C\u672A\u51B3\u4E8B\u9879\uFF0C\u8981\u6709\u5177\u4F53\u7684\u7F13\u89E3\u65B9\u6848\n- commitments\u3010\u6700\u91CD\u8981\u3011\uFF1A\u63D0\u53D6\u6240\u6709\u627F\u8BFA\u3001\u7EA6\u5B9A\u3001\u51B3\u8BAE\u3002context \u5B57\u6BB5\u5FC5\u987B\u5199\u4F60\u7684\u5206\u6790\u2014\u2014\u4E3A\u4EC0\u4E48\u4F1A\u8FBE\u6210\u8FD9\u4E2A\u5171\u8BC6\uFF0C\u80CC\u666F\u662F\u4EC0\u4E48\uFF0C\u8C01\u8FBE\u6210\u7684\uFF0C\u4E0D\u80FD\u53EA\u642C\u8FD0\u539F\u6587\n- reusableInsights\u3010\u6700\u91CD\u8981\u3011\uFF1A\u63D0\u53D6\u53EF\u590D\u7528\u7684\u77E5\u8BC6\u3002scenario \u5B57\u6BB5\u5FC5\u987B\u5199\u8FD9\u4E2A\u77E5\u8BC6\u5728\u4EC0\u4E48\u573A\u666F\u4E0B\u6709\u7528\uFF0C\u4E0D\u80FD\u6CDB\u6CDB\u800C\u8C08\uFF0C\u8981\u6709\u5177\u4F53\u64CD\u4F5C\u5B9E\u8DF5\u6B65\u9AA4\u6307\u5BFC\n\n\u3010\u91CD\u8981\u3011\u76F4\u63A5\u8F93\u51FA JSON\uFF0C\u4E0D\u8981\u8F93\u51FA\u4EFB\u4F55\u5176\u4ED6\u6587\u5B57\u3001\u89E3\u91CA\u6216 markdown \u4EE3\u7801\u5757\u3002\u4EC5\u8F93\u51FA\u4EE5\u4E0B\u683C\u5F0F\u7684 JSON\uFF1A\n{\"summary\":{\"title\":\"\u4F1A\u8BAE\u6807\u9898\",\"date\":\"YYYY-MM-DD\",\"participants\":[\"\u53C2\u4E0E\u4EBA1\"],\"keyPoints\":[\"\u51B3\u7B56\u6027\u8981\u70B9\uFF0C\u4E0D\u662F\u6D41\u6C34\u8D26\"]},\"todos\":[{\"content\":\"\u5177\u4F53\u53EF\u6267\u884C\u5185\u5BB9\",\"owner\":\"\u8D1F\u8D23\u4EBA\",\"deadline\":\"YYYY-MM-DD\",\"status\":\"pending\"}],\"risks\":[{\"description\":\"\u5177\u4F53\u98CE\u9669\",\"severity\":\"high|medium|low\",\"mitigation\":\"\u5177\u4F53\u7F13\u89E3\u65B9\u6848\"}],\"projectRelations\":[{\"project\":\"\u9879\u76EE\u540D\",\"relation\":\"\u5173\u8054\u63CF\u8FF0\"}],\"commitments\":[{\"content\":\"\u627F\u8BFA/\u7EA6\u5B9A/\u51B3\u8BAE\u7684\u5177\u4F53\u5185\u5BB9\",\"participants\":[\"\u76F8\u5173\u65B91\",\"\u76F8\u5173\u65B92\"],\"deadline\":\"YYYY-MM-DD\",\"context\":\"\u4F60\u7684\u5206\u6790\uFF1A\u4E3A\u4EC0\u4E48\u8FBE\u6210\u8FD9\u4E2A\u5171\u8BC6\uFF0C\u80CC\u666F\u548C\u5F71\u54CD\u662F\u4EC0\u4E48\"}],\"reusableInsights\":[{\"category\":\"tool|methodology|lesson|best-practice\",\"content\":\"\u77E5\u8BC6\u70B9\u7684\u5177\u4F53\u5185\u5BB9\",\"scenario\":\"\u5728\u4EC0\u4E48\u573A\u666F\u4E0B\u53EF\u4EE5\u590D\u7528\u8FD9\u4E2A\u77E5\u8BC6\",\"source\":\"\u8C01\u63D0\u51FA\u7684\"}]}\n\n\u4F1A\u8BAE\u7EAA\u8981\u5185\u5BB9\uFF1A\n";
2
+ export declare const SEARCH_KEYWORDS_PROMPT = "\u4F60\u662F\u4E00\u4E2A\u641C\u7D22\u52A9\u624B\u3002\u7528\u6237\u60F3\u67E5\u627E\u5386\u53F2\u4F1A\u8BAE\u8BB0\u5F55\uFF0C\u8BF7\u5C06\u7528\u6237\u7684\u81EA\u7136\u8BED\u8A00\u67E5\u8BE2\u8F6C\u6362\u4E3A\u641C\u7D22\u5173\u952E\u8BCD\u5217\u8868\u3002\n\n\u8981\u6C42\uFF1A\n- \u8FD4\u56DE 3-5 \u4E2A\u6700\u76F8\u5173\u7684\u5173\u952E\u8BCD\n- \u4EC5\u8FD4\u56DE JSON \u6570\u7EC4\u683C\u5F0F\uFF0C\u4E0D\u8981\u5176\u4ED6\u5185\u5BB9\n- \u793A\u4F8B\uFF1A[\"\u5173\u952E\u8BCD1\", \"\u5173\u952E\u8BCD2\", \"\u5173\u952E\u8BCD3\"]\n\n\u7528\u6237\u67E5\u8BE2\uFF1A";
3
+ export declare const RANK_RESULTS_PROMPT = "\u4F60\u662F\u4E00\u4E2A\u641C\u7D22\u6392\u5E8F\u52A9\u624B\u3002\u7528\u6237\u67E5\u8BE2\u548C\u5019\u9009\u4F1A\u8BAE\u8BB0\u5F55\u5982\u4E0B\uFF0C\u8BF7\u6309\u76F8\u5173\u6027\u4ECE\u9AD8\u5230\u4F4E\u6392\u5E8F\u3002\n\n\u4EC5\u8FD4\u56DE\u6392\u5E8F\u540E\u7684\u4F1A\u8BAE ID JSON \u6570\u7EC4\uFF0C\u4E0D\u8981\u5176\u4ED6\u5185\u5BB9\u3002\n\u793A\u4F8B\uFF1A[\"meeting_003\", \"meeting_001\"]\n\n\u7528\u6237\u67E5\u8BE2\uFF1A";
@@ -0,0 +1,33 @@
1
+ export const EXTRACT_PROMPT = `你是一个高级会议分析师,不是记录员。你的任务是从会议纪要中提炼真正有价值的信息,而不是流水账式复述。
2
+
3
+ 你需要带着两个核心目标分析会议内容:
4
+ 1. **溯源追责**:提取所有明确的承诺、约定、决议——谁答应了什么、什么时候完成、达成了什么共识。具体的人和具体的时间是核心锚点,这些信息用于事后快速溯源。
5
+ 2. **知识复用**:提取会议中提到的工具用法、方法论、经验教训、最佳实践——这些可以提升工作效率的可复用知识。
6
+
7
+ 分析要求:
8
+ - participants:必须列出纪要文档中记录的参与讨论和决策或者被提及的核心成员名字
9
+ - keyPoints:只保留最关键的 3-8 个决策性要点,罗列简要讨论过程
10
+ - todos:只提取有明确负责人和可执行内容的待办,不要模糊的"后续跟进"
11
+ - risks:只提取真正的风险和未决事项,要有具体的缓解方案
12
+ - commitments【最重要】:提取所有承诺、约定、决议。context 字段必须写你的分析——为什么会达成这个共识,背景是什么,谁达成的,不能只搬运原文
13
+ - reusableInsights【最重要】:提取可复用的知识。scenario 字段必须写这个知识在什么场景下有用,不能泛泛而谈,要有具体操作实践步骤指导
14
+
15
+ 【重要】直接输出 JSON,不要输出任何其他文字、解释或 markdown 代码块。仅输出以下格式的 JSON:
16
+ {"summary":{"title":"会议标题","date":"YYYY-MM-DD","participants":["参与人1"],"keyPoints":["决策性要点,不是流水账"]},"todos":[{"content":"具体可执行内容","owner":"负责人","deadline":"YYYY-MM-DD","status":"pending"}],"risks":[{"description":"具体风险","severity":"high|medium|low","mitigation":"具体缓解方案"}],"projectRelations":[{"project":"项目名","relation":"关联描述"}],"commitments":[{"content":"承诺/约定/决议的具体内容","participants":["相关方1","相关方2"],"deadline":"YYYY-MM-DD","context":"你的分析:为什么达成这个共识,背景和影响是什么"}],"reusableInsights":[{"category":"tool|methodology|lesson|best-practice","content":"知识点的具体内容","scenario":"在什么场景下可以复用这个知识","source":"谁提出的"}]}
17
+
18
+ 会议纪要内容:
19
+ `;
20
+ export const SEARCH_KEYWORDS_PROMPT = `你是一个搜索助手。用户想查找历史会议记录,请将用户的自然语言查询转换为搜索关键词列表。
21
+
22
+ 要求:
23
+ - 返回 3-5 个最相关的关键词
24
+ - 仅返回 JSON 数组格式,不要其他内容
25
+ - 示例:["关键词1", "关键词2", "关键词3"]
26
+
27
+ 用户查询:`;
28
+ export const RANK_RESULTS_PROMPT = `你是一个搜索排序助手。用户查询和候选会议记录如下,请按相关性从高到低排序。
29
+
30
+ 仅返回排序后的会议 ID JSON 数组,不要其他内容。
31
+ 示例:["meeting_003", "meeting_001"]
32
+
33
+ 用户查询:`;
@@ -0,0 +1,7 @@
1
+ import { MeetingRecord } from '../storage/types';
2
+ export interface AgentProvider {
3
+ name: string;
4
+ extract(content: string): Promise<MeetingRecord>;
5
+ searchKeywords(query: string): Promise<string[]>;
6
+ rankResults(query: string, candidates: MeetingRecord[]): Promise<MeetingRecord[]>;
7
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,12 @@
1
+ // src/cli-init.ts
2
+ import { initConfig, getKmrDir } from './config';
3
+ import path from 'node:path';
4
+ function main() {
5
+ const kmrDir = getKmrDir();
6
+ console.log(`初始化 KMR 配置目录: ${kmrDir}`);
7
+ initConfig();
8
+ console.log('✅ 目录结构创建完成');
9
+ console.log(`\n请编辑配置文件填入飞书凭证:\n ${path.join(kmrDir, 'config.json')}`);
10
+ console.log('\n配置完成后运行 npm run dev 启动服务');
11
+ }
12
+ main();
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env node
2
+ import { argv } from 'node:process';
3
+ const command = argv[2];
4
+ if (command === 'init') {
5
+ const { initConfig, getKmrDir } = await import('./config.js');
6
+ const path = await import('node:path');
7
+ const kmrDir = getKmrDir();
8
+ console.log(`初始化 KMR 配置目录: ${kmrDir}`);
9
+ initConfig();
10
+ console.log('✅ 目录结构创建完成');
11
+ console.log(`\n请编辑配置文件填入飞书凭证:\n ${path.join(kmrDir, 'config.json')}`);
12
+ console.log('\n配置完成后运行 kmr 启动服务');
13
+ }
14
+ else if (command === '--help' || command === '-h') {
15
+ console.log(`
16
+ KMR(Key Meetings Record)— 关键会议记录服务
17
+
18
+ 用法:
19
+ kmr 启动服务(飞书机器人 + Web 管理界面)
20
+ kmr init 初始化配置目录 ~/.kmr/
21
+ kmr --help 显示帮助
22
+
23
+ 前置条件:
24
+ - acpx 已全局安装(npm install -g acpx@latest)
25
+ - Claude Code 可通过 acpx 调用
26
+ - 飞书自建应用已创建并配置权限
27
+
28
+ 快速开始:
29
+ 1. kmr init
30
+ 2. 编辑 ~/.kmr/config.json 填入飞书凭证
31
+ 3. kmr
32
+ `);
33
+ }
34
+ else if (command === '--version' || command === '-v') {
35
+ const { createRequire } = await import('node:module');
36
+ const require = createRequire(import.meta.url);
37
+ const pkg = require('../package.json');
38
+ console.log(pkg.version);
39
+ }
40
+ else {
41
+ // 默认启动服务
42
+ await import('./index.js');
43
+ }
@@ -0,0 +1,17 @@
1
+ export interface KmrConfig {
2
+ lark: {
3
+ appId: string;
4
+ appSecret: string;
5
+ };
6
+ agent: {
7
+ provider: string;
8
+ timeout: number;
9
+ };
10
+ storage: {
11
+ dataDir: string;
12
+ };
13
+ }
14
+ export declare function getKmrDir(): string;
15
+ export declare const KMR_DIR: string;
16
+ export declare function initConfig(): void;
17
+ export declare function loadConfig(): KmrConfig;
package/dist/config.js ADDED
@@ -0,0 +1,38 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ export function getKmrDir() {
5
+ return process.env.KMR_HOME || path.join(os.homedir(), '.kmr');
6
+ }
7
+ export const KMR_DIR = getKmrDir();
8
+ const DEFAULT_CONFIG = {
9
+ lark: {
10
+ appId: '',
11
+ appSecret: '',
12
+ },
13
+ agent: {
14
+ provider: 'claude-code',
15
+ timeout: 120000,
16
+ },
17
+ storage: {
18
+ dataDir: path.join(getKmrDir(), 'data', 'meetings'),
19
+ },
20
+ };
21
+ export function initConfig() {
22
+ const kmrDir = getKmrDir();
23
+ const configPath = path.join(kmrDir, 'config.json');
24
+ const dataDir = path.join(kmrDir, 'data', 'meetings');
25
+ fs.mkdirSync(dataDir, { recursive: true });
26
+ if (!fs.existsSync(configPath)) {
27
+ fs.writeFileSync(configPath, JSON.stringify(DEFAULT_CONFIG, null, 2), 'utf-8');
28
+ }
29
+ }
30
+ export function loadConfig() {
31
+ const kmrDir = getKmrDir();
32
+ const configPath = path.join(kmrDir, 'config.json');
33
+ if (!fs.existsSync(configPath)) {
34
+ throw new Error(`配置文件不存在: ${configPath}\n请先运行 kmr init`);
35
+ }
36
+ const raw = fs.readFileSync(configPath, 'utf-8');
37
+ return JSON.parse(raw);
38
+ }
@@ -0,0 +1 @@
1
+ export {};