@lih-x-x/kmr 1.0.15 → 1.0.17
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/README.md +18 -10
- package/dist/index.js +29 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
## 功能
|
|
6
6
|
|
|
7
7
|
- **会议纪要提取**:发送飞书云文档链接给机器人,自动提取会议摘要、待办事项、风险项、关键共识、可复用知识
|
|
8
|
-
- **飞书任务创建**:提取待办后自动发送确认消息,回复 `/confirm` 创建飞书任务,`/reject` 取消,
|
|
9
|
-
- **AI 对话**:发送非指令文本即可与 AI 自由对话,基于 acpx claude
|
|
8
|
+
- **飞书任务创建**:提取待办后自动发送确认消息,回复 `/confirm` 创建飞书任务,`/reject` 取消,3 分钟超时自动取消。自动通过群成员列表解析负责人并指派任务
|
|
9
|
+
- **AI 对话**:发送非指令文本即可与 AI 自由对话,基于 acpx 持久化 session,按用户隔离,支持 claude 和 codex 两种 agent
|
|
10
10
|
- **历史会议查询**:通过 `/find` 指令用自然语言搜索历史会议记录
|
|
11
11
|
- **记录管理**:`/listall` 列出所有记录,`/show <id>` 查看详情,`/del <id>` 删除记录
|
|
12
12
|
- **群聊智能识别**:群聊中非 @消息自动识别会议纪要文档链接(通过文档标题判断),@机器人可使用所有指令和 AI 对话
|
|
@@ -51,10 +51,11 @@ KMR 不做流水账式的会议记录搬运。每条提取结果都围绕两个
|
|
|
51
51
|
|
|
52
52
|
- Node.js >= 18
|
|
53
53
|
- [acpx](https://www.npmjs.com/package/acpx) 已全局安装(`npm install -g acpx@latest`)
|
|
54
|
-
- Claude Code 已安装并可通过 acpx 调用
|
|
54
|
+
- Claude Code 或 Codex 已安装并可通过 acpx 调用
|
|
55
55
|
- 飞书自建应用,开启以下权限:
|
|
56
56
|
- `im:message:receive_v1`(接收消息事件)
|
|
57
57
|
- `im:message`(发送消息)
|
|
58
|
+
- `im:chat:readonly` 或 `im:chat.members:read`(获取群成员列表,用于解析任务负责人)
|
|
58
59
|
- `docs:document.content:read`(读取文档内容)
|
|
59
60
|
- `docs:document:readonly`(读取文档元信息,用于标题识别)
|
|
60
61
|
- `task:task:write`(创建飞书任务)
|
|
@@ -64,7 +65,7 @@ KMR 不做流水账式的会议记录搬运。每条提取结果都围绕两个
|
|
|
64
65
|
### 方式一:全局安装(推荐)
|
|
65
66
|
|
|
66
67
|
```bash
|
|
67
|
-
npm install -g kmr
|
|
68
|
+
npm install -g @lih-x-x/kmr
|
|
68
69
|
kmr init # 初始化配置
|
|
69
70
|
# 编辑 ~/.kmr/config.json 填入飞书凭证
|
|
70
71
|
kmr # 启动服务
|
|
@@ -90,7 +91,7 @@ npm run dev # 启动服务
|
|
|
90
91
|
"appSecret": "你的飞书应用 App Secret"
|
|
91
92
|
},
|
|
92
93
|
"agent": {
|
|
93
|
-
"provider": "claude
|
|
94
|
+
"provider": "claude",
|
|
94
95
|
"timeout": 120000
|
|
95
96
|
},
|
|
96
97
|
"storage": {
|
|
@@ -99,6 +100,8 @@ npm run dev # 启动服务
|
|
|
99
100
|
}
|
|
100
101
|
```
|
|
101
102
|
|
|
103
|
+
`provider` 支持 `claude` 和 `codex` 两种 agent,可在 Web 设置页面实时切换,无需重启服务。
|
|
104
|
+
|
|
102
105
|
配置完成后运行 `kmr` 或 `npm run dev` 启动服务。
|
|
103
106
|
|
|
104
107
|
## 使用方式
|
|
@@ -151,10 +154,10 @@ https://xxx.feishu.cn/docx/abc123
|
|
|
151
154
|
回复 /confirm 1,2 创建选中的任务
|
|
152
155
|
回复 /reject 取消创建
|
|
153
156
|
|
|
154
|
-
⏱
|
|
157
|
+
⏱ 3 分钟内未回复将自动取消
|
|
155
158
|
```
|
|
156
159
|
|
|
157
|
-
|
|
160
|
+
创建的任务会自动设置截止时间,并通过群成员列表自动解析负责人姓名为飞书 open_id 进行指派(解析失败时 fallback 到确认者)。任务信息同步回写到会议记录中。
|
|
158
161
|
|
|
159
162
|
### AI 自由对话
|
|
160
163
|
|
|
@@ -197,16 +200,18 @@ kmr/
|
|
|
197
200
|
├── src/
|
|
198
201
|
│ ├── index.ts # 服务入口
|
|
199
202
|
│ ├── config.ts # 配置管理(~/.kmr/config.json)
|
|
203
|
+
│ ├── cli.ts # CLI 入口(kmr 命令、版本更新检查)
|
|
200
204
|
│ ├── cli-init.ts # kmr init 命令
|
|
201
205
|
│ ├── lark/
|
|
202
206
|
│ │ ├── client.ts # 飞书长连接客户端(消息去重、群聊过滤)
|
|
203
207
|
│ │ ├── docReader.ts # 飞书文档内容读取(含标题获取)
|
|
204
208
|
│ │ ├── messenger.ts # 飞书消息回复(摘要、确认、任务结果等)
|
|
205
209
|
│ │ ├── router.ts # 消息路由(指令解析、自然语言匹配)
|
|
206
|
-
│ │
|
|
210
|
+
│ │ ├── taskCreator.ts # 飞书任务创建(Task API v2)
|
|
211
|
+
│ │ └── userResolver.ts # 群成员姓名→open_id 解析
|
|
207
212
|
│ ├── agent/
|
|
208
213
|
│ │ ├── types.ts # AgentProvider 接口
|
|
209
|
-
│ │ ├── claudeCode.ts # Claude
|
|
214
|
+
│ │ ├── claudeCode.ts # Claude/Codex 实现(acpx)
|
|
210
215
|
│ │ └── prompt.ts # Prompt 模板
|
|
211
216
|
│ ├── session/
|
|
212
217
|
│ │ ├── manager.ts # AI 会话管理(per-user acpx session)
|
|
@@ -221,10 +226,13 @@ kmr/
|
|
|
221
226
|
│ │ ├── index.html
|
|
222
227
|
│ │ ├── style.css
|
|
223
228
|
│ │ └── app.js
|
|
229
|
+
│ ├── utils/
|
|
230
|
+
│ │ └── claudeEnv.ts # ~/.claude/settings.json 环境变量加载
|
|
224
231
|
│ └── storage/
|
|
225
232
|
│ ├── types.ts # 数据类型定义
|
|
226
233
|
│ └── jsonStore.ts # JSON 文件存储
|
|
227
234
|
├── tests/ # 测试文件
|
|
235
|
+
├── tsup.config.ts # tsup 构建配置
|
|
228
236
|
├── package.json
|
|
229
237
|
└── tsconfig.json
|
|
230
238
|
```
|
|
@@ -266,7 +274,7 @@ npm run build
|
|
|
266
274
|
|
|
267
275
|
## 架构扩展
|
|
268
276
|
|
|
269
|
-
Agent 模块通过 `AgentProvider`
|
|
277
|
+
Agent 模块通过 `AgentProvider` 接口抽象,当前支持 Claude Code 和 Codex 两种 agent(均通过 acpx 调用)。可在 Web 设置页面实时切换,无需重启服务。后续可扩展其他 Agent(如 Kiro、Gemini 等),只需实现该接口:
|
|
270
278
|
|
|
271
279
|
```typescript
|
|
272
280
|
interface AgentProvider {
|
package/dist/index.js
CHANGED
|
@@ -13,7 +13,25 @@ function createLarkClient(config) {
|
|
|
13
13
|
appType: lark.AppType.SelfBuild
|
|
14
14
|
});
|
|
15
15
|
}
|
|
16
|
-
function
|
|
16
|
+
async function getBotOpenId(client) {
|
|
17
|
+
try {
|
|
18
|
+
const res = await client.request({
|
|
19
|
+
method: "GET",
|
|
20
|
+
url: "/open-apis/bot/v3/info"
|
|
21
|
+
});
|
|
22
|
+
const openId = res.data?.bot?.open_id || "";
|
|
23
|
+
if (openId) {
|
|
24
|
+
console.log(`[bot] \u673A\u5668\u4EBA open_id: ${openId}`);
|
|
25
|
+
} else {
|
|
26
|
+
console.warn(`[bot] \u672A\u80FD\u83B7\u53D6\u673A\u5668\u4EBA open_id\uFF0C\u7FA4\u804A @\u5224\u65AD\u53EF\u80FD\u4E0D\u51C6\u786E`);
|
|
27
|
+
}
|
|
28
|
+
return openId;
|
|
29
|
+
} catch (err) {
|
|
30
|
+
console.error(`[bot] \u83B7\u53D6\u673A\u5668\u4EBA\u4FE1\u606F\u5931\u8D25:`, err.message);
|
|
31
|
+
return "";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function createEventDispatcher(botOpenId, onMessage) {
|
|
17
35
|
const dispatcher = new lark.EventDispatcher({});
|
|
18
36
|
const processedMessages = /* @__PURE__ */ new Set();
|
|
19
37
|
dispatcher.register({
|
|
@@ -47,10 +65,11 @@ function createEventDispatcher(onMessage) {
|
|
|
47
65
|
const content = JSON.parse(message.content);
|
|
48
66
|
let text = content.text || "";
|
|
49
67
|
const chatId = message.chat_id;
|
|
50
|
-
const
|
|
68
|
+
const mentions = message.mentions || [];
|
|
69
|
+
const isMentioned = botOpenId ? mentions.some((m) => m.id?.open_id === botOpenId) : mentions.some((m) => m.id?.open_id && data.sender?.sender_type !== "user");
|
|
51
70
|
const isGroup = message.chat_type === "group";
|
|
52
|
-
if (
|
|
53
|
-
for (const mention of
|
|
71
|
+
if (mentions.length > 0) {
|
|
72
|
+
for (const mention of mentions) {
|
|
54
73
|
if (mention.key) {
|
|
55
74
|
text = text.replace(mention.key, "").trim();
|
|
56
75
|
}
|
|
@@ -58,9 +77,8 @@ function createEventDispatcher(onMessage) {
|
|
|
58
77
|
}
|
|
59
78
|
if (isGroup && !isMentioned) {
|
|
60
79
|
const hasDocLink = /(https?:\/\/[^\s]*feishu\.cn\/[^\s]+)/.test(text);
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
console.log(`[recv] \u7FA4\u804A\u975E@\u6D88\u606F\u4E14\u975E\u6587\u6863\u94FE\u63A5/\u5F85\u529E\u786E\u8BA4, \u5FFD\u7565`);
|
|
80
|
+
if (!hasDocLink) {
|
|
81
|
+
console.log(`[recv] \u7FA4\u804A\u975E@\u6D88\u606F\u4E14\u975E\u6587\u6863\u94FE\u63A5, \u5FFD\u7565`);
|
|
64
82
|
return;
|
|
65
83
|
}
|
|
66
84
|
}
|
|
@@ -295,7 +313,7 @@ var Messenger = class {
|
|
|
295
313
|
lines.push("", "\u56DE\u590D /confirm all \u521B\u5EFA\u5168\u90E8");
|
|
296
314
|
lines.push("\u56DE\u590D /confirm 1,2 \u521B\u5EFA\u9009\u4E2D\u7684\u4EFB\u52A1");
|
|
297
315
|
lines.push("\u56DE\u590D /reject \u53D6\u6D88\u521B\u5EFA");
|
|
298
|
-
lines.push("", "\u23F1
|
|
316
|
+
lines.push("", "\u23F1 3 \u5206\u949F\u5185\u672A\u56DE\u590D\u5C06\u81EA\u52A8\u53D6\u6D88");
|
|
299
317
|
await this.replyText(messageId, lines.join("\n"));
|
|
300
318
|
}
|
|
301
319
|
async replyTaskResults(messageId, results) {
|
|
@@ -1102,7 +1120,8 @@ async function main() {
|
|
|
1102
1120
|
const taskCreator = new TaskCreator(client);
|
|
1103
1121
|
const userResolver = new UserResolver(client);
|
|
1104
1122
|
const pendingConfirmations = /* @__PURE__ */ new Map();
|
|
1105
|
-
const
|
|
1123
|
+
const botOpenId = await getBotOpenId(client);
|
|
1124
|
+
const dispatcher = createEventDispatcher(botOpenId, async (messageId, text, chatId, senderId, meta) => {
|
|
1106
1125
|
const parsed = parseMessage(text);
|
|
1107
1126
|
console.log(`[route] \u6D88\u606F\u8DEF\u7531: type=${parsed.type}, messageId=${messageId}, isGroup=${meta.isGroup}, isMentioned=${meta.isMentioned}`);
|
|
1108
1127
|
if (meta.isGroup && !meta.isMentioned && parsed.type === "document_link" /* DOCUMENT_LINK */) {
|
|
@@ -1142,7 +1161,7 @@ async function main() {
|
|
|
1142
1161
|
pendingConfirmations.delete(senderId);
|
|
1143
1162
|
console.log(`[timeout] \u5F85\u529E\u786E\u8BA4\u8D85\u65F6, userId=${senderId}, meetingId=${record.id}`);
|
|
1144
1163
|
}
|
|
1145
|
-
},
|
|
1164
|
+
}, 18e4);
|
|
1146
1165
|
pendingConfirmations.set(senderId, {
|
|
1147
1166
|
meetingId: record.id,
|
|
1148
1167
|
todos: record.todos,
|