sumulige-claude 1.3.3 → 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 +55 -0
- package/README.md +96 -1
- package/config/version-manifest.json +85 -0
- package/lib/commands.js +56 -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/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/rag-skill-loader.cjs +84 -4
- package/template/.claude/settings.json +8 -82
- package/template/CHANGELOG.md +297 -0
- package/template/README.md +558 -88
|
@@ -0,0 +1 @@
|
|
|
1
|
+
1.3.3
|
package/.claude/AGENTS.md
CHANGED
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
|
|
7
7
|
This project uses **Sumulige Claude** for multi-agent collaboration.
|
|
8
8
|
|
|
9
|
+
### conductor
|
|
10
|
+
- **Model**: claude-opus-4-5-20251101
|
|
11
|
+
- **Role**: Task coordination and decomposition
|
|
12
|
+
|
|
9
13
|
### architect
|
|
10
14
|
- **Model**: claude-opus-4-5-20251101
|
|
11
15
|
- **Role**: Architecture design and decisions
|
|
@@ -14,18 +18,14 @@ This project uses **Sumulige Claude** for multi-agent collaboration.
|
|
|
14
18
|
- **Model**: claude-opus-4-5-20251101
|
|
15
19
|
- **Role**: Code implementation and testing
|
|
16
20
|
|
|
17
|
-
###
|
|
21
|
+
### reviewer
|
|
18
22
|
- **Model**: claude-opus-4-5-20251101
|
|
19
|
-
- **Role**:
|
|
23
|
+
- **Role**: Code review and quality check
|
|
20
24
|
|
|
21
25
|
### librarian
|
|
22
26
|
- **Model**: claude-opus-4-5-20251101
|
|
23
27
|
- **Role**: Documentation and knowledge
|
|
24
28
|
|
|
25
|
-
### reviewer
|
|
26
|
-
- **Model**: claude-opus-4-5-20251101
|
|
27
|
-
- **Role**: Code review and quality check
|
|
28
|
-
|
|
29
29
|
## Usage
|
|
30
30
|
|
|
31
31
|
```bash
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# /workflow - 统一工作流命令
|
|
2
|
+
|
|
3
|
+
一键执行常见工作流操作。
|
|
4
|
+
|
|
5
|
+
## 可用子命令
|
|
6
|
+
|
|
7
|
+
### `/workflow check` - 检查状态
|
|
8
|
+
检查 sumulige-claude 更新和项目状态。
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
# 检查版本和更新
|
|
12
|
+
smc sync --check-update
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### `/workflow pull` - 拉取更新
|
|
16
|
+
增量同步 sumulige-claude 更新到当前项目。
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# 增量同步(推荐)
|
|
20
|
+
smc sync --incremental
|
|
21
|
+
|
|
22
|
+
# 强制全量同步
|
|
23
|
+
smc sync --hooks
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### `/workflow task <description>` - 执行任务
|
|
27
|
+
标准任务执行流程:
|
|
28
|
+
1. 分析任务需求
|
|
29
|
+
2. 创建 TODO 列表
|
|
30
|
+
3. 逐步实现
|
|
31
|
+
4. 更新文档
|
|
32
|
+
|
|
33
|
+
### `/workflow sync` - 同步文档
|
|
34
|
+
更新项目文档和记忆:
|
|
35
|
+
- MEMORY.md - 最新变更
|
|
36
|
+
- PROJECT_LOG.md - 历史记录
|
|
37
|
+
- CHANGELOG.md - 版本日志
|
|
38
|
+
|
|
39
|
+
### `/workflow commit <message>` - 提交变更
|
|
40
|
+
Git 提交工作流:
|
|
41
|
+
1. 检查变更状态
|
|
42
|
+
2. 暂存相关文件
|
|
43
|
+
3. 提交并生成消息
|
|
44
|
+
|
|
45
|
+
### `/workflow push` - 推送远程
|
|
46
|
+
将本地变更推送到远程仓库。
|
|
47
|
+
|
|
48
|
+
### `/workflow full <description>` - 一键完整流程
|
|
49
|
+
执行完整的开发工作流:
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
1. 检查更新 → 2. 增量同步 → 3. 执行任务 → 4. 更新文档 → 5. 提交 → 6. 推送
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
示例:
|
|
56
|
+
```
|
|
57
|
+
/workflow full "实现用户认证功能"
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## 使用示例
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
# 日常任务
|
|
64
|
+
/workflow task "修复登录 bug"
|
|
65
|
+
|
|
66
|
+
# 完整流程
|
|
67
|
+
/workflow full "添加暗黑模式"
|
|
68
|
+
|
|
69
|
+
# 仅同步文档
|
|
70
|
+
/workflow sync
|
|
71
|
+
|
|
72
|
+
# 仅提交
|
|
73
|
+
/workflow commit "fix: 修复登录问题"
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## 注意事项
|
|
77
|
+
|
|
78
|
+
- 任务执行前会自动检查更新
|
|
79
|
+
- 增量同步只更新变更的文件
|
|
80
|
+
- 文档同步包括 MEMORY.md 和 PROJECT_LOG.md
|
|
81
|
+
- 提交前会自动运行测试(如配置)
|
|
File without changes
|
|
@@ -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
|
+
};
|