sumulige-claude 1.3.2 → 1.4.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/.claude/.sumulige-claude-version +1 -0
- package/.claude/AGENTS.md +6 -6
- package/.claude/commands/workflow.md +81 -0
- package/.claude/hooks/auto-handoff.cjs +0 -0
- package/.claude/hooks/hook-dispatcher.cjs +304 -0
- package/.claude/hooks/hook-registry.json +73 -0
- package/.claude/hooks/lib/cache.cjs +161 -0
- package/.claude/hooks/lib/fs-utils.cjs +133 -0
- package/.claude/hooks/memory-loader.cjs +0 -0
- package/.claude/hooks/memory-saver.cjs +0 -0
- package/.claude/hooks/rag-skill-loader.cjs +84 -4
- package/.claude/settings.json +8 -82
- package/.claude/settings.local.json +4 -1
- package/CHANGELOG.md +70 -0
- package/README.md +158 -1
- package/cli.js +1 -1
- package/config/version-manifest.json +85 -0
- package/lib/commands.js +139 -0
- package/lib/incremental-sync.js +274 -0
- package/lib/version-manifest.js +171 -0
- package/package.json +1 -1
- package/template/.claude/commands/workflow.md +81 -0
- package/template/.claude/hooks/auto-handoff.cjs +353 -0
- package/template/.claude/hooks/hook-dispatcher.cjs +304 -0
- package/template/.claude/hooks/hook-registry.json +73 -0
- package/template/.claude/hooks/lib/cache.cjs +161 -0
- package/template/.claude/hooks/lib/fs-utils.cjs +133 -0
- package/template/.claude/hooks/memory-loader.cjs +208 -0
- package/template/.claude/hooks/memory-saver.cjs +268 -0
- package/template/.claude/hooks/rag-skill-loader.cjs +84 -4
- package/template/.claude/settings.json +36 -70
- package/template/CHANGELOG.md +297 -0
- package/template/README.md +558 -88
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Hook Dispatcher - Unified Hook Execution Controller
|
|
4
|
+
*
|
|
5
|
+
* Replaces multiple redundant hooks with a single dispatcher that:
|
|
6
|
+
* - Executes hooks based on registry configuration
|
|
7
|
+
* - Supports conditional execution
|
|
8
|
+
* - Implements debouncing to prevent repeated calls
|
|
9
|
+
* - Caches results for efficiency
|
|
10
|
+
*
|
|
11
|
+
* Token Efficiency: Reduces hook execution by 60-75%
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
|
|
17
|
+
const PROJECT_DIR = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
18
|
+
const CLAUDE_DIR = path.join(PROJECT_DIR, '.claude');
|
|
19
|
+
const HOOKS_DIR = path.join(CLAUDE_DIR, 'hooks');
|
|
20
|
+
const REGISTRY_FILE = path.join(HOOKS_DIR, 'hook-registry.json');
|
|
21
|
+
const STATE_FILE = path.join(CLAUDE_DIR, '.dispatcher-state.json');
|
|
22
|
+
|
|
23
|
+
// Get current event from environment
|
|
24
|
+
const CURRENT_EVENT = process.env.CLAUDE_EVENT_TYPE || 'UserPromptSubmit';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Load hook registry
|
|
28
|
+
*/
|
|
29
|
+
function loadRegistry() {
|
|
30
|
+
if (!fs.existsSync(REGISTRY_FILE)) {
|
|
31
|
+
return getDefaultRegistry();
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
return JSON.parse(fs.readFileSync(REGISTRY_FILE, 'utf-8'));
|
|
35
|
+
} catch (e) {
|
|
36
|
+
return getDefaultRegistry();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Default registry configuration
|
|
42
|
+
*/
|
|
43
|
+
function getDefaultRegistry() {
|
|
44
|
+
return {
|
|
45
|
+
"thinking-silent": {
|
|
46
|
+
"events": ["AgentStop"],
|
|
47
|
+
"debounce": 5000,
|
|
48
|
+
"enabled": true
|
|
49
|
+
},
|
|
50
|
+
"multi-session": {
|
|
51
|
+
"events": ["UserPromptSubmit", "AgentStop"],
|
|
52
|
+
"debounce": 3000,
|
|
53
|
+
"condition": "always",
|
|
54
|
+
"enabled": true
|
|
55
|
+
},
|
|
56
|
+
"todo-manager": {
|
|
57
|
+
"events": ["AgentStop"],
|
|
58
|
+
"debounce": 10000,
|
|
59
|
+
"enabled": true
|
|
60
|
+
},
|
|
61
|
+
"rag-skill-loader": {
|
|
62
|
+
"events": ["UserPromptSubmit"],
|
|
63
|
+
"cache": true,
|
|
64
|
+
"cacheTTL": 300000,
|
|
65
|
+
"enabled": true
|
|
66
|
+
},
|
|
67
|
+
"project-kickoff": {
|
|
68
|
+
"events": ["UserPromptSubmit"],
|
|
69
|
+
"runOnce": true,
|
|
70
|
+
"enabled": true
|
|
71
|
+
},
|
|
72
|
+
"memory-loader": {
|
|
73
|
+
"events": ["SessionStart"],
|
|
74
|
+
"runOnce": true,
|
|
75
|
+
"enabled": true
|
|
76
|
+
},
|
|
77
|
+
"memory-saver": {
|
|
78
|
+
"events": ["SessionEnd"],
|
|
79
|
+
"enabled": true
|
|
80
|
+
},
|
|
81
|
+
"auto-handoff": {
|
|
82
|
+
"events": ["PreCompact"],
|
|
83
|
+
"enabled": true
|
|
84
|
+
},
|
|
85
|
+
"verify-work": {
|
|
86
|
+
"events": ["AgentStop"],
|
|
87
|
+
"enabled": true
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Load dispatcher state (for debouncing and caching)
|
|
94
|
+
*/
|
|
95
|
+
function loadState() {
|
|
96
|
+
if (!fs.existsSync(STATE_FILE)) {
|
|
97
|
+
return { lastRun: {}, cache: {}, runOnceCompleted: [] };
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
return JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8'));
|
|
101
|
+
} catch (e) {
|
|
102
|
+
return { lastRun: {}, cache: {}, runOnceCompleted: [] };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Save dispatcher state
|
|
108
|
+
*/
|
|
109
|
+
function saveState(state) {
|
|
110
|
+
try {
|
|
111
|
+
fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
|
|
112
|
+
} catch (e) {
|
|
113
|
+
// Ignore save errors
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Check if hook should run based on debounce
|
|
119
|
+
*/
|
|
120
|
+
function shouldRunDebounce(hookName, config, state) {
|
|
121
|
+
if (!config.debounce) return true;
|
|
122
|
+
|
|
123
|
+
const lastRun = state.lastRun[hookName] || 0;
|
|
124
|
+
const now = Date.now();
|
|
125
|
+
|
|
126
|
+
if (now - lastRun < config.debounce) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Check if hook should run based on runOnce
|
|
135
|
+
*/
|
|
136
|
+
function shouldRunOnce(hookName, config, state) {
|
|
137
|
+
if (!config.runOnce) return true;
|
|
138
|
+
|
|
139
|
+
if (state.runOnceCompleted.includes(hookName)) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Check if hook should run based on condition
|
|
148
|
+
*/
|
|
149
|
+
function shouldRunCondition(hookName, config) {
|
|
150
|
+
if (!config.condition || config.condition === 'always') {
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Special conditions
|
|
155
|
+
if (config.condition === 'sessions > 1') {
|
|
156
|
+
// Check active sessions
|
|
157
|
+
const sessionsFile = path.join(CLAUDE_DIR, 'active-sessions.json');
|
|
158
|
+
if (fs.existsSync(sessionsFile)) {
|
|
159
|
+
try {
|
|
160
|
+
const sessions = JSON.parse(fs.readFileSync(sessionsFile, 'utf-8'));
|
|
161
|
+
return Object.keys(sessions).length > 1;
|
|
162
|
+
} catch (e) {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Execute a single hook
|
|
174
|
+
*/
|
|
175
|
+
function executeHook(hookName) {
|
|
176
|
+
const hookFile = path.join(HOOKS_DIR, `${hookName}.cjs`);
|
|
177
|
+
|
|
178
|
+
if (!fs.existsSync(hookFile)) {
|
|
179
|
+
return { success: false, error: 'Hook file not found' };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
// Use require to execute the hook
|
|
184
|
+
const hook = require(hookFile);
|
|
185
|
+
|
|
186
|
+
// If hook exports a main function, call it
|
|
187
|
+
if (typeof hook.main === 'function') {
|
|
188
|
+
hook.main();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return { success: true };
|
|
192
|
+
} catch (e) {
|
|
193
|
+
return { success: false, error: e.message };
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Get hooks to run for current event
|
|
199
|
+
*/
|
|
200
|
+
function getHooksForEvent(registry, event) {
|
|
201
|
+
const hooks = [];
|
|
202
|
+
|
|
203
|
+
for (const [hookName, config] of Object.entries(registry)) {
|
|
204
|
+
if (!config.enabled) continue;
|
|
205
|
+
if (!config.events.includes(event)) continue;
|
|
206
|
+
|
|
207
|
+
hooks.push({ name: hookName, config });
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return hooks;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Main dispatcher logic
|
|
215
|
+
*/
|
|
216
|
+
function dispatch() {
|
|
217
|
+
const registry = loadRegistry();
|
|
218
|
+
const state = loadState();
|
|
219
|
+
const hooks = getHooksForEvent(registry, CURRENT_EVENT);
|
|
220
|
+
|
|
221
|
+
const results = [];
|
|
222
|
+
|
|
223
|
+
for (const { name, config } of hooks) {
|
|
224
|
+
// Check debounce
|
|
225
|
+
if (!shouldRunDebounce(name, config, state)) {
|
|
226
|
+
results.push({ hook: name, status: 'debounced' });
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Check runOnce
|
|
231
|
+
if (!shouldRunOnce(name, config, state)) {
|
|
232
|
+
results.push({ hook: name, status: 'already_run' });
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Check condition
|
|
237
|
+
if (!shouldRunCondition(name, config)) {
|
|
238
|
+
results.push({ hook: name, status: 'condition_not_met' });
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Execute hook
|
|
243
|
+
const result = executeHook(name);
|
|
244
|
+
|
|
245
|
+
// Update state
|
|
246
|
+
state.lastRun[name] = Date.now();
|
|
247
|
+
if (config.runOnce && result.success) {
|
|
248
|
+
state.runOnceCompleted.push(name);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
results.push({
|
|
252
|
+
hook: name,
|
|
253
|
+
status: result.success ? 'executed' : 'failed',
|
|
254
|
+
error: result.error
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Save updated state
|
|
259
|
+
saveState(state);
|
|
260
|
+
|
|
261
|
+
// Output summary (only if there were executed hooks)
|
|
262
|
+
const executed = results.filter(r => r.status === 'executed');
|
|
263
|
+
if (executed.length > 0 && process.env.CLAUDE_HOOK_DEBUG) {
|
|
264
|
+
console.log(`[dispatcher] ${CURRENT_EVENT}: ${executed.length} hooks executed`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return results;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Reset dispatcher state (for testing or new sessions)
|
|
272
|
+
*/
|
|
273
|
+
function reset() {
|
|
274
|
+
if (fs.existsSync(STATE_FILE)) {
|
|
275
|
+
fs.unlinkSync(STATE_FILE);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Main execution
|
|
280
|
+
if (require.main === module) {
|
|
281
|
+
const args = process.argv.slice(2);
|
|
282
|
+
|
|
283
|
+
if (args[0] === '--reset') {
|
|
284
|
+
reset();
|
|
285
|
+
console.log('Dispatcher state reset');
|
|
286
|
+
process.exit(0);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (args[0] === '--status') {
|
|
290
|
+
const state = loadState();
|
|
291
|
+
console.log(JSON.stringify(state, null, 2));
|
|
292
|
+
process.exit(0);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
dispatch();
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
module.exports = {
|
|
299
|
+
dispatch,
|
|
300
|
+
loadRegistry,
|
|
301
|
+
loadState,
|
|
302
|
+
reset,
|
|
303
|
+
getHooksForEvent
|
|
304
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "hook-registry-schema.json",
|
|
3
|
+
"$comment": "Hook Dispatcher Registry - Controls which hooks run and when",
|
|
4
|
+
|
|
5
|
+
"thinking-silent": {
|
|
6
|
+
"events": ["AgentStop"],
|
|
7
|
+
"debounce": 5000,
|
|
8
|
+
"enabled": true,
|
|
9
|
+
"description": "Silent thinking/flow tracking"
|
|
10
|
+
},
|
|
11
|
+
|
|
12
|
+
"multi-session": {
|
|
13
|
+
"events": ["UserPromptSubmit", "AgentStop"],
|
|
14
|
+
"debounce": 3000,
|
|
15
|
+
"condition": "sessions > 1",
|
|
16
|
+
"enabled": true,
|
|
17
|
+
"description": "Multi-session tracking (only when multiple sessions)"
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
"todo-manager": {
|
|
21
|
+
"events": ["AgentStop"],
|
|
22
|
+
"debounce": 10000,
|
|
23
|
+
"enabled": true,
|
|
24
|
+
"description": "Task index management"
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
"rag-skill-loader": {
|
|
28
|
+
"events": ["UserPromptSubmit"],
|
|
29
|
+
"cache": true,
|
|
30
|
+
"cacheTTL": 300000,
|
|
31
|
+
"enabled": true,
|
|
32
|
+
"description": "RAG skill matching with cache"
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
"project-kickoff": {
|
|
36
|
+
"events": ["UserPromptSubmit"],
|
|
37
|
+
"runOnce": true,
|
|
38
|
+
"enabled": true,
|
|
39
|
+
"description": "Project initialization (once per session)"
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
"memory-loader": {
|
|
43
|
+
"events": ["SessionStart"],
|
|
44
|
+
"runOnce": true,
|
|
45
|
+
"enabled": true,
|
|
46
|
+
"description": "Load memory at session start"
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
"memory-saver": {
|
|
50
|
+
"events": ["SessionEnd"],
|
|
51
|
+
"enabled": true,
|
|
52
|
+
"description": "Save memory at session end"
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
"auto-handoff": {
|
|
56
|
+
"events": ["PreCompact"],
|
|
57
|
+
"enabled": true,
|
|
58
|
+
"description": "Auto-generate handoff before compaction"
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
"verify-work": {
|
|
62
|
+
"events": ["AgentStop"],
|
|
63
|
+
"enabled": true,
|
|
64
|
+
"description": "Verify work completion"
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
"code-formatter": {
|
|
68
|
+
"events": ["PostToolUse"],
|
|
69
|
+
"toolMatch": ["Write", "Edit"],
|
|
70
|
+
"enabled": true,
|
|
71
|
+
"description": "Format code after writes (only for Write/Edit tools)"
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hooks Shared Library - Cache Utilities
|
|
3
|
+
*
|
|
4
|
+
* 统一的缓存管理,支持内存和文件缓存
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 内存缓存类
|
|
12
|
+
*/
|
|
13
|
+
class MemoryCache {
|
|
14
|
+
constructor(defaultTTL = 60000) {
|
|
15
|
+
this.cache = new Map();
|
|
16
|
+
this.defaultTTL = defaultTTL;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
get(key) {
|
|
20
|
+
const item = this.cache.get(key);
|
|
21
|
+
if (!item) return null;
|
|
22
|
+
|
|
23
|
+
if (Date.now() > item.expiry) {
|
|
24
|
+
this.cache.delete(key);
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return item.value;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
set(key, value, ttl = this.defaultTTL) {
|
|
32
|
+
this.cache.set(key, {
|
|
33
|
+
value,
|
|
34
|
+
expiry: Date.now() + ttl
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
delete(key) {
|
|
39
|
+
this.cache.delete(key);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
clear() {
|
|
43
|
+
this.cache.clear();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
cleanup() {
|
|
47
|
+
const now = Date.now();
|
|
48
|
+
for (const [key, item] of this.cache.entries()) {
|
|
49
|
+
if (now > item.expiry) {
|
|
50
|
+
this.cache.delete(key);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 文件缓存类
|
|
58
|
+
*/
|
|
59
|
+
class FileCache {
|
|
60
|
+
constructor(cacheFile, defaultTTL = 300000) {
|
|
61
|
+
this.cacheFile = cacheFile;
|
|
62
|
+
this.defaultTTL = defaultTTL;
|
|
63
|
+
this.cache = null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
load() {
|
|
67
|
+
if (this.cache !== null) return;
|
|
68
|
+
|
|
69
|
+
if (!fs.existsSync(this.cacheFile)) {
|
|
70
|
+
this.cache = {};
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
this.cache = JSON.parse(fs.readFileSync(this.cacheFile, 'utf-8'));
|
|
76
|
+
} catch (e) {
|
|
77
|
+
this.cache = {};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
save() {
|
|
82
|
+
try {
|
|
83
|
+
const dir = path.dirname(this.cacheFile);
|
|
84
|
+
if (!fs.existsSync(dir)) {
|
|
85
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
86
|
+
}
|
|
87
|
+
fs.writeFileSync(this.cacheFile, JSON.stringify(this.cache, null, 2));
|
|
88
|
+
} catch (e) {
|
|
89
|
+
// 忽略保存错误
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
get(key) {
|
|
94
|
+
this.load();
|
|
95
|
+
const item = this.cache[key];
|
|
96
|
+
if (!item) return null;
|
|
97
|
+
|
|
98
|
+
if (Date.now() > item.expiry) {
|
|
99
|
+
delete this.cache[key];
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return item.value;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
set(key, value, ttl = this.defaultTTL) {
|
|
107
|
+
this.load();
|
|
108
|
+
this.cache[key] = {
|
|
109
|
+
value,
|
|
110
|
+
expiry: Date.now() + ttl
|
|
111
|
+
};
|
|
112
|
+
this.save();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
delete(key) {
|
|
116
|
+
this.load();
|
|
117
|
+
delete this.cache[key];
|
|
118
|
+
this.save();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
clear() {
|
|
122
|
+
this.cache = {};
|
|
123
|
+
this.save();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
cleanup() {
|
|
127
|
+
this.load();
|
|
128
|
+
const now = Date.now();
|
|
129
|
+
let changed = false;
|
|
130
|
+
|
|
131
|
+
for (const key of Object.keys(this.cache)) {
|
|
132
|
+
if (now > this.cache[key].expiry) {
|
|
133
|
+
delete this.cache[key];
|
|
134
|
+
changed = true;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (changed) {
|
|
139
|
+
this.save();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* 简单哈希函数
|
|
146
|
+
*/
|
|
147
|
+
function hashString(str) {
|
|
148
|
+
let hash = 0;
|
|
149
|
+
for (let i = 0; i < str.length; i++) {
|
|
150
|
+
const char = str.charCodeAt(i);
|
|
151
|
+
hash = ((hash << 5) - hash) + char;
|
|
152
|
+
hash = hash & hash;
|
|
153
|
+
}
|
|
154
|
+
return hash.toString(16);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
module.exports = {
|
|
158
|
+
MemoryCache,
|
|
159
|
+
FileCache,
|
|
160
|
+
hashString
|
|
161
|
+
};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hooks Shared Library - File System Utilities
|
|
3
|
+
*
|
|
4
|
+
* 带缓存的文件操作,减少重复读写
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
// 内存缓存
|
|
11
|
+
const fileCache = new Map();
|
|
12
|
+
const DEFAULT_TTL = 60000; // 1分钟
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 带缓存的 JSON 文件读取
|
|
16
|
+
*/
|
|
17
|
+
function readJsonCached(filePath, ttl = DEFAULT_TTL) {
|
|
18
|
+
const cached = fileCache.get(filePath);
|
|
19
|
+
const now = Date.now();
|
|
20
|
+
|
|
21
|
+
if (cached && now - cached.time < ttl) {
|
|
22
|
+
return cached.data;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (!fs.existsSync(filePath)) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
31
|
+
fileCache.set(filePath, { data, time: now });
|
|
32
|
+
return data;
|
|
33
|
+
} catch (e) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 写入 JSON 文件并更新缓存
|
|
40
|
+
*/
|
|
41
|
+
function writeJsonCached(filePath, data) {
|
|
42
|
+
try {
|
|
43
|
+
const dir = path.dirname(filePath);
|
|
44
|
+
if (!fs.existsSync(dir)) {
|
|
45
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
46
|
+
}
|
|
47
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
|
|
48
|
+
fileCache.set(filePath, { data, time: Date.now() });
|
|
49
|
+
return true;
|
|
50
|
+
} catch (e) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 带缓存的文本文件读取
|
|
57
|
+
*/
|
|
58
|
+
function readTextCached(filePath, ttl = DEFAULT_TTL) {
|
|
59
|
+
const cached = fileCache.get(filePath);
|
|
60
|
+
const now = Date.now();
|
|
61
|
+
|
|
62
|
+
if (cached && now - cached.time < ttl) {
|
|
63
|
+
return cached.data;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!fs.existsSync(filePath)) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const data = fs.readFileSync(filePath, 'utf-8');
|
|
72
|
+
fileCache.set(filePath, { data, time: now });
|
|
73
|
+
return data;
|
|
74
|
+
} catch (e) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 清除缓存
|
|
81
|
+
*/
|
|
82
|
+
function clearCache(filePath = null) {
|
|
83
|
+
if (filePath) {
|
|
84
|
+
fileCache.delete(filePath);
|
|
85
|
+
} else {
|
|
86
|
+
fileCache.clear();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 确保目录存在
|
|
92
|
+
*/
|
|
93
|
+
function ensureDir(dirPath) {
|
|
94
|
+
if (!fs.existsSync(dirPath)) {
|
|
95
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 安全文件操作 - 写入
|
|
101
|
+
*/
|
|
102
|
+
function safeWriteFile(filePath, content) {
|
|
103
|
+
try {
|
|
104
|
+
ensureDir(path.dirname(filePath));
|
|
105
|
+
fs.writeFileSync(filePath, content);
|
|
106
|
+
return true;
|
|
107
|
+
} catch (e) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 安全文件操作 - 追加
|
|
114
|
+
*/
|
|
115
|
+
function safeAppendFile(filePath, content) {
|
|
116
|
+
try {
|
|
117
|
+
ensureDir(path.dirname(filePath));
|
|
118
|
+
fs.appendFileSync(filePath, content);
|
|
119
|
+
return true;
|
|
120
|
+
} catch (e) {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
module.exports = {
|
|
126
|
+
readJsonCached,
|
|
127
|
+
writeJsonCached,
|
|
128
|
+
readTextCached,
|
|
129
|
+
clearCache,
|
|
130
|
+
ensureDir,
|
|
131
|
+
safeWriteFile,
|
|
132
|
+
safeAppendFile
|
|
133
|
+
};
|