@lih-x-x/kmr 1.0.0 → 1.0.1

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 (49) hide show
  1. package/dist/chunk-X36FOW5Y.js +47 -0
  2. package/dist/cli.js +35 -35
  3. package/dist/config-KM4HTJA2.js +12 -0
  4. package/dist/index.js +1134 -207
  5. package/package.json +11 -3
  6. package/dist/agent/claudeCode.d.ts +0 -12
  7. package/dist/agent/claudeCode.js +0 -109
  8. package/dist/agent/prompt.d.ts +0 -3
  9. package/dist/agent/prompt.js +0 -33
  10. package/dist/agent/types.d.ts +0 -7
  11. package/dist/agent/types.js +0 -1
  12. package/dist/cli-init.d.ts +0 -1
  13. package/dist/cli-init.js +0 -12
  14. package/dist/cli.d.ts +0 -2
  15. package/dist/config.d.ts +0 -17
  16. package/dist/config.js +0 -38
  17. package/dist/index.d.ts +0 -1
  18. package/dist/lark/client.d.ts +0 -8
  19. package/dist/lark/client.js +0 -68
  20. package/dist/lark/docReader.d.ts +0 -9
  21. package/dist/lark/docReader.js +0 -75
  22. package/dist/lark/messenger.d.ts +0 -22
  23. package/dist/lark/messenger.js +0 -156
  24. package/dist/lark/router.d.ts +0 -20
  25. package/dist/lark/router.js +0 -75
  26. package/dist/lark/taskCreator.d.ts +0 -15
  27. package/dist/lark/taskCreator.js +0 -41
  28. package/dist/query/finder.d.ts +0 -2
  29. package/dist/query/finder.js +0 -18
  30. package/dist/query/handler.d.ts +0 -8
  31. package/dist/query/handler.js +0 -17
  32. package/dist/session/manager.d.ts +0 -12
  33. package/dist/session/manager.js +0 -114
  34. package/dist/session/skill.d.ts +0 -1
  35. package/dist/session/skill.js +0 -19
  36. package/dist/storage/jsonStore.d.ts +0 -11
  37. package/dist/storage/jsonStore.js +0 -51
  38. package/dist/storage/types.d.ts +0 -52
  39. package/dist/storage/types.js +0 -1
  40. package/dist/web/openBrowser.d.ts +0 -1
  41. package/dist/web/openBrowser.js +0 -15
  42. package/dist/web/public/public/app.js +0 -344
  43. package/dist/web/public/public/index.html +0 -28
  44. package/dist/web/public/style.css +0 -428
  45. package/dist/web/server.d.ts +0 -6
  46. package/dist/web/server.js +0 -209
  47. /package/dist/{web → public}/public/app.js +0 -0
  48. /package/dist/{web → public}/public/index.html +0 -0
  49. /package/dist/{web/public → public}/public/style.css +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lih-x-x/kmr",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -15,13 +15,20 @@
15
15
  },
16
16
  "scripts": {
17
17
  "dev": "tsx src/index.ts",
18
- "build": "tsc && cp -r src/web/public dist/web/public && node -e \"const fs=require('fs');const f='dist/cli.js';const c=fs.readFileSync(f,'utf8');if(!c.startsWith('#!')){fs.writeFileSync(f,'#!/usr/bin/env node\\n'+c)}\"",
18
+ "build": "tsup && cp -r src/web/public dist/public && node -e \"const fs=require('fs');const f='dist/cli.js';const c=fs.readFileSync(f,'utf8');if(!c.startsWith('#!')){fs.writeFileSync(f,'#!/usr/bin/env node\\n'+c)}\"",
19
19
  "prepublishOnly": "npm run build",
20
20
  "test": "vitest run",
21
21
  "test:watch": "vitest",
22
22
  "init": "tsx src/cli-init.ts"
23
23
  },
24
- "keywords": ["feishu", "lark", "meeting", "notes", "ai", "claude"],
24
+ "keywords": [
25
+ "feishu",
26
+ "lark",
27
+ "meeting",
28
+ "notes",
29
+ "ai",
30
+ "claude"
31
+ ],
25
32
  "author": "",
26
33
  "license": "ISC",
27
34
  "description": "关键会议记录服务 — 从飞书会议纪要中提取关键信息并持久化保存",
@@ -31,6 +38,7 @@
31
38
  },
32
39
  "devDependencies": {
33
40
  "@types/node": "^25.6.0",
41
+ "tsup": "^8.5.1",
34
42
  "tsx": "^4.21.0",
35
43
  "vitest": "^4.1.5"
36
44
  }
@@ -1,12 +0,0 @@
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
- }
@@ -1,109 +0,0 @@
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
- }
@@ -1,3 +0,0 @@
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";
@@ -1,33 +0,0 @@
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
- 用户查询:`;
@@ -1,7 +0,0 @@
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
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
package/dist/cli-init.js DELETED
@@ -1,12 +0,0 @@
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 DELETED
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env node
2
- export {};
package/dist/config.d.ts DELETED
@@ -1,17 +0,0 @@
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 DELETED
@@ -1,38 +0,0 @@
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
- }
package/dist/index.d.ts DELETED
@@ -1 +0,0 @@
1
- export {};
@@ -1,8 +0,0 @@
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;
@@ -1,68 +0,0 @@
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
- }
@@ -1,9 +0,0 @@
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
- }
@@ -1,75 +0,0 @@
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
- }
@@ -1,22 +0,0 @@
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
- }
@@ -1,156 +0,0 @@
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
- }