dsh-qq-digest 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/store-file.js CHANGED
@@ -7,7 +7,7 @@
7
7
  * 损坏行启动时跳过并计数,不因为一行坏掉就丢整份历史。
8
8
  */
9
9
 
10
- import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
10
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs';
11
11
  import { join } from 'node:path';
12
12
 
13
13
  import { RECORD_FIELDS, fail } from './core/store.js';
@@ -23,26 +23,87 @@ export function createFileStore({ dir, now = () => new Date().toISOString() } =
23
23
  const entries = [];
24
24
  let skippedLines = 0;
25
25
  let allowed = new Set();
26
+ let stamp = null;
26
27
 
27
- if (existsSync(file)) {
28
- const text = readFileSync(file, 'utf8');
29
- for (const line of text.split('\n')) {
30
- const trimmed = line.trim();
31
- if (trimmed === '') continue;
32
- try {
33
- const parsed = JSON.parse(trimmed);
34
- if (parsed && typeof parsed.roomHandle === 'string') entries.push(parsed);
35
- else skippedLines += 1;
36
- } catch {
37
- skippedLines += 1;
28
+ function currentStamp() {
29
+ try {
30
+ const stat = statSync(file);
31
+ return `${stat.mtimeMs}:${stat.size}`;
32
+ } catch {
33
+ return 'missing';
34
+ }
35
+ }
36
+
37
+ /** 载入时按落盘白名单重建:文件可能被别的进程改过,原样塞进内存等于绕过白名单。 */
38
+ function rebuildRecord(raw) {
39
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
40
+ if (typeof raw.roomHandle !== 'string' || !/^[0-9a-f]{16}$/.test(raw.roomHandle)) return null;
41
+
42
+ const p = raw.provenance;
43
+ if (!p || typeof p.seqFrom !== 'number' || typeof p.seqTo !== 'number') return null;
44
+
45
+ const draft = {
46
+ roomHandle: raw.roomHandle,
47
+ roomTitle: typeof raw.roomTitle === 'string' ? raw.roomTitle.slice(0, 60) : null,
48
+ digest: sanitizeDigest(raw.digest),
49
+ provenance: {
50
+ roomHandle: raw.roomHandle,
51
+ seqFrom: p.seqFrom,
52
+ seqTo: p.seqTo,
53
+ ...(typeof p.messageCount === 'number' ? { messageCount: p.messageCount } : {}),
54
+ },
55
+ trigger: raw.trigger === 'scheduled' ? 'scheduled' : 'manual',
56
+ createdAt: typeof raw.createdAt === 'string' ? raw.createdAt : now(),
57
+ };
58
+
59
+ return Object.freeze(
60
+ Object.fromEntries(RECORD_FIELDS.filter((key) => key in draft).map((key) => [key, draft[key]])),
61
+ );
62
+ }
63
+
64
+ function loadFromDisk() {
65
+ entries.length = 0;
66
+ skippedLines = 0;
67
+ try {
68
+ const text = readFileSync(file, 'utf8');
69
+ for (const line of text.split('\n')) {
70
+ const trimmed = line.trim();
71
+ if (trimmed === '') continue;
72
+ try {
73
+ const rebuilt = rebuildRecord(JSON.parse(trimmed));
74
+ if (rebuilt) entries.push(rebuilt);
75
+ else skippedLines += 1;
76
+ } catch {
77
+ skippedLines += 1;
78
+ }
38
79
  }
80
+ } catch {
81
+ // 文件不存在 = 空表
39
82
  }
83
+ stamp = currentStamp();
84
+ }
85
+
86
+ /**
87
+ * 跨进程可见性:MCP 子进程写入的摘要,宿主进程(设置页)也必须看得到。
88
+ * 用 mtime+size 判断有没有变化,变了就整体重载 —— 比 tail 增量简单,
89
+ * 也不会因为读到半行而把一条合法记录判成损坏。
90
+ */
91
+ function refresh() {
92
+ if (currentStamp() !== stamp) loadFromDisk();
40
93
  }
41
94
 
95
+ loadFromDisk();
96
+
42
97
  return {
43
98
  dir,
44
99
  file,
45
- skippedLines,
100
+
101
+ get skippedLines() {
102
+ refresh();
103
+ return skippedLines;
104
+ },
105
+
106
+ refresh,
46
107
 
47
108
  bindAllowed(handles) {
48
109
  allowed = new Set(Array.isArray(handles) ? handles : []);
@@ -78,14 +139,18 @@ export function createFileStore({ dir, now = () => new Date().toISOString() } =
78
139
 
79
140
  appendFileSync(file, `${JSON.stringify(stored)}\n`, 'utf8');
80
141
  entries.push(stored);
142
+ // 自己刚写过:记下新的 stamp,避免下一次读取又整体重载一遍。
143
+ stamp = currentStamp();
81
144
  return stored;
82
145
  },
83
146
 
84
147
  all() {
148
+ refresh();
85
149
  return entries.slice();
86
150
  },
87
151
 
88
152
  latest(roomHandle) {
153
+ refresh();
89
154
  for (let index = entries.length - 1; index >= 0; index -= 1) {
90
155
  if (entries[index].roomHandle === roomHandle) return entries[index];
91
156
  }
@@ -93,12 +158,14 @@ export function createFileStore({ dir, now = () => new Date().toISOString() } =
93
158
  },
94
159
 
95
160
  latestPerRoom() {
161
+ refresh();
96
162
  const map = new Map();
97
163
  for (const entry of entries) map.set(entry.roomHandle, entry);
98
164
  return [...map.values()];
99
165
  },
100
166
 
101
167
  dump() {
168
+ refresh();
102
169
  return JSON.stringify(entries);
103
170
  },
104
171
  };