chattercatcher 0.1.6 → 0.1.8

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/dist/cli.js CHANGED
@@ -1,9 +1,224 @@
1
1
  #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+
12
+ // src/config/paths.ts
13
+ import os2 from "os";
14
+ import path2 from "path";
15
+ function getChatterCatcherHome() {
16
+ return process.env.CHATTERCATCHER_HOME || path2.join(os2.homedir(), ".chattercatcher");
17
+ }
18
+ function resolveHomePath(value) {
19
+ if (value === "~") {
20
+ return os2.homedir();
21
+ }
22
+ if (value.startsWith("~/") || value.startsWith("~\\")) {
23
+ return path2.join(os2.homedir(), value.slice(2));
24
+ }
25
+ return path2.resolve(value);
26
+ }
27
+ function getConfigPath() {
28
+ return path2.join(getChatterCatcherHome(), "config.json");
29
+ }
30
+ function getSecretsPath() {
31
+ return path2.join(getChatterCatcherHome(), "secrets.json");
32
+ }
33
+ var init_paths = __esm({
34
+ "src/config/paths.ts"() {
35
+ "use strict";
36
+ }
37
+ });
38
+
39
+ // src/rag/lancedb-store.ts
40
+ var lancedb_store_exports = {};
41
+ __export(lancedb_store_exports, {
42
+ LanceDbVectorStore: () => LanceDbVectorStore,
43
+ getLanceDbPath: () => getLanceDbPath
44
+ });
45
+ import fs6 from "fs/promises";
46
+ import path9 from "path";
47
+ function getLanceDbPath(config) {
48
+ return path9.join(resolveHomePath(config.storage.dataDir), "vector", "lancedb");
49
+ }
50
+ function toRow(record) {
51
+ return {
52
+ id: record.id,
53
+ vector: record.vector,
54
+ text: record.evidence.text,
55
+ source_json: JSON.stringify(record.evidence.source)
56
+ };
57
+ }
58
+ function toLanceData(rows) {
59
+ return rows.map((row) => ({
60
+ id: row.id,
61
+ vector: row.vector,
62
+ text: row.text,
63
+ source_json: row.source_json
64
+ }));
65
+ }
66
+ function escapeSqlString(value) {
67
+ return value.replace(/'/g, "''");
68
+ }
69
+ function toEvidence(row) {
70
+ const distance = row._distance ?? 0;
71
+ const vectorScore = 1 / (1 + Math.max(0, distance));
72
+ return {
73
+ id: row.id,
74
+ text: row.text,
75
+ score: vectorScore,
76
+ vectorScore,
77
+ source: JSON.parse(row.source_json)
78
+ };
79
+ }
80
+ var DEFAULT_TABLE_NAME, LanceDbVectorStore;
81
+ var init_lancedb_store = __esm({
82
+ "src/rag/lancedb-store.ts"() {
83
+ "use strict";
84
+ init_paths();
85
+ DEFAULT_TABLE_NAME = "message_chunks";
86
+ LanceDbVectorStore = class _LanceDbVectorStore {
87
+ constructor(connection, tableName) {
88
+ this.connection = connection;
89
+ this.tableName = tableName;
90
+ }
91
+ connection;
92
+ tableName;
93
+ static async connect(uri, tableName = DEFAULT_TABLE_NAME) {
94
+ await fs6.mkdir(uri, { recursive: true });
95
+ const lancedb = await import("@lancedb/lancedb");
96
+ const connection = await lancedb.connect(uri);
97
+ return new _LanceDbVectorStore(connection, tableName);
98
+ }
99
+ static async connectFromConfig(config, tableName = DEFAULT_TABLE_NAME) {
100
+ return _LanceDbVectorStore.connect(getLanceDbPath(config), tableName);
101
+ }
102
+ close() {
103
+ this.connection.close();
104
+ }
105
+ async upsert(records) {
106
+ if (records.length === 0) {
107
+ return;
108
+ }
109
+ const rows = records.map(toRow);
110
+ const data2 = toLanceData(rows);
111
+ const table = await this.ensureTable(data2);
112
+ const ids = rows.map((row) => `'${escapeSqlString(row.id)}'`).join(", ");
113
+ await table.delete(`id IN (${ids})`);
114
+ await table.add(data2);
115
+ }
116
+ async search(vector, limit) {
117
+ const table = await this.openTableIfExists();
118
+ if (!table) {
119
+ return [];
120
+ }
121
+ const rows = await table.vectorSearch(vector).limit(limit).toArray();
122
+ return rows.map(toEvidence);
123
+ }
124
+ async count() {
125
+ const table = await this.openTableIfExists();
126
+ if (!table) {
127
+ return 0;
128
+ }
129
+ return table.countRows();
130
+ }
131
+ async ensureTable(initialRows) {
132
+ const table = await this.openTableIfExists();
133
+ if (table) {
134
+ return table;
135
+ }
136
+ return this.connection.createTable(this.tableName, initialRows);
137
+ }
138
+ async openTableIfExists() {
139
+ const tableNames = await this.connection.tableNames();
140
+ if (!tableNames.includes(this.tableName)) {
141
+ return null;
142
+ }
143
+ return this.connection.openTable(this.tableName);
144
+ }
145
+ };
146
+ }
147
+ });
2
148
 
3
149
  // src/cli.ts
4
150
  import { input, password, select, confirm, number } from "@inquirer/prompts";
5
151
  import { Command } from "commander";
6
- import fs12 from "fs/promises";
152
+ import fs14 from "fs/promises";
153
+
154
+ // package.json
155
+ var package_default = {
156
+ name: "chattercatcher",
157
+ version: "0.1.8",
158
+ description: "\u672C\u5730\u4F18\u5148\u7684\u98DE\u4E66/Lark \u5BB6\u5EAD\u7FA4\u77E5\u8BC6\u5E93\u673A\u5668\u4EBA",
159
+ type: "module",
160
+ main: "dist/index.js",
161
+ types: "dist/index.d.ts",
162
+ homepage: "https://github.com/FlashingChen2024/chattercatcher#readme",
163
+ repository: {
164
+ type: "git",
165
+ url: "git+https://github.com/FlashingChen2024/chattercatcher.git"
166
+ },
167
+ bugs: {
168
+ url: "https://github.com/FlashingChen2024/chattercatcher/issues"
169
+ },
170
+ bin: {
171
+ chattercatcher: "dist/cli.js"
172
+ },
173
+ files: [
174
+ "assets",
175
+ "dist",
176
+ "docs",
177
+ "README.md",
178
+ "AGENTS.md"
179
+ ],
180
+ directories: {
181
+ doc: "docs"
182
+ },
183
+ scripts: {
184
+ build: "tsup",
185
+ dev: "tsx src/cli.ts",
186
+ lint: "tsc --noEmit",
187
+ typecheck: "tsc --noEmit",
188
+ test: "vitest run"
189
+ },
190
+ keywords: [
191
+ "feishu",
192
+ "lark",
193
+ "rag",
194
+ "local-first",
195
+ "knowledge-base"
196
+ ],
197
+ author: "FlashingChen2024",
198
+ license: "MIT",
199
+ dependencies: {
200
+ "@inquirer/prompts": "^8.4.2",
201
+ "@lancedb/lancedb": "0.23.0",
202
+ "@larksuiteoapi/node-sdk": "^1.62.0",
203
+ "better-sqlite3": "^12.9.0",
204
+ commander: "^14.0.3",
205
+ fastify: "^5.8.5",
206
+ mammoth: "^1.12.0",
207
+ "pdf-parse": "^2.4.5",
208
+ pino: "^10.3.1",
209
+ zod: "^4.3.6"
210
+ },
211
+ devDependencies: {
212
+ "@types/better-sqlite3": "^7.6.13",
213
+ "@types/node": "^25.6.0",
214
+ "@types/yazl": "^3.3.1",
215
+ tsup: "^8.5.1",
216
+ tsx: "^4.21.0",
217
+ typescript: "^6.0.3",
218
+ vitest: "^4.1.5",
219
+ yazl: "^3.3.1"
220
+ }
221
+ };
7
222
 
8
223
  // src/config/store.ts
9
224
  import fs from "fs/promises";
@@ -72,29 +287,8 @@ function createDefaultSecrets() {
72
287
  });
73
288
  }
74
289
 
75
- // src/config/paths.ts
76
- import os2 from "os";
77
- import path2 from "path";
78
- function getChatterCatcherHome() {
79
- return process.env.CHATTERCATCHER_HOME || path2.join(os2.homedir(), ".chattercatcher");
80
- }
81
- function resolveHomePath(value) {
82
- if (value === "~") {
83
- return os2.homedir();
84
- }
85
- if (value.startsWith("~/") || value.startsWith("~\\")) {
86
- return path2.join(os2.homedir(), value.slice(2));
87
- }
88
- return path2.resolve(value);
89
- }
90
- function getConfigPath() {
91
- return path2.join(getChatterCatcherHome(), "config.json");
92
- }
93
- function getSecretsPath() {
94
- return path2.join(getChatterCatcherHome(), "secrets.json");
95
- }
96
-
97
290
  // src/config/store.ts
291
+ init_paths();
98
292
  async function readJsonFile(filePath, fallback) {
99
293
  try {
100
294
  const raw = await fs.readFile(filePath, "utf8");
@@ -157,7 +351,11 @@ function resolveEmbeddingApiKey(input2) {
157
351
  return explicit || input2.llmApiKey;
158
352
  }
159
353
 
354
+ // src/cli.ts
355
+ init_paths();
356
+
160
357
  // src/data/deletion.ts
358
+ init_paths();
161
359
  import fs2 from "fs/promises";
162
360
  import path4 from "path";
163
361
  function emptyResult(targetType, targetId) {
@@ -283,6 +481,7 @@ async function deleteLocalData(input2) {
283
481
  }
284
482
 
285
483
  // src/db/database.ts
484
+ init_paths();
286
485
  import Database from "better-sqlite3";
287
486
  import fs3 from "fs";
288
487
  import path5 from "path";
@@ -343,13 +542,6 @@ function migrateDatabase(database) {
343
542
  tokenize = 'unicode61'
344
543
  );
345
544
 
346
- CREATE TABLE IF NOT EXISTS message_chunk_vectors (
347
- chunk_id TEXT PRIMARY KEY REFERENCES message_chunks(id) ON DELETE CASCADE,
348
- vector_json TEXT NOT NULL,
349
- evidence_json TEXT NOT NULL,
350
- updated_at TEXT NOT NULL
351
- );
352
-
353
545
  CREATE TABLE IF NOT EXISTS file_jobs (
354
546
  id TEXT PRIMARY KEY,
355
547
  source_path TEXT NOT NULL,
@@ -369,7 +561,8 @@ function migrateDatabase(database) {
369
561
  }
370
562
 
371
563
  // src/doctor/checks.ts
372
- import fs5 from "fs/promises";
564
+ init_paths();
565
+ import fs7 from "fs/promises";
373
566
 
374
567
  // src/files/jobs.ts
375
568
  import crypto from "crypto";
@@ -511,10 +704,120 @@ var FileJobRepository = class {
511
704
  };
512
705
 
513
706
  // src/gateway/runtime.ts
514
- import fs4 from "fs";
707
+ init_paths();
708
+ import fs5 from "fs";
709
+ import path8 from "path";
710
+
711
+ // src/logs/reader.ts
712
+ init_paths();
713
+ import fs4 from "fs/promises";
714
+ import { watch } from "fs";
515
715
  import path7 from "path";
716
+ function getLogsDirectory() {
717
+ return path7.join(getChatterCatcherHome(), "logs");
718
+ }
719
+ function resolveLogPath(fileName, logsDir = getLogsDirectory()) {
720
+ return path7.isAbsolute(fileName) ? fileName : path7.join(logsDir, fileName);
721
+ }
722
+ function normalizeLineCount(value, fallback = 200) {
723
+ const parsed = Number(value ?? fallback);
724
+ return Number.isFinite(parsed) ? Math.min(Math.max(Math.trunc(parsed), 1), 1e4) : fallback;
725
+ }
726
+ async function listLogFiles(logsDir = getLogsDirectory()) {
727
+ let entries;
728
+ try {
729
+ entries = await fs4.readdir(logsDir, { withFileTypes: true });
730
+ } catch (error) {
731
+ if (error.code === "ENOENT") {
732
+ return [];
733
+ }
734
+ throw error;
735
+ }
736
+ const files2 = await Promise.all(
737
+ entries.filter((entry) => entry.isFile() && entry.name.endsWith(".log")).map(async (entry) => {
738
+ const filePath = path7.join(logsDir, entry.name);
739
+ const stats = await fs4.stat(filePath);
740
+ return {
741
+ name: entry.name,
742
+ path: filePath,
743
+ updatedAt: stats.mtime,
744
+ bytes: stats.size
745
+ };
746
+ })
747
+ );
748
+ return files2.sort((left, right) => right.updatedAt.getTime() - left.updatedAt.getTime());
749
+ }
750
+ function tailLines(content, lines) {
751
+ const normalized = content.replace(/\r\n/g, "\n");
752
+ const parts = normalized.endsWith("\n") ? normalized.slice(0, -1).split("\n") : normalized.split("\n");
753
+ return parts.slice(-lines).join("\n");
754
+ }
755
+ async function readLogTail(input2) {
756
+ const stats = await fs4.stat(input2.filePath);
757
+ const content = await fs4.readFile(input2.filePath, "utf8");
758
+ return {
759
+ file: {
760
+ name: path7.basename(input2.filePath),
761
+ path: input2.filePath,
762
+ updatedAt: stats.mtime,
763
+ bytes: stats.size
764
+ },
765
+ content: tailLines(content, normalizeLineCount(input2.lines))
766
+ };
767
+ }
768
+ async function readLatestLogTail(input2 = {}) {
769
+ if (input2.fileName) {
770
+ return readLogTail({
771
+ filePath: resolveLogPath(input2.fileName, input2.logsDir),
772
+ lines: input2.lines
773
+ });
774
+ }
775
+ const [latest] = await listLogFiles(input2.logsDir);
776
+ if (!latest) {
777
+ return null;
778
+ }
779
+ return readLogTail({ filePath: latest.path, lines: input2.lines });
780
+ }
781
+ async function followLogFile(input2) {
782
+ let offset = (await fs4.stat(input2.filePath)).size;
783
+ const directory = path7.dirname(input2.filePath);
784
+ const fileName = path7.basename(input2.filePath);
785
+ async function readAppended() {
786
+ const stats = await fs4.stat(input2.filePath);
787
+ if (stats.size < offset) {
788
+ offset = 0;
789
+ }
790
+ if (stats.size === offset) {
791
+ return;
792
+ }
793
+ const handle = await fs4.open(input2.filePath, "r");
794
+ try {
795
+ const length = stats.size - offset;
796
+ const buffer = Buffer.alloc(length);
797
+ await handle.read(buffer, 0, length, offset);
798
+ offset = stats.size;
799
+ input2.onChunk(buffer.toString("utf8"));
800
+ } finally {
801
+ await handle.close();
802
+ }
803
+ }
804
+ const watcher = watch(directory, (eventType, changedFileName) => {
805
+ if (eventType !== "change" || changedFileName?.toString() !== fileName) {
806
+ return;
807
+ }
808
+ void readAppended().catch((error) => {
809
+ input2.onError?.(error instanceof Error ? error : new Error(String(error)));
810
+ });
811
+ });
812
+ return () => watcher.close();
813
+ }
814
+
815
+ // src/gateway/runtime.ts
516
816
  function getGatewayPidPath() {
517
- return path7.join(getChatterCatcherHome(), "gateway.pid");
817
+ return path8.join(getChatterCatcherHome(), "gateway.pid");
818
+ }
819
+ function getGatewayLogPath() {
820
+ return path8.join(getLogsDirectory(), "gateway.log");
518
821
  }
519
822
  function isProcessRunning(pid) {
520
823
  if (!Number.isInteger(pid) || pid <= 0) {
@@ -529,7 +832,7 @@ function isProcessRunning(pid) {
529
832
  }
530
833
  function readGatewayPidRecord(pidFile = getGatewayPidPath()) {
531
834
  try {
532
- const raw = fs4.readFileSync(pidFile, "utf8");
835
+ const raw = fs5.readFileSync(pidFile, "utf8");
533
836
  const parsed = JSON.parse(raw);
534
837
  if (!Number.isInteger(parsed.pid) || typeof parsed.startedAt !== "string" || typeof parsed.command !== "string") {
535
838
  return null;
@@ -541,7 +844,9 @@ function readGatewayPidRecord(pidFile = getGatewayPidPath()) {
541
844
  return {
542
845
  pid,
543
846
  startedAt: parsed.startedAt,
544
- command: parsed.command
847
+ command: parsed.command,
848
+ ...typeof parsed.logFile === "string" ? { logFile: parsed.logFile } : {},
849
+ ...parsed.mode === "gateway" || parsed.mode === "web" ? { mode: parsed.mode } : {}
545
850
  };
546
851
  } catch (error) {
547
852
  if (error.code === "ENOENT") {
@@ -555,13 +860,13 @@ function writeGatewayPidRecord(pidFile = getGatewayPidPath(), record = {
555
860
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
556
861
  command: process.argv.join(" ")
557
862
  }) {
558
- fs4.mkdirSync(path7.dirname(pidFile), { recursive: true });
559
- fs4.writeFileSync(pidFile, `${JSON.stringify(record, null, 2)}
863
+ fs5.mkdirSync(path8.dirname(pidFile), { recursive: true });
864
+ fs5.writeFileSync(pidFile, `${JSON.stringify(record, null, 2)}
560
865
  `, "utf8");
561
866
  }
562
867
  function removeGatewayPidRecord(pidFile = getGatewayPidPath()) {
563
868
  try {
564
- fs4.rmSync(pidFile, { force: true });
869
+ fs5.rmSync(pidFile, { force: true });
565
870
  } catch {
566
871
  }
567
872
  }
@@ -613,6 +918,28 @@ function stopGatewayProcess(pidFile = getGatewayPidPath()) {
613
918
 
614
919
  // src/gateway/index.ts
615
920
  function getGatewayStatus(config, secrets) {
921
+ const runtime = getGatewayRuntimeState();
922
+ const configured = Boolean(config.feishu.appId && (!secrets || secrets.feishu.appSecret));
923
+ if (runtime.running && runtime.record) {
924
+ if (runtime.record.mode === "web" && !configured) {
925
+ return {
926
+ configured,
927
+ connection: "running",
928
+ message: `\u672C\u5730 Web UI \u8FDB\u7A0B\u6B63\u5728\u8FD0\u884C\uFF1Apid=${runtime.record.pid}\uFF0CstartedAt=${runtime.record.startedAt}\uFF1B\u98DE\u4E66\u914D\u7F6E\u5C1A\u672A\u5B8C\u6210\u3002`,
929
+ pid: runtime.record.pid,
930
+ pidFile: runtime.pidFile,
931
+ logFile: runtime.record.logFile
932
+ };
933
+ }
934
+ return {
935
+ configured: true,
936
+ connection: "running",
937
+ message: `\u98DE\u4E66 Gateway \u6B63\u5728\u8FD0\u884C\uFF1Apid=${runtime.record.pid}\uFF0CstartedAt=${runtime.record.startedAt}`,
938
+ pid: runtime.record.pid,
939
+ pidFile: runtime.pidFile,
940
+ logFile: runtime.record.logFile
941
+ };
942
+ }
616
943
  if (!config.feishu.appId) {
617
944
  return {
618
945
  configured: false,
@@ -627,23 +954,14 @@ function getGatewayStatus(config, secrets) {
627
954
  message: "\u5C1A\u672A\u914D\u7F6E\u98DE\u4E66 App Secret\u3002\u8BF7\u8FD0\u884C chattercatcher setup \u6216 chattercatcher settings\u3002"
628
955
  };
629
956
  }
630
- const runtime = getGatewayRuntimeState();
631
- if (runtime.running && runtime.record) {
632
- return {
633
- configured: true,
634
- connection: "running",
635
- message: `\u98DE\u4E66 Gateway \u6B63\u5728\u8FD0\u884C\uFF1Apid=${runtime.record.pid}\uFF0CstartedAt=${runtime.record.startedAt}`,
636
- pid: runtime.record.pid,
637
- pidFile: runtime.pidFile
638
- };
639
- }
640
957
  if (runtime.stale && runtime.record) {
641
958
  return {
642
959
  configured: true,
643
960
  connection: "ready_for_start",
644
961
  message: `\u98DE\u4E66\u957F\u8FDE\u63A5\u914D\u7F6E\u5DF2\u5C31\u7EEA\uFF1B\u53D1\u73B0\u8FC7\u671F PID \u6587\u4EF6\uFF1Apid=${runtime.record.pid}\u3002\u8FD0\u884C chattercatcher gateway start \u4F1A\u8986\u76D6\u8FD0\u884C\u8BB0\u5F55\u3002`,
645
962
  pid: runtime.record.pid,
646
- pidFile: runtime.pidFile
963
+ pidFile: runtime.pidFile,
964
+ logFile: runtime.record.logFile
647
965
  };
648
966
  }
649
967
  return {
@@ -1140,82 +1458,6 @@ var MessageFtsRetriever = class {
1140
1458
  }
1141
1459
  };
1142
1460
 
1143
- // src/rag/embedding.ts
1144
- function cosineSimilarity(left, right) {
1145
- if (left.length === 0 || right.length === 0 || left.length !== right.length) {
1146
- return 0;
1147
- }
1148
- let dot = 0;
1149
- let leftNorm = 0;
1150
- let rightNorm = 0;
1151
- for (let index2 = 0; index2 < left.length; index2 += 1) {
1152
- const leftValue = left[index2] ?? 0;
1153
- const rightValue = right[index2] ?? 0;
1154
- dot += leftValue * rightValue;
1155
- leftNorm += leftValue * leftValue;
1156
- rightNorm += rightValue * rightValue;
1157
- }
1158
- if (leftNorm === 0 || rightNorm === 0) {
1159
- return 0;
1160
- }
1161
- return dot / (Math.sqrt(leftNorm) * Math.sqrt(rightNorm));
1162
- }
1163
-
1164
- // src/rag/sqlite-vector-store.ts
1165
- var SQLiteVectorStore = class {
1166
- constructor(database) {
1167
- this.database = database;
1168
- }
1169
- database;
1170
- async upsert(records) {
1171
- if (records.length === 0) {
1172
- return;
1173
- }
1174
- const statement = this.database.prepare(`
1175
- INSERT INTO message_chunk_vectors (chunk_id, vector_json, evidence_json, updated_at)
1176
- VALUES (@chunkId, @vectorJson, @evidenceJson, @updatedAt)
1177
- ON CONFLICT(chunk_id) DO UPDATE SET
1178
- vector_json = excluded.vector_json,
1179
- evidence_json = excluded.evidence_json,
1180
- updated_at = excluded.updated_at
1181
- `);
1182
- const upsertMany = this.database.transaction((items) => {
1183
- const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1184
- for (const item of items) {
1185
- statement.run({
1186
- chunkId: item.id,
1187
- vectorJson: JSON.stringify(item.vector),
1188
- evidenceJson: JSON.stringify(item.evidence),
1189
- updatedAt
1190
- });
1191
- }
1192
- });
1193
- upsertMany(records);
1194
- }
1195
- async search(vector, limit) {
1196
- if (limit <= 0) {
1197
- return [];
1198
- }
1199
- const rows = this.database.prepare("SELECT chunk_id, vector_json, evidence_json FROM message_chunk_vectors").all();
1200
- return rows.map((row) => {
1201
- const storedVector = JSON.parse(row.vector_json);
1202
- const evidence = JSON.parse(row.evidence_json);
1203
- const vectorScore = cosineSimilarity(vector, storedVector);
1204
- return {
1205
- ...evidence,
1206
- score: vectorScore,
1207
- vectorScore
1208
- };
1209
- }).sort((left, right) => right.vectorScore - left.vectorScore).slice(0, limit);
1210
- }
1211
- async count() {
1212
- const row = this.database.prepare("SELECT COUNT(*) AS count FROM message_chunk_vectors").get();
1213
- return row.count;
1214
- }
1215
- close() {
1216
- }
1217
- };
1218
-
1219
1461
  // src/rag/vector-retriever.ts
1220
1462
  var VectorRetriever = class {
1221
1463
  constructor(embedding, store, limit = 8) {
@@ -1240,7 +1482,8 @@ async function createHybridRetriever(input2) {
1240
1482
  const retrievers = [new MessageFtsRetriever(input2.messages, { excludeMessageIds: input2.excludeMessageIds })];
1241
1483
  const closers = [];
1242
1484
  if (hasEmbeddingConfig(input2.config, input2.secrets)) {
1243
- const vectorStore = new SQLiteVectorStore(input2.messages.database);
1485
+ const { LanceDbVectorStore: LanceDbVectorStore2 } = await Promise.resolve().then(() => (init_lancedb_store(), lancedb_store_exports));
1486
+ const vectorStore = await LanceDbVectorStore2.connectFromConfig(input2.config);
1244
1487
  retrievers.push(new VectorRetriever(createEmbeddingModel(input2.config, input2.secrets), vectorStore));
1245
1488
  closers.push(() => vectorStore.close());
1246
1489
  }
@@ -1272,7 +1515,7 @@ async function runDoctor(config, secrets, options = {}) {
1272
1515
  checks.push(checkEmbeddingConfig(config, secrets));
1273
1516
  checks.push(await checkSqlite(config));
1274
1517
  checks.push(await checkFilePipeline(config));
1275
- checks.push(await checkSqliteVector(config));
1518
+ checks.push(await checkLanceDb(config));
1276
1519
  checks.push(checkRagPolicy());
1277
1520
  if (options.online) {
1278
1521
  checks.push(await checkChatModel(config, secrets));
@@ -1283,8 +1526,8 @@ async function runDoctor(config, secrets, options = {}) {
1283
1526
  async function checkHomeDirectory() {
1284
1527
  const home = getChatterCatcherHome();
1285
1528
  try {
1286
- await fs5.mkdir(home, { recursive: true });
1287
- await fs5.access(home);
1529
+ await fs7.mkdir(home, { recursive: true });
1530
+ await fs7.access(home);
1288
1531
  return pass("\u914D\u7F6E\u76EE\u5F55", home);
1289
1532
  } catch (error) {
1290
1533
  return fail("\u914D\u7F6E\u76EE\u5F55", error instanceof Error ? error.message : String(error));
@@ -1305,7 +1548,7 @@ function checkLlmConfig(config, secrets) {
1305
1548
  }
1306
1549
  function checkEmbeddingConfig(config, secrets) {
1307
1550
  if (!hasEmbeddingConfig(config, secrets)) {
1308
- return warn("Embedding \u914D\u7F6E", "\u672A\u914D\u7F6E\u5B8C\u6574\uFF1BRAG \u4F1A\u4F7F\u7528 SQLite FTS\uFF0C\u65E0\u6CD5\u4F7F\u7528 SQLite Vector \u8BED\u4E49\u68C0\u7D22\u3002");
1551
+ return warn("Embedding \u914D\u7F6E", "\u672A\u914D\u7F6E\u5B8C\u6574\uFF1BRAG \u4F1A\u4F7F\u7528 SQLite FTS\uFF0C\u65E0\u6CD5\u4F7F\u7528 LanceDB \u8BED\u4E49\u68C0\u7D22\u3002");
1309
1552
  }
1310
1553
  return pass("Embedding \u914D\u7F6E", `${config.embedding.model} @ ${config.embedding.baseUrl || config.llm.baseUrl}`);
1311
1554
  }
@@ -1339,17 +1582,17 @@ async function checkFilePipeline(config) {
1339
1582
  database?.close();
1340
1583
  }
1341
1584
  }
1342
- async function checkSqliteVector(config) {
1343
- let database = null;
1585
+ async function checkLanceDb(config) {
1586
+ let store = null;
1344
1587
  try {
1345
- database = openDatabase(config);
1346
- const store = new SQLiteVectorStore(database);
1588
+ const { getLanceDbPath: getLanceDbPath2, LanceDbVectorStore: LanceDbVectorStore2 } = await Promise.resolve().then(() => (init_lancedb_store(), lancedb_store_exports));
1589
+ store = await LanceDbVectorStore2.connectFromConfig(config);
1347
1590
  const count = await store.count();
1348
- return pass("SQLite Vector", `${getDatabasePath(config)}\uFF1Bvectors=${count}`);
1591
+ return pass("LanceDB", `${getLanceDbPath2(config)}\uFF1Bvectors=${count}`);
1349
1592
  } catch (error) {
1350
- return fail("SQLite Vector", error instanceof Error ? error.message : String(error));
1593
+ return fail("LanceDB", error instanceof Error ? error.message : String(error));
1351
1594
  } finally {
1352
- database?.close();
1595
+ store?.close();
1353
1596
  }
1354
1597
  }
1355
1598
  function checkRagPolicy() {
@@ -1390,8 +1633,9 @@ function formatDoctorChecks(checks) {
1390
1633
  }
1391
1634
 
1392
1635
  // src/export/data-export.ts
1393
- import fs6 from "fs/promises";
1394
- import path8 from "path";
1636
+ init_paths();
1637
+ import fs8 from "fs/promises";
1638
+ import path10 from "path";
1395
1639
  function parseJsonObject(value) {
1396
1640
  try {
1397
1641
  const parsed = JSON.parse(value);
@@ -1410,11 +1654,11 @@ function parseJsonArray(value) {
1410
1654
  }
1411
1655
  function defaultExportPath(config, exportedAt) {
1412
1656
  const fileName = `chattercatcher-export-${exportedAt.replace(/[:.]/g, "-")}.json`;
1413
- return path8.join(resolveHomePath(config.storage.dataDir), "exports", fileName);
1657
+ return path10.join(resolveHomePath(config.storage.dataDir), "exports", fileName);
1414
1658
  }
1415
1659
  async function exportLocalData(input2) {
1416
1660
  const exportedAt = input2.exportedAt ?? (/* @__PURE__ */ new Date()).toISOString();
1417
- const outputPath = path8.resolve(input2.outputPath ?? defaultExportPath(input2.config, exportedAt));
1661
+ const outputPath = path10.resolve(input2.outputPath ?? defaultExportPath(input2.config, exportedAt));
1418
1662
  const chats = input2.database.prepare(
1419
1663
  `
1420
1664
  SELECT
@@ -1501,8 +1745,8 @@ async function exportLocalData(input2) {
1501
1745
  fileJobs
1502
1746
  }
1503
1747
  };
1504
- await fs6.mkdir(path8.dirname(outputPath), { recursive: true });
1505
- await fs6.writeFile(outputPath, `${JSON.stringify(payload, null, 2)}
1748
+ await fs8.mkdir(path10.dirname(outputPath), { recursive: true });
1749
+ await fs8.writeFile(outputPath, `${JSON.stringify(payload, null, 2)}
1506
1750
  `, "utf8");
1507
1751
  return {
1508
1752
  outputPath,
@@ -1514,8 +1758,8 @@ async function exportLocalData(input2) {
1514
1758
  }
1515
1759
 
1516
1760
  // src/export/data-restore.ts
1517
- import fs7 from "fs/promises";
1518
- import path9 from "path";
1761
+ import fs9 from "fs/promises";
1762
+ import path11 from "path";
1519
1763
  function asObject(value) {
1520
1764
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
1521
1765
  }
@@ -1563,8 +1807,8 @@ function clearDatabase(database) {
1563
1807
  database.prepare("DELETE FROM chats").run();
1564
1808
  }
1565
1809
  async function restoreLocalData(input2) {
1566
- const inputPath = path9.resolve(input2.inputPath);
1567
- const payload = parsePayload(await fs7.readFile(inputPath, "utf8"));
1810
+ const inputPath = path11.resolve(input2.inputPath);
1811
+ const payload = parsePayload(await fs9.readFile(inputPath, "utf8"));
1568
1812
  const mode = input2.replace ? "replace" : "merge";
1569
1813
  const restore = input2.database.transaction(() => {
1570
1814
  if (input2.replace) {
@@ -2156,9 +2400,10 @@ function createFeishuGateway(options) {
2156
2400
  }
2157
2401
 
2158
2402
  // src/feishu/resource-downloader.ts
2403
+ init_paths();
2159
2404
  import * as lark3 from "@larksuiteoapi/node-sdk";
2160
- import fs8 from "fs/promises";
2161
- import path10 from "path";
2405
+ import fs10 from "fs/promises";
2406
+ import path12 from "path";
2162
2407
  var RESOURCE_TYPE_BY_KIND = {
2163
2408
  file: "file",
2164
2409
  image: "image",
@@ -2196,10 +2441,10 @@ var FeishuResourceDownloader = class _FeishuResourceDownloader {
2196
2441
  }
2197
2442
  async download(input2) {
2198
2443
  const resourceType = RESOURCE_TYPE_BY_KIND[input2.attachment.kind];
2199
- const targetDir = path10.join(this.dataDir, "files", "feishu");
2200
- await fs8.mkdir(targetDir, { recursive: true });
2444
+ const targetDir = path12.join(this.dataDir, "files", "feishu");
2445
+ await fs10.mkdir(targetDir, { recursive: true });
2201
2446
  const fileName = buildStoredFileName(input2);
2202
- const storedPath = path10.join(targetDir, fileName);
2447
+ const storedPath = path12.join(targetDir, fileName);
2203
2448
  const payload = {
2204
2449
  params: { type: resourceType },
2205
2450
  path: { message_id: input2.messageId, file_key: input2.attachment.fileKey }
@@ -2221,30 +2466,31 @@ var FeishuResourceDownloader = class _FeishuResourceDownloader {
2221
2466
  };
2222
2467
 
2223
2468
  // src/files/ingest.ts
2469
+ init_paths();
2224
2470
  import crypto3 from "crypto";
2225
- import fs10 from "fs/promises";
2226
- import path12 from "path";
2471
+ import fs12 from "fs/promises";
2472
+ import path14 from "path";
2227
2473
 
2228
2474
  // src/files/parser.ts
2229
- import fs9 from "fs/promises";
2230
- import path11 from "path";
2475
+ import fs11 from "fs/promises";
2476
+ import path13 from "path";
2231
2477
  import mammoth from "mammoth";
2232
2478
  import { PDFParse } from "pdf-parse";
2233
2479
  var TEXT_EXTENSIONS = /* @__PURE__ */ new Set([".txt", ".md", ".markdown", ".json", ".csv", ".tsv", ".log"]);
2234
2480
  var DOCX_EXTENSIONS = /* @__PURE__ */ new Set([".docx"]);
2235
2481
  var PDF_EXTENSIONS = /* @__PURE__ */ new Set([".pdf"]);
2236
2482
  function isSupportedParseFile(filePath) {
2237
- const extension = path11.extname(filePath).toLowerCase();
2483
+ const extension = path13.extname(filePath).toLowerCase();
2238
2484
  return TEXT_EXTENSIONS.has(extension) || DOCX_EXTENSIONS.has(extension) || PDF_EXTENSIONS.has(extension);
2239
2485
  }
2240
2486
  function describeSupportedParseTypes() {
2241
2487
  return "txt\u3001md\u3001json\u3001csv\u3001tsv\u3001log\u3001docx\u3001pdf";
2242
2488
  }
2243
2489
  async function parseFileToText(filePath) {
2244
- const extension = path11.extname(filePath).toLowerCase();
2490
+ const extension = path13.extname(filePath).toLowerCase();
2245
2491
  if (TEXT_EXTENSIONS.has(extension)) {
2246
2492
  return {
2247
- text: await fs9.readFile(filePath, "utf8"),
2493
+ text: await fs11.readFile(filePath, "utf8"),
2248
2494
  parser: "text",
2249
2495
  warnings: []
2250
2496
  };
@@ -2258,7 +2504,7 @@ async function parseFileToText(filePath) {
2258
2504
  };
2259
2505
  }
2260
2506
  if (PDF_EXTENSIONS.has(extension)) {
2261
- const buffer = await fs9.readFile(filePath);
2507
+ const buffer = await fs11.readFile(filePath);
2262
2508
  const parser = new PDFParse({ data: buffer });
2263
2509
  try {
2264
2510
  const result = await parser.getText();
@@ -2280,7 +2526,7 @@ function isSupportedTextFile(filePath) {
2280
2526
  }
2281
2527
  function ensureSupportedTextFile(filePath) {
2282
2528
  if (!isSupportedTextFile(filePath)) {
2283
- const extension = path12.extname(filePath).toLowerCase();
2529
+ const extension = path14.extname(filePath).toLowerCase();
2284
2530
  throw new Error(`\u6682\u4E0D\u652F\u6301\u8BE5\u6587\u4EF6\u7C7B\u578B\uFF1A${extension || "\u65E0\u6269\u5C55\u540D"}\u3002\u5F53\u524D\u652F\u6301 ${describeSupportedParseTypes()}\u3002`);
2285
2531
  }
2286
2532
  }
@@ -2289,12 +2535,12 @@ function stableStoredName(sourcePath, fileName) {
2289
2535
  return `${digest}-${fileName}`;
2290
2536
  }
2291
2537
  async function ingestLocalFile(input2) {
2292
- const sourcePath = path12.resolve(input2.filePath);
2293
- const fileName = path12.basename(sourcePath);
2538
+ const sourcePath = path14.resolve(input2.filePath);
2539
+ const fileName = path14.basename(sourcePath);
2294
2540
  const jobId = input2.jobs?.start({ sourcePath, fileName });
2295
2541
  try {
2296
2542
  ensureSupportedTextFile(sourcePath);
2297
- const stat = await fs10.stat(sourcePath);
2543
+ const stat = await fs12.stat(sourcePath);
2298
2544
  if (!stat.isFile()) {
2299
2545
  throw new Error(`\u4E0D\u662F\u6587\u4EF6\uFF1A${sourcePath}`);
2300
2546
  }
@@ -2303,10 +2549,10 @@ async function ingestLocalFile(input2) {
2303
2549
  if (!text) {
2304
2550
  throw new Error(`\u6587\u4EF6\u6CA1\u6709\u53EF\u7D22\u5F15\u6587\u672C\uFF1A${sourcePath}`);
2305
2551
  }
2306
- const fileDir = path12.join(resolveHomePath(input2.config.storage.dataDir), "files");
2307
- await fs10.mkdir(fileDir, { recursive: true });
2308
- const storedPath = path12.join(fileDir, stableStoredName(sourcePath, fileName));
2309
- await fs10.copyFile(sourcePath, storedPath);
2552
+ const fileDir = path14.join(resolveHomePath(input2.config.storage.dataDir), "files");
2553
+ await fs12.mkdir(fileDir, { recursive: true });
2554
+ const storedPath = path14.join(fileDir, stableStoredName(sourcePath, fileName));
2555
+ await fs12.copyFile(sourcePath, storedPath);
2310
2556
  const messageId = input2.messages.ingest({
2311
2557
  platform: "local-file",
2312
2558
  platformChatId: "local-files",
@@ -2587,107 +2833,72 @@ var GatewayIngestor = class {
2587
2833
  }
2588
2834
  };
2589
2835
 
2590
- // src/logs/reader.ts
2591
- import fs11 from "fs/promises";
2592
- import { watch } from "fs";
2593
- import path13 from "path";
2594
- function getLogsDirectory() {
2595
- return path13.join(getChatterCatcherHome(), "logs");
2596
- }
2597
- function resolveLogPath(fileName, logsDir = getLogsDirectory()) {
2598
- return path13.isAbsolute(fileName) ? fileName : path13.join(logsDir, fileName);
2599
- }
2600
- function normalizeLineCount(value, fallback = 200) {
2601
- const parsed = Number(value ?? fallback);
2602
- return Number.isFinite(parsed) ? Math.min(Math.max(Math.trunc(parsed), 1), 1e4) : fallback;
2603
- }
2604
- async function listLogFiles(logsDir = getLogsDirectory()) {
2605
- let entries;
2606
- try {
2607
- entries = await fs11.readdir(logsDir, { withFileTypes: true });
2608
- } catch (error) {
2609
- if (error.code === "ENOENT") {
2610
- return [];
2611
- }
2612
- throw error;
2836
+ // src/gateway/detached.ts
2837
+ import { spawn } from "child_process";
2838
+ import fs13 from "fs";
2839
+ import path15 from "path";
2840
+ function buildGatewayForegroundSpawnCommand(argv = process.argv) {
2841
+ const [command = process.execPath, ...rawArgs] = argv;
2842
+ const args = [...rawArgs];
2843
+ while (args.at(-1) === "--foreground") {
2844
+ args.pop();
2845
+ }
2846
+ if (args.at(-1) === "start" && args.at(-2) === "gateway") {
2847
+ args.splice(-2, 2);
2613
2848
  }
2614
- const files2 = await Promise.all(
2615
- entries.filter((entry) => entry.isFile() && entry.name.endsWith(".log")).map(async (entry) => {
2616
- const filePath = path13.join(logsDir, entry.name);
2617
- const stats = await fs11.stat(filePath);
2618
- return {
2619
- name: entry.name,
2620
- path: filePath,
2621
- updatedAt: stats.mtime,
2622
- bytes: stats.size
2623
- };
2624
- })
2625
- );
2626
- return files2.sort((left, right) => right.updatedAt.getTime() - left.updatedAt.getTime());
2627
- }
2628
- function tailLines(content, lines) {
2629
- const normalized = content.replace(/\r\n/g, "\n");
2630
- const parts = normalized.endsWith("\n") ? normalized.slice(0, -1).split("\n") : normalized.split("\n");
2631
- return parts.slice(-lines).join("\n");
2632
- }
2633
- async function readLogTail(input2) {
2634
- const stats = await fs11.stat(input2.filePath);
2635
- const content = await fs11.readFile(input2.filePath, "utf8");
2636
2849
  return {
2637
- file: {
2638
- name: path13.basename(input2.filePath),
2639
- path: input2.filePath,
2640
- updatedAt: stats.mtime,
2641
- bytes: stats.size
2642
- },
2643
- content: tailLines(content, normalizeLineCount(input2.lines))
2850
+ command,
2851
+ args: [...args, "gateway", "start", "--foreground"]
2644
2852
  };
2645
2853
  }
2646
- async function readLatestLogTail(input2 = {}) {
2647
- if (input2.fileName) {
2648
- return readLogTail({
2649
- filePath: resolveLogPath(input2.fileName, input2.logsDir),
2650
- lines: input2.lines
2651
- });
2652
- }
2653
- const [latest] = await listLogFiles(input2.logsDir);
2654
- if (!latest) {
2655
- return null;
2854
+ function startDetachedGateway(input2) {
2855
+ const status = getGatewayStatus(input2.config, input2.secrets);
2856
+ const logFile = getGatewayLogPath();
2857
+ if (status.connection === "running") {
2858
+ return {
2859
+ started: false,
2860
+ message: `\u98DE\u4E66 Gateway \u5DF2\u7ECF\u6B63\u5728\u8FD0\u884C\uFF1Apid=${status.pid ?? "unknown"}`,
2861
+ logFile,
2862
+ ...status.pid ? { pid: status.pid } : {}
2863
+ };
2656
2864
  }
2657
- return readLogTail({ filePath: latest.path, lines: input2.lines });
2658
- }
2659
- async function followLogFile(input2) {
2660
- let offset = (await fs11.stat(input2.filePath)).size;
2661
- const directory = path13.dirname(input2.filePath);
2662
- const fileName = path13.basename(input2.filePath);
2663
- async function readAppended() {
2664
- const stats = await fs11.stat(input2.filePath);
2665
- if (stats.size < offset) {
2666
- offset = 0;
2667
- }
2668
- if (stats.size === offset) {
2865
+ fs13.mkdirSync(path15.dirname(logFile), { recursive: true });
2866
+ let out;
2867
+ let err;
2868
+ let stdioClosed = false;
2869
+ const closeStdio = () => {
2870
+ if (stdioClosed) {
2669
2871
  return;
2670
2872
  }
2671
- const handle = await fs11.open(input2.filePath, "r");
2672
- try {
2673
- const length = stats.size - offset;
2674
- const buffer = Buffer.alloc(length);
2675
- await handle.read(buffer, 0, length, offset);
2676
- offset = stats.size;
2677
- input2.onChunk(buffer.toString("utf8"));
2678
- } finally {
2679
- await handle.close();
2873
+ stdioClosed = true;
2874
+ if (typeof out === "number") {
2875
+ fs13.closeSync(out);
2680
2876
  }
2681
- }
2682
- const watcher = watch(directory, (eventType, changedFileName) => {
2683
- if (eventType !== "change" || changedFileName?.toString() !== fileName) {
2684
- return;
2877
+ if (typeof err === "number") {
2878
+ fs13.closeSync(err);
2685
2879
  }
2686
- void readAppended().catch((error) => {
2687
- input2.onError?.(error instanceof Error ? error : new Error(String(error)));
2880
+ };
2881
+ try {
2882
+ out = fs13.openSync(logFile, "a");
2883
+ err = fs13.openSync(logFile, "a");
2884
+ const foreground = buildGatewayForegroundSpawnCommand(input2.argv);
2885
+ const child = spawn(foreground.command, foreground.args, {
2886
+ detached: true,
2887
+ stdio: ["ignore", out, err],
2888
+ windowsHide: true
2688
2889
  });
2689
- });
2690
- return () => watcher.close();
2890
+ closeStdio();
2891
+ child.unref();
2892
+ return {
2893
+ started: true,
2894
+ message: `\u5DF2\u5728\u540E\u53F0\u542F\u52A8\u98DE\u4E66 Gateway\uFF1Apid=${child.pid}`,
2895
+ pid: child.pid,
2896
+ logFile
2897
+ };
2898
+ } catch (error) {
2899
+ closeStdio();
2900
+ throw error;
2901
+ }
2691
2902
  }
2692
2903
 
2693
2904
  // src/rag/indexer.ts
@@ -2749,7 +2960,8 @@ async function processMessagesNow(input2) {
2749
2960
  finishedAt: (/* @__PURE__ */ new Date()).toISOString()
2750
2961
  };
2751
2962
  }
2752
- const vectorStore = new SQLiteVectorStore(input2.database);
2963
+ const { LanceDbVectorStore: LanceDbVectorStore2 } = await Promise.resolve().then(() => (init_lancedb_store(), lancedb_store_exports));
2964
+ const vectorStore = await LanceDbVectorStore2.connectFromConfig(input2.config);
2753
2965
  try {
2754
2966
  const stats = await indexMessageChunks({
2755
2967
  messages: new MessageRepository(input2.database),
@@ -2769,6 +2981,108 @@ async function processMessagesNow(input2) {
2769
2981
  }
2770
2982
  }
2771
2983
 
2984
+ // src/update/npm-updater.ts
2985
+ import { execFile } from "child_process";
2986
+ import { promisify } from "util";
2987
+ var execFileAsync = promisify(execFile);
2988
+ var packageName = "chattercatcher";
2989
+ var installArgs = ["install", "-g", `${packageName}@latest`];
2990
+ var latestVersionArgs = ["view", packageName, "version", "--json"];
2991
+ function formatCommand(command, args) {
2992
+ return [command, ...args].join(" ");
2993
+ }
2994
+ function getErrorMessage(error) {
2995
+ if (error instanceof Error) {
2996
+ return error.message;
2997
+ }
2998
+ return String(error);
2999
+ }
3000
+ function parseLatestVersion(stdout) {
3001
+ const trimmed = stdout.trim();
3002
+ if (!trimmed) {
3003
+ throw new Error("npm registry returned an empty version");
3004
+ }
3005
+ let parsed;
3006
+ try {
3007
+ parsed = JSON.parse(trimmed);
3008
+ } catch {
3009
+ throw new Error("npm registry returned an invalid version");
3010
+ }
3011
+ if (typeof parsed !== "string" || !parsed) {
3012
+ throw new Error("npm registry returned an invalid version");
3013
+ }
3014
+ return parsed;
3015
+ }
3016
+ function compareVersions(left, right) {
3017
+ const leftParts = left.split(".").map((part) => Number(part));
3018
+ const rightParts = right.split(".").map((part) => Number(part));
3019
+ const length = Math.max(leftParts.length, rightParts.length);
3020
+ for (let index2 = 0; index2 < length; index2 += 1) {
3021
+ const leftPart = leftParts[index2] ?? 0;
3022
+ const rightPart = rightParts[index2] ?? 0;
3023
+ if (leftPart > rightPart) {
3024
+ return 1;
3025
+ }
3026
+ if (leftPart < rightPart) {
3027
+ return -1;
3028
+ }
3029
+ }
3030
+ return 0;
3031
+ }
3032
+ async function defaultUpdateCommandRunner(command, args) {
3033
+ const { stdout, stderr } = await execFileAsync(command, args);
3034
+ return { stdout, stderr };
3035
+ }
3036
+ async function updateChatterCatcher(options) {
3037
+ const runner = options.runner ?? defaultUpdateCommandRunner;
3038
+ const command = formatCommand("npm", installArgs);
3039
+ let latestVersion;
3040
+ try {
3041
+ const output = await runner("npm", latestVersionArgs);
3042
+ latestVersion = parseLatestVersion(output.stdout);
3043
+ } catch (error) {
3044
+ return {
3045
+ status: "query-failed",
3046
+ currentVersion: options.currentVersion,
3047
+ command,
3048
+ error: getErrorMessage(error)
3049
+ };
3050
+ }
3051
+ if (compareVersions(latestVersion, options.currentVersion) <= 0) {
3052
+ return {
3053
+ status: "up-to-date",
3054
+ currentVersion: options.currentVersion,
3055
+ latestVersion,
3056
+ command
3057
+ };
3058
+ }
3059
+ if (options.dryRun) {
3060
+ return {
3061
+ status: "dry-run",
3062
+ currentVersion: options.currentVersion,
3063
+ latestVersion,
3064
+ command
3065
+ };
3066
+ }
3067
+ try {
3068
+ await runner("npm", installArgs);
3069
+ } catch (error) {
3070
+ return {
3071
+ status: "install-failed",
3072
+ currentVersion: options.currentVersion,
3073
+ latestVersion,
3074
+ command,
3075
+ error: getErrorMessage(error)
3076
+ };
3077
+ }
3078
+ return {
3079
+ status: "updated",
3080
+ currentVersion: options.currentVersion,
3081
+ latestVersion,
3082
+ command
3083
+ };
3084
+ }
3085
+
2772
3086
  // src/web/server.ts
2773
3087
  import Fastify from "fastify";
2774
3088
  function buildHtml() {
@@ -3166,7 +3480,7 @@ function createWebApp(config) {
3166
3480
  note: "\u95EE\u7B54\u5FC5\u987B\u5148\u68C0\u7D22\u8BC1\u636E\uFF0C\u7981\u6B62\u5168\u91CF\u4E0A\u4E0B\u6587\u5806\u53E0\u3002",
3167
3481
  retrieval: {
3168
3482
  keyword: "SQLite FTS5",
3169
- vector: "SQLite Vector",
3483
+ vector: "LanceDB",
3170
3484
  hybrid: true
3171
3485
  }
3172
3486
  },
@@ -3286,7 +3600,7 @@ function printSettings(config, secrets) {
3286
3600
  2
3287
3601
  ));
3288
3602
  }
3289
- program.name("chattercatcher").description("\u672C\u5730\u4F18\u5148\u7684\u98DE\u4E66/Lark \u5BB6\u5EAD\u7FA4\u77E5\u8BC6\u673A\u5668\u4EBA").version("0.1.6");
3603
+ program.name("chattercatcher").description("\u672C\u5730\u4F18\u5148\u7684\u98DE\u4E66/Lark \u5BB6\u5EAD\u7FA4\u77E5\u8BC6\u673A\u5668\u4EBA").version(package_default.version);
3290
3604
  program.command("setup").description("\u4EA4\u4E92\u5F0F\u521D\u59CB\u5316\u914D\u7F6E").action(async () => {
3291
3605
  const { config, secrets } = await ensureConfigFiles();
3292
3606
  await promptForConfiguration(config, secrets);
@@ -3322,19 +3636,60 @@ program.command("doctor").description("\u68C0\u67E5\u672C\u5730\u914D\u7F6E\u300
3322
3636
  const checks = await runDoctor(config, secrets, { online: options.online });
3323
3637
  console.log(formatDoctorChecks(checks));
3324
3638
  });
3639
+ program.command("update").description("\u5347\u7EA7 ChatterCatcher \u5230 npm \u6700\u65B0\u7248\u672C").option("--dry-run", "\u53EA\u68C0\u67E5\u5E76\u663E\u793A\u5C06\u6267\u884C\u7684\u5347\u7EA7\u547D\u4EE4").action(async (options) => {
3640
+ const result = await updateChatterCatcher({ currentVersion: package_default.version, dryRun: options.dryRun });
3641
+ if (result.status === "up-to-date") {
3642
+ console.log(`ChatterCatcher \u5DF2\u662F\u6700\u65B0\u7248\u672C\uFF1A${result.currentVersion}`);
3643
+ return;
3644
+ }
3645
+ if (result.status === "dry-run") {
3646
+ console.log(`\u5F53\u524D\u7248\u672C\uFF1A${result.currentVersion}`);
3647
+ console.log(`\u6700\u65B0\u7248\u672C\uFF1A${result.latestVersion}`);
3648
+ console.log(`\u5C06\u6267\u884C\uFF1A${result.command}`);
3649
+ return;
3650
+ }
3651
+ if (result.status === "updated") {
3652
+ console.log(`\u5347\u7EA7\u5B8C\u6210\uFF1A${result.currentVersion} -> ${result.latestVersion}`);
3653
+ console.log("\u8BF7\u91CD\u65B0\u6253\u5F00\u7EC8\u7AEF\u6216\u91CD\u65B0\u8FD0\u884C chattercatcher --version \u786E\u8BA4\u7248\u672C\u3002");
3654
+ return;
3655
+ }
3656
+ if (result.status === "query-failed") {
3657
+ console.error(`\u65E0\u6CD5\u83B7\u53D6\u6700\u65B0\u7248\u672C\uFF1A${result.error}`);
3658
+ process.exitCode = 1;
3659
+ return;
3660
+ }
3661
+ console.error(`\u5347\u7EA7\u5931\u8D25\uFF1A${result.error}`);
3662
+ console.error(`\u53EF\u624B\u52A8\u8FD0\u884C\uFF1A${result.command}`);
3663
+ process.exitCode = 1;
3664
+ });
3325
3665
  var gateway = program.command("gateway").description("\u7BA1\u7406\u672C\u5730\u98DE\u4E66 Gateway");
3326
- async function startGatewayCommand() {
3666
+ async function startGatewayForegroundCommand() {
3327
3667
  const config = await loadConfig();
3328
3668
  const secrets = await loadSecrets();
3329
3669
  const status = getGatewayStatus(config, secrets);
3670
+ const pidRecordBase = {
3671
+ pid: process.pid,
3672
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
3673
+ command: process.argv.join(" "),
3674
+ logFile: getGatewayLogPath()
3675
+ };
3330
3676
  if (!status.configured) {
3677
+ writeGatewayPidRecord(void 0, {
3678
+ ...pidRecordBase,
3679
+ mode: "web"
3680
+ });
3331
3681
  console.log(status.message);
3332
3682
  console.log("\u672C\u5730 Web UI \u4ECD\u4F1A\u542F\u52A8\uFF0C\u65B9\u4FBF\u7EE7\u7EED\u914D\u7F6E\u3002");
3333
3683
  await startWebServer(config);
3334
3684
  return;
3335
3685
  }
3686
+ writeGatewayPidRecord(void 0, {
3687
+ ...pidRecordBase,
3688
+ mode: "gateway"
3689
+ });
3336
3690
  const database = openDatabase(config);
3337
- const vectorStore = hasEmbeddingConfig(config, secrets) ? new SQLiteVectorStore(database) : null;
3691
+ const { LanceDbVectorStore: LanceDbVectorStore2 } = hasEmbeddingConfig(config, secrets) ? await Promise.resolve().then(() => (init_lancedb_store(), lancedb_store_exports)) : { LanceDbVectorStore: null };
3692
+ const vectorStore = LanceDbVectorStore2 ? await LanceDbVectorStore2.connectFromConfig(config) : null;
3338
3693
  const gatewayRuntime = createFeishuGateway({
3339
3694
  config,
3340
3695
  secrets,
@@ -3369,7 +3724,6 @@ async function startGatewayCommand() {
3369
3724
  process.exit(0);
3370
3725
  });
3371
3726
  console.log(status.message);
3372
- writeGatewayPidRecord();
3373
3727
  try {
3374
3728
  await gatewayRuntime.start();
3375
3729
  await startWebServer(config);
@@ -3378,12 +3732,28 @@ async function startGatewayCommand() {
3378
3732
  throw error;
3379
3733
  }
3380
3734
  }
3735
+ async function startGatewayCommand(options = {}) {
3736
+ if (options.foreground) {
3737
+ await startGatewayForegroundCommand();
3738
+ return;
3739
+ }
3740
+ const config = await loadConfig();
3741
+ const secrets = await loadSecrets();
3742
+ const result = startDetachedGateway({ config, secrets });
3743
+ console.log(result.message);
3744
+ if (result.pid) {
3745
+ console.log(`PID\uFF1A${result.pid}`);
3746
+ }
3747
+ console.log(`\u65E5\u5FD7\u6587\u4EF6\uFF1A${result.logFile}`);
3748
+ console.log("\u67E5\u770B\u65E5\u5FD7\uFF1Achattercatcher logs --follow --file gateway.log");
3749
+ console.log("\u505C\u6B62 Gateway\uFF1Achattercatcher gateway stop");
3750
+ }
3381
3751
  gateway.command("status").description("\u67E5\u770B Gateway \u72B6\u6001").action(async () => {
3382
3752
  const config = await loadConfig();
3383
3753
  const secrets = await loadSecrets();
3384
3754
  console.log(JSON.stringify(getGatewayStatus(config, secrets), null, 2));
3385
3755
  });
3386
- gateway.command("start").description("\u542F\u52A8\u98DE\u4E66\u957F\u8FDE\u63A5 Gateway \u548C\u672C\u5730 Web UI").action(startGatewayCommand);
3756
+ gateway.command("start").description("\u542F\u52A8\u98DE\u4E66\u957F\u8FDE\u63A5 Gateway \u548C\u672C\u5730 Web UI").option("--foreground", "\u5728\u5F53\u524D\u7EC8\u7AEF\u4EE5\u524D\u53F0\u6A21\u5F0F\u8FD0\u884C").action(startGatewayCommand);
3387
3757
  gateway.command("stop").description("\u505C\u6B62 Gateway").action(() => {
3388
3758
  console.log(stopGatewayProcess().message);
3389
3759
  });
@@ -3419,7 +3789,7 @@ async function deleteDataCommand(targetType, targetId, options) {
3419
3789
  if (result.skippedStoredFiles.length > 0) {
3420
3790
  console.log(`\u8DF3\u8FC7\u975E\u6570\u636E\u76EE\u5F55\u6587\u4EF6\uFF1A${result.skippedStoredFiles.join("\uFF1B")}`);
3421
3791
  }
3422
- console.log("SQLite FTS \u5DF2\u540C\u6B65\u5220\u9664\uFF1B\u5982\u4F7F\u7528\u8BED\u4E49\u68C0\u7D22\uFF0C\u8BF7\u8FD0\u884C chattercatcher index rebuild \u91CD\u5EFA SQLite Vector\u3002");
3792
+ console.log("SQLite FTS \u5DF2\u540C\u6B65\u5220\u9664\uFF1B\u5982\u4F7F\u7528 LanceDB \u8BED\u4E49\u68C0\u7D22\uFF0C\u8BF7\u8FD0\u884C chattercatcher index rebuild\u3002");
3423
3793
  } finally {
3424
3794
  database.close();
3425
3795
  }
@@ -3434,19 +3804,20 @@ index.command("status").description("\u67E5\u770B\u7D22\u5F15\u72B6\u6001").acti
3434
3804
  const secrets = await loadSecrets();
3435
3805
  const database = openDatabase(config);
3436
3806
  const messages = new MessageRepository(database);
3437
- const vectorStore = new SQLiteVectorStore(database);
3807
+ const { getLanceDbPath: getLanceDbPath2, LanceDbVectorStore: LanceDbVectorStore2 } = await Promise.resolve().then(() => (init_lancedb_store(), lancedb_store_exports));
3808
+ const vectorStore = await LanceDbVectorStore2.connectFromConfig(config);
3438
3809
  const vectors = await vectorStore.count();
3439
3810
  console.log(JSON.stringify(
3440
3811
  {
3441
3812
  database: getDatabasePath(config),
3442
- vectorDatabase: getDatabasePath(config),
3813
+ vectorDatabase: getLanceDbPath2(config),
3443
3814
  chats: messages.getChatCount(),
3444
3815
  messages: messages.getMessageCount(),
3445
3816
  vectors,
3446
3817
  retrieval: {
3447
3818
  keyword: "SQLite FTS5",
3448
- vector: hasEmbeddingConfig(config, secrets) ? "SQLite Vector \u5DF2\u53EF\u7528\u4E8E\u8BED\u4E49\u68C0\u7D22" : "SQLite Vector \u5DF2\u63A5\u5165\uFF1B\u9700\u914D\u7F6E embedding \u540E\u542F\u7528\u8BED\u4E49\u68C0\u7D22",
3449
- hybrid: "\u542F\u7528\uFF1ASQLite FTS + SQLite Vector",
3819
+ vector: hasEmbeddingConfig(config, secrets) ? "LanceDB \u5DF2\u53EF\u7528\u4E8E\u8BED\u4E49\u68C0\u7D22" : "LanceDB \u5DF2\u63A5\u5165\uFF1B\u9700\u914D\u7F6E embedding \u540E\u542F\u7528\u8BED\u4E49\u68C0\u7D22",
3820
+ hybrid: "\u542F\u7528\uFF1ASQLite FTS + LanceDB Vector",
3450
3821
  rag: "\u5F3A\u5236\u5148\u68C0\u7D22\u8BC1\u636E\u518D\u56DE\u7B54\uFF0C\u7981\u6B62\u5168\u91CF\u4E0A\u4E0B\u6587\u5806\u53E0"
3451
3822
  }
3452
3823
  },
@@ -3456,7 +3827,7 @@ index.command("status").description("\u67E5\u770B\u7D22\u5F15\u72B6\u6001").acti
3456
3827
  vectorStore.close();
3457
3828
  database.close();
3458
3829
  });
3459
- index.command("rebuild").description("\u91CD\u5EFA SQLite \u5411\u91CF\u7D22\u5F15").option("--limit <number>", "\u6700\u591A\u7D22\u5F15\u7684 chunk \u6570", "10000").action(async (options) => {
3830
+ index.command("rebuild").description("\u91CD\u5EFA LanceDB \u5411\u91CF\u7D22\u5F15").option("--limit <number>", "\u6700\u591A\u7D22\u5F15\u7684 chunk \u6570", "10000").action(async (options) => {
3460
3831
  const config = await loadConfig();
3461
3832
  const secrets = await loadSecrets();
3462
3833
  if (!hasEmbeddingConfig(config, secrets)) {
@@ -3464,7 +3835,8 @@ index.command("rebuild").description("\u91CD\u5EFA SQLite \u5411\u91CF\u7D22\u5F
3464
3835
  return;
3465
3836
  }
3466
3837
  const database = openDatabase(config);
3467
- const vectorStore = new SQLiteVectorStore(database);
3838
+ const { LanceDbVectorStore: LanceDbVectorStore2 } = await Promise.resolve().then(() => (init_lancedb_store(), lancedb_store_exports));
3839
+ const vectorStore = await LanceDbVectorStore2.connectFromConfig(config);
3468
3840
  try {
3469
3841
  const stats = await indexMessageChunks({
3470
3842
  messages: new MessageRepository(database),
@@ -3479,7 +3851,7 @@ index.command("rebuild").description("\u91CD\u5EFA SQLite \u5411\u91CF\u7D22\u5F
3479
3851
  }
3480
3852
  });
3481
3853
  var processCommand = program.command("process").description("\u7ACB\u5373\u5904\u7406\u540E\u53F0\u4EFB\u52A1");
3482
- processCommand.command("messages").description("\u7ACB\u5373\u5904\u7406\u6D88\u606F\u7D22\u5F15\u4EFB\u52A1\uFF0C\u628A\u6D88\u606F chunks \u5199\u5165 SQLite \u5411\u91CF\u7D22\u5F15").option("--limit <number>", "\u6700\u591A\u5904\u7406\u7684 chunk \u6570", "10000").action(async (options) => {
3854
+ processCommand.command("messages").description("\u7ACB\u5373\u5904\u7406\u6D88\u606F\u7D22\u5F15\u4EFB\u52A1\uFF0C\u628A\u6D88\u606F chunks \u5199\u5165 LanceDB \u5411\u91CF\u7D22\u5F15").option("--limit <number>", "\u6700\u591A\u5904\u7406\u7684 chunk \u6570", "10000").action(async (options) => {
3483
3855
  const config = await loadConfig();
3484
3856
  const secrets = await loadSecrets();
3485
3857
  const database = openDatabase(config);
@@ -3513,7 +3885,7 @@ files.command("add").description("\u628A\u672C\u5730\u6587\u4EF6\u89E3\u6790\u30
3513
3885
  `\u5DF2\u5BFC\u5165\u6587\u4EF6\uFF1A${result.fileName}\uFF0C\u89E3\u6790\u5668=${result.parser}\uFF0C\u5B57\u7B26\u6570=${result.characters}\uFF0C\u6D88\u606FID=${result.messageId}`
3514
3886
  );
3515
3887
  }
3516
- console.log("\u6587\u4EF6\u5DF2\u8FDB\u5165 SQLite FTS \u68C0\u7D22\uFF1B\u5982\u5DF2\u914D\u7F6E embedding\uFF0C\u53EF\u8FD0\u884C chattercatcher index rebuild \u66F4\u65B0 SQLite \u5411\u91CF\u7D22\u5F15\u3002");
3888
+ console.log("\u6587\u4EF6\u5DF2\u8FDB\u5165 SQLite FTS \u68C0\u7D22\uFF1B\u5982\u5DF2\u914D\u7F6E embedding\uFF0C\u53EF\u8FD0\u884C chattercatcher index rebuild \u66F4\u65B0 LanceDB \u5411\u91CF\u7D22\u5F15\u3002");
3517
3889
  } finally {
3518
3890
  database.close();
3519
3891
  }
@@ -3642,7 +4014,7 @@ program.command("restore").description("\u4ECE ChatterCatcher \u5BFC\u51FA\u6587
3642
4014
  console.log(`\u6062\u590D\u5B8C\u6210\uFF1A${result.inputPath}`);
3643
4015
  console.log(`\u6A21\u5F0F\uFF1A${result.mode === "replace" ? "\u66FF\u6362" : "\u5408\u5E76"}`);
3644
4016
  console.log(`\u5305\u542B\uFF1A\u7FA4\u804A=${result.chats}\uFF0C\u6D88\u606F=${result.messages}\uFF0Cchunks=${result.chunks}\uFF0C\u6587\u4EF6\u4EFB\u52A1=${result.fileJobs}`);
3645
- console.log("SQLite FTS \u5DF2\u91CD\u5EFA\uFF1B\u5982\u4F7F\u7528\u8BED\u4E49\u68C0\u7D22\uFF0C\u8BF7\u8FD0\u884C chattercatcher index rebuild \u91CD\u5EFA SQLite Vector\u3002");
4017
+ console.log("SQLite FTS \u5DF2\u91CD\u5EFA\uFF1B\u5982\u4F7F\u7528 LanceDB \u8BED\u4E49\u68C0\u7D22\uFF0C\u8BF7\u8FD0\u884C chattercatcher index rebuild\u3002");
3646
4018
  } finally {
3647
4019
  database.close();
3648
4020
  }
@@ -3672,7 +4044,7 @@ dev.command("ingest-feishu-event").description("\u4ECE JSON \u6587\u4EF6\u6A21\u
3672
4044
  const config = await loadConfig();
3673
4045
  const database = openDatabase(config);
3674
4046
  try {
3675
- const raw = await fs12.readFile(options.file, "utf8");
4047
+ const raw = await fs14.readFile(options.file, "utf8");
3676
4048
  const payload = JSON.parse(raw);
3677
4049
  const result = new GatewayIngestor(database).ingestFeishuEvent(payload);
3678
4050
  if (!result.accepted) {