@jhihjian/claude-daemon 1.1.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/QUICKSTART.md +214 -0
- package/README.md +173 -0
- package/bin/cli.js +118 -0
- package/hooks/SessionAnalyzer.hook.ts +567 -0
- package/hooks/SessionRecorder.hook.ts +202 -0
- package/hooks/SessionToolCapture-v2.hook.ts +231 -0
- package/hooks/SessionToolCapture.hook.ts +119 -0
- package/install.sh +257 -0
- package/lib/config.ts +223 -0
- package/lib/errors.ts +213 -0
- package/lib/logger.ts +140 -0
- package/package.json +45 -0
- package/tools/SessionQuery.ts +262 -0
- package/tools/SessionStats.ts +139 -0
- package/tools/show-conversation.sh +80 -0
package/install.sh
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Claude Code 会话历史系统 - 一键安装脚本
|
|
3
|
+
# 支持在任何电脑上快速部署
|
|
4
|
+
|
|
5
|
+
set -e
|
|
6
|
+
|
|
7
|
+
# 颜色输出
|
|
8
|
+
RED='\033[0;31m'
|
|
9
|
+
GREEN='\033[0;32m'
|
|
10
|
+
YELLOW='\033[1;33m'
|
|
11
|
+
NC='\033[0m' # No Color
|
|
12
|
+
|
|
13
|
+
echo -e "${GREEN}======================================${NC}"
|
|
14
|
+
echo -e "${GREEN}Claude Code 会话历史系统 - 安装程序${NC}"
|
|
15
|
+
echo -e "${GREEN}======================================${NC}"
|
|
16
|
+
echo ""
|
|
17
|
+
|
|
18
|
+
# 检测系统
|
|
19
|
+
OS=$(uname -s)
|
|
20
|
+
echo -e "${YELLOW}检测到系统: $OS${NC}"
|
|
21
|
+
|
|
22
|
+
# 获取脚本所在目录
|
|
23
|
+
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
|
24
|
+
echo -e "${YELLOW}安装源目录: $SCRIPT_DIR${NC}"
|
|
25
|
+
echo ""
|
|
26
|
+
|
|
27
|
+
# 1. 检查 Bun
|
|
28
|
+
echo -e "${GREEN}[1/6] 检查 Bun 运行时...${NC}"
|
|
29
|
+
if command -v bun &> /dev/null; then
|
|
30
|
+
BUN_PATH=$(which bun)
|
|
31
|
+
echo -e "${GREEN}✓ Bun 已安装: $BUN_PATH${NC}"
|
|
32
|
+
else
|
|
33
|
+
echo -e "${YELLOW}⚠ Bun 未安装,正在安装...${NC}"
|
|
34
|
+
curl -fsSL https://bun.sh/install | bash
|
|
35
|
+
|
|
36
|
+
# 添加到当前 shell
|
|
37
|
+
export BUN_INSTALL="$HOME/.bun"
|
|
38
|
+
export PATH="$BUN_INSTALL/bin:$PATH"
|
|
39
|
+
|
|
40
|
+
BUN_PATH="$HOME/.bun/bin/bun"
|
|
41
|
+
echo -e "${GREEN}✓ Bun 安装完成: $BUN_PATH${NC}"
|
|
42
|
+
fi
|
|
43
|
+
echo ""
|
|
44
|
+
|
|
45
|
+
# 2. 创建目录结构(设置安全权限)
|
|
46
|
+
echo -e "${GREEN}[2/6] 创建目录结构...${NC}"
|
|
47
|
+
mkdir -p ~/.claude/SESSIONS/raw
|
|
48
|
+
mkdir -p ~/.claude/SESSIONS/analysis/summaries
|
|
49
|
+
mkdir -p ~/.claude/SESSIONS/analysis/by-type
|
|
50
|
+
mkdir -p ~/.claude/SESSIONS/analysis/by-directory
|
|
51
|
+
mkdir -p ~/.claude/SESSIONS/index
|
|
52
|
+
mkdir -p ~/.claude/hooks
|
|
53
|
+
|
|
54
|
+
# 设置目录权限:700(仅所有者可访问)
|
|
55
|
+
chmod 700 ~/.claude/SESSIONS
|
|
56
|
+
chmod -R 700 ~/.claude/SESSIONS/*
|
|
57
|
+
chmod 700 ~/.claude/hooks
|
|
58
|
+
|
|
59
|
+
echo -e "${GREEN}✓ 目录创建完成(权限:700)${NC}"
|
|
60
|
+
echo ""
|
|
61
|
+
|
|
62
|
+
# 3. 配置 hooks(使用 #!/usr/bin/env bun)
|
|
63
|
+
echo -e "${GREEN}[3/6] 配置 hooks...${NC}"
|
|
64
|
+
for hook in "$SCRIPT_DIR/hooks"/*.ts; do
|
|
65
|
+
if [ -f "$hook" ]; then
|
|
66
|
+
hook_name=$(basename "$hook")
|
|
67
|
+
target_hook=~/.claude/hooks/"$hook_name"
|
|
68
|
+
|
|
69
|
+
# 直接复制文件(保留 #!/usr/bin/env bun)
|
|
70
|
+
cp "$hook" "$target_hook"
|
|
71
|
+
|
|
72
|
+
# 设置权限:700(仅所有者可读写执行)
|
|
73
|
+
chmod 700 "$target_hook"
|
|
74
|
+
|
|
75
|
+
echo -e "${GREEN} ✓ $hook_name${NC}"
|
|
76
|
+
fi
|
|
77
|
+
done
|
|
78
|
+
echo ""
|
|
79
|
+
|
|
80
|
+
# 4. 配置 Claude Code settings
|
|
81
|
+
echo -e "${GREEN}[4/7] 配置 Claude Code...${NC}"
|
|
82
|
+
SETTINGS_FILE=~/.claude/settings.json
|
|
83
|
+
|
|
84
|
+
if [ -f "$SETTINGS_FILE" ]; then
|
|
85
|
+
echo -e "${YELLOW}⚠ settings.json 已存在,备份到 settings.json.backup${NC}"
|
|
86
|
+
cp "$SETTINGS_FILE" "$SETTINGS_FILE.backup"
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
# 创建或更新 settings.json
|
|
90
|
+
cat > "$SETTINGS_FILE" << EOF
|
|
91
|
+
{
|
|
92
|
+
"model": "opus",
|
|
93
|
+
"hooks": {
|
|
94
|
+
"SessionStart": [
|
|
95
|
+
{
|
|
96
|
+
"hooks": [
|
|
97
|
+
{
|
|
98
|
+
"type": "command",
|
|
99
|
+
"command": "$HOME/.claude/hooks/SessionRecorder.hook.ts"
|
|
100
|
+
}
|
|
101
|
+
]
|
|
102
|
+
}
|
|
103
|
+
],
|
|
104
|
+
"PostToolUse": [
|
|
105
|
+
{
|
|
106
|
+
"hooks": [
|
|
107
|
+
{
|
|
108
|
+
"type": "command",
|
|
109
|
+
"command": "$HOME/.claude/hooks/SessionToolCapture-v2.hook.ts"
|
|
110
|
+
}
|
|
111
|
+
]
|
|
112
|
+
}
|
|
113
|
+
],
|
|
114
|
+
"Stop": [
|
|
115
|
+
{
|
|
116
|
+
"hooks": [
|
|
117
|
+
{
|
|
118
|
+
"type": "command",
|
|
119
|
+
"command": "$HOME/.claude/hooks/SessionAnalyzer.hook.ts"
|
|
120
|
+
}
|
|
121
|
+
]
|
|
122
|
+
}
|
|
123
|
+
]
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
EOF
|
|
127
|
+
|
|
128
|
+
echo -e "${GREEN}✓ settings.json 配置完成${NC}"
|
|
129
|
+
echo ""
|
|
130
|
+
|
|
131
|
+
# 5. 安装查询工具
|
|
132
|
+
echo -e "${GREEN}[5/7] 安装查询工具...${NC}"
|
|
133
|
+
mkdir -p ~/bin
|
|
134
|
+
|
|
135
|
+
# 创建查询工具的包装脚本
|
|
136
|
+
cat > ~/bin/claude-sessions << EOF
|
|
137
|
+
#!/bin/bash
|
|
138
|
+
# Claude 会话历史查询工具
|
|
139
|
+
|
|
140
|
+
BUN_PATH="$BUN_PATH"
|
|
141
|
+
TOOLS_DIR="$SCRIPT_DIR/tools"
|
|
142
|
+
|
|
143
|
+
case "\$1" in
|
|
144
|
+
recent|type|dir)
|
|
145
|
+
\$BUN_PATH \$TOOLS_DIR/SessionQuery.ts "\$@"
|
|
146
|
+
;;
|
|
147
|
+
stats)
|
|
148
|
+
shift
|
|
149
|
+
\$BUN_PATH \$TOOLS_DIR/SessionStats.ts "\$@"
|
|
150
|
+
;;
|
|
151
|
+
show)
|
|
152
|
+
\$TOOLS_DIR/show-conversation.sh "\$2"
|
|
153
|
+
;;
|
|
154
|
+
*)
|
|
155
|
+
echo "用法:"
|
|
156
|
+
echo " claude-sessions recent [N] - 查看最近 N 个会话"
|
|
157
|
+
echo " claude-sessions type <类型> - 查看指定类型的会话"
|
|
158
|
+
echo " claude-sessions dir <目录> - 查看指定目录的会话"
|
|
159
|
+
echo " claude-sessions stats global - 查看全局统计"
|
|
160
|
+
echo " claude-sessions show <会话ID> - 查看会话详情"
|
|
161
|
+
;;
|
|
162
|
+
esac
|
|
163
|
+
EOF
|
|
164
|
+
|
|
165
|
+
chmod +x ~/bin/claude-sessions
|
|
166
|
+
echo -e "${GREEN}✓ 查询工具安装完成${NC}"
|
|
167
|
+
echo ""
|
|
168
|
+
|
|
169
|
+
# 6. 添加到 PATH
|
|
170
|
+
echo -e "${GREEN}[6/7] 配置环境变量...${NC}"
|
|
171
|
+
|
|
172
|
+
# 检测 shell
|
|
173
|
+
if [ -n "$ZSH_VERSION" ]; then
|
|
174
|
+
SHELL_RC=~/.zshrc
|
|
175
|
+
elif [ -n "$BASH_VERSION" ]; then
|
|
176
|
+
SHELL_RC=~/.bashrc
|
|
177
|
+
else
|
|
178
|
+
SHELL_RC=~/.profile
|
|
179
|
+
fi
|
|
180
|
+
|
|
181
|
+
# 添加 PATH
|
|
182
|
+
if ! grep -q "export PATH=\"\$HOME/bin:\$PATH\"" "$SHELL_RC"; then
|
|
183
|
+
echo "" >> "$SHELL_RC"
|
|
184
|
+
echo "# Claude 会话历史工具" >> "$SHELL_RC"
|
|
185
|
+
echo "export PATH=\"\$HOME/bin:\$PATH\"" >> "$SHELL_RC"
|
|
186
|
+
echo -e "${GREEN}✓ 已添加到 $SHELL_RC${NC}"
|
|
187
|
+
else
|
|
188
|
+
echo -e "${GREEN}✓ PATH 已配置${NC}"
|
|
189
|
+
fi
|
|
190
|
+
|
|
191
|
+
echo ""
|
|
192
|
+
|
|
193
|
+
# 7. 验证安装
|
|
194
|
+
echo -e "${GREEN}[7/7] 验证安装...${NC}"
|
|
195
|
+
|
|
196
|
+
# 检查 Bun 是否在 PATH 中
|
|
197
|
+
if command -v bun &> /dev/null; then
|
|
198
|
+
echo -e "${GREEN} ✓ Bun 在 PATH 中${NC}"
|
|
199
|
+
else
|
|
200
|
+
echo -e "${YELLOW} ⚠ Bun 不在 PATH 中,hooks 可能无法执行${NC}"
|
|
201
|
+
echo -e "${YELLOW} 请确保 ~/.bun/bin 在 PATH 中${NC}"
|
|
202
|
+
fi
|
|
203
|
+
|
|
204
|
+
# 检查 hooks 是否可执行
|
|
205
|
+
hooks_ok=true
|
|
206
|
+
for hook_name in SessionRecorder.hook.ts SessionToolCapture-v2.hook.ts SessionAnalyzer.hook.ts; do
|
|
207
|
+
hook_file=~/.claude/hooks/$hook_name
|
|
208
|
+
if [ -x "$hook_file" ]; then
|
|
209
|
+
echo -e "${GREEN} ✓ $hook_name 可执行${NC}"
|
|
210
|
+
else
|
|
211
|
+
echo -e "${RED} ✗ $hook_name 不可执行${NC}"
|
|
212
|
+
hooks_ok=false
|
|
213
|
+
fi
|
|
214
|
+
done
|
|
215
|
+
|
|
216
|
+
# 检查目录权限
|
|
217
|
+
if [ -d ~/.claude/SESSIONS ] && [ "$(stat -c %a ~/.claude/SESSIONS 2>/dev/null || stat -f %A ~/.claude/SESSIONS 2>/dev/null)" = "700" ]; then
|
|
218
|
+
echo -e "${GREEN} ✓ 目录权限正确(700)${NC}"
|
|
219
|
+
else
|
|
220
|
+
echo -e "${YELLOW} ⚠ 目录权限可能不正确${NC}"
|
|
221
|
+
fi
|
|
222
|
+
|
|
223
|
+
# 检查 lib 目录
|
|
224
|
+
if [ -d "$SCRIPT_DIR/lib" ] && [ -f "$SCRIPT_DIR/lib/logger.ts" ]; then
|
|
225
|
+
echo -e "${GREEN} ✓ lib 目录存在${NC}"
|
|
226
|
+
else
|
|
227
|
+
echo -e "${RED} ✗ lib 目录不存在或不完整${NC}"
|
|
228
|
+
hooks_ok=false
|
|
229
|
+
fi
|
|
230
|
+
|
|
231
|
+
echo ""
|
|
232
|
+
|
|
233
|
+
if [ "$hooks_ok" = true ]; then
|
|
234
|
+
echo -e "${GREEN}======================================${NC}"
|
|
235
|
+
echo -e "${GREEN}✓ 安装完成!${NC}"
|
|
236
|
+
echo -e "${GREEN}======================================${NC}"
|
|
237
|
+
else
|
|
238
|
+
echo -e "${YELLOW}======================================${NC}"
|
|
239
|
+
echo -e "${YELLOW}⚠ 安装完成,但有警告${NC}"
|
|
240
|
+
echo -e "${YELLOW}======================================${NC}"
|
|
241
|
+
fi
|
|
242
|
+
|
|
243
|
+
echo ""
|
|
244
|
+
echo -e "${YELLOW}使用方法:${NC}"
|
|
245
|
+
echo ""
|
|
246
|
+
echo -e " ${GREEN}claude-sessions recent 5${NC} # 查看最近 5 个会话"
|
|
247
|
+
echo -e " ${GREEN}claude-sessions stats global${NC} # 查看统计信息"
|
|
248
|
+
echo -e " ${GREEN}claude-sessions show <ID>${NC} # 查看会话详情"
|
|
249
|
+
echo ""
|
|
250
|
+
echo -e "${YELLOW}重要提示:${NC}"
|
|
251
|
+
echo -e " 1. 重新加载 shell 配置: ${GREEN}source $SHELL_RC${NC}"
|
|
252
|
+
echo -e " 2. 或者重启终端"
|
|
253
|
+
echo -e " 3. 设置日志级别: ${GREEN}export SESSION_LOG_LEVEL=DEBUG${NC}"
|
|
254
|
+
echo ""
|
|
255
|
+
echo -e "${YELLOW}数据同步:${NC}"
|
|
256
|
+
echo -e " 查看同步指南: ${GREEN}cat $SCRIPT_DIR/SYNC-GUIDE.md${NC}"
|
|
257
|
+
echo ""
|
package/lib/config.ts
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* config.ts
|
|
3
|
+
* 统一的配置管理模块
|
|
4
|
+
*
|
|
5
|
+
* 功能:
|
|
6
|
+
* - 集中管理所有配置项
|
|
7
|
+
* - 支持环境变量覆盖
|
|
8
|
+
* - 提供默认值
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { join } from 'path';
|
|
12
|
+
import { homedir } from 'os';
|
|
13
|
+
import { existsSync, readFileSync } from 'fs';
|
|
14
|
+
import { safeJSONParse } from './errors.ts';
|
|
15
|
+
|
|
16
|
+
export interface SessionConfig {
|
|
17
|
+
// 路径配置
|
|
18
|
+
paiDir: string;
|
|
19
|
+
sessionsDir: string;
|
|
20
|
+
rawDir: string;
|
|
21
|
+
analysisDir: string;
|
|
22
|
+
summariesDir: string;
|
|
23
|
+
indexDir: string;
|
|
24
|
+
|
|
25
|
+
// 行为配置
|
|
26
|
+
maxOutputLength: number;
|
|
27
|
+
hookTimeout: number;
|
|
28
|
+
gitTimeout: number;
|
|
29
|
+
|
|
30
|
+
// 分类阈值
|
|
31
|
+
classificationThresholds: {
|
|
32
|
+
coding: number; // Edit/Write 占比
|
|
33
|
+
debugging: number; // 测试命令 + Read > Edit
|
|
34
|
+
research: number; // Search 占比
|
|
35
|
+
writing: number; // Markdown 文件编辑占比
|
|
36
|
+
git: number; // Git 命令占比
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// 日志配置
|
|
40
|
+
logLevel: string;
|
|
41
|
+
|
|
42
|
+
// 性能配置
|
|
43
|
+
enableCache: boolean;
|
|
44
|
+
cacheTTL: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
class ConfigManager {
|
|
48
|
+
private config: SessionConfig;
|
|
49
|
+
private configFilePath?: string;
|
|
50
|
+
|
|
51
|
+
constructor() {
|
|
52
|
+
this.config = this.loadConfig();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private loadConfig(): SessionConfig {
|
|
56
|
+
// 默认配置
|
|
57
|
+
const defaults: SessionConfig = {
|
|
58
|
+
// 路径配置
|
|
59
|
+
paiDir: process.env.PAI_DIR || join(homedir(), '.claude'),
|
|
60
|
+
sessionsDir: '',
|
|
61
|
+
rawDir: '',
|
|
62
|
+
analysisDir: '',
|
|
63
|
+
summariesDir: '',
|
|
64
|
+
indexDir: '',
|
|
65
|
+
|
|
66
|
+
// 行为配置
|
|
67
|
+
maxOutputLength: parseInt(process.env.MAX_OUTPUT_LENGTH || '5000', 10),
|
|
68
|
+
hookTimeout: parseInt(process.env.HOOK_TIMEOUT || '10000', 10),
|
|
69
|
+
gitTimeout: parseInt(process.env.GIT_TIMEOUT || '3000', 10),
|
|
70
|
+
|
|
71
|
+
// 分类阈值
|
|
72
|
+
classificationThresholds: {
|
|
73
|
+
coding: parseFloat(process.env.THRESHOLD_CODING || '0.4'),
|
|
74
|
+
debugging: parseFloat(process.env.THRESHOLD_DEBUGGING || '0.0'),
|
|
75
|
+
research: parseFloat(process.env.THRESHOLD_RESEARCH || '0.3'),
|
|
76
|
+
writing: parseFloat(process.env.THRESHOLD_WRITING || '0.5'),
|
|
77
|
+
git: parseFloat(process.env.THRESHOLD_GIT || '0.5'),
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
// 日志配置
|
|
81
|
+
logLevel: process.env.SESSION_LOG_LEVEL || 'INFO',
|
|
82
|
+
|
|
83
|
+
// 性能配置
|
|
84
|
+
enableCache: process.env.ENABLE_CACHE !== 'false',
|
|
85
|
+
cacheTTL: parseInt(process.env.CACHE_TTL || '300000', 10), // 5 分钟
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// 计算派生路径
|
|
89
|
+
defaults.sessionsDir = join(defaults.paiDir, 'SESSIONS');
|
|
90
|
+
defaults.rawDir = join(defaults.sessionsDir, 'raw');
|
|
91
|
+
defaults.analysisDir = join(defaults.sessionsDir, 'analysis');
|
|
92
|
+
defaults.summariesDir = join(defaults.analysisDir, 'summaries');
|
|
93
|
+
defaults.indexDir = join(defaults.sessionsDir, 'index');
|
|
94
|
+
|
|
95
|
+
// 尝试从配置文件加载
|
|
96
|
+
this.configFilePath = join(defaults.paiDir, 'session-config.json');
|
|
97
|
+
if (existsSync(this.configFilePath)) {
|
|
98
|
+
try {
|
|
99
|
+
const fileContent = readFileSync(this.configFilePath, 'utf-8');
|
|
100
|
+
const fileConfig = safeJSONParse<Partial<SessionConfig>>(
|
|
101
|
+
fileContent,
|
|
102
|
+
{},
|
|
103
|
+
'session-config.json'
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
// 合并配置(文件配置优先,但环境变量最优先)
|
|
107
|
+
return this.mergeConfig(defaults, fileConfig);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
// 配置文件加载失败,使用默认配置
|
|
110
|
+
console.error('[Config] Failed to load config file, using defaults');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return defaults;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private mergeConfig(defaults: SessionConfig, fileConfig: Partial<SessionConfig>): SessionConfig {
|
|
118
|
+
return {
|
|
119
|
+
...defaults,
|
|
120
|
+
...fileConfig,
|
|
121
|
+
// 确保环境变量优先
|
|
122
|
+
paiDir: process.env.PAI_DIR || fileConfig.paiDir || defaults.paiDir,
|
|
123
|
+
maxOutputLength: parseInt(
|
|
124
|
+
process.env.MAX_OUTPUT_LENGTH ||
|
|
125
|
+
String(fileConfig.maxOutputLength || defaults.maxOutputLength),
|
|
126
|
+
10
|
|
127
|
+
),
|
|
128
|
+
hookTimeout: parseInt(
|
|
129
|
+
process.env.HOOK_TIMEOUT ||
|
|
130
|
+
String(fileConfig.hookTimeout || defaults.hookTimeout),
|
|
131
|
+
10
|
|
132
|
+
),
|
|
133
|
+
gitTimeout: parseInt(
|
|
134
|
+
process.env.GIT_TIMEOUT ||
|
|
135
|
+
String(fileConfig.gitTimeout || defaults.gitTimeout),
|
|
136
|
+
10
|
|
137
|
+
),
|
|
138
|
+
logLevel: process.env.SESSION_LOG_LEVEL || fileConfig.logLevel || defaults.logLevel,
|
|
139
|
+
enableCache: process.env.ENABLE_CACHE !== 'false' &&
|
|
140
|
+
(fileConfig.enableCache ?? defaults.enableCache),
|
|
141
|
+
cacheTTL: parseInt(
|
|
142
|
+
process.env.CACHE_TTL ||
|
|
143
|
+
String(fileConfig.cacheTTL || defaults.cacheTTL),
|
|
144
|
+
10
|
|
145
|
+
),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* 获取配置
|
|
151
|
+
*/
|
|
152
|
+
get(): SessionConfig {
|
|
153
|
+
return { ...this.config };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* 获取特定配置项
|
|
158
|
+
*/
|
|
159
|
+
getPath(key: keyof Pick<SessionConfig, 'paiDir' | 'sessionsDir' | 'rawDir' | 'analysisDir' | 'summariesDir' | 'indexDir'>): string {
|
|
160
|
+
return this.config[key];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* 获取会话文件路径
|
|
165
|
+
*/
|
|
166
|
+
getSessionFilePath(sessionId: string, yearMonth: string): string {
|
|
167
|
+
return join(this.config.rawDir, yearMonth, `session-${sessionId}.jsonl`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* 获取摘要文件路径
|
|
172
|
+
*/
|
|
173
|
+
getSummaryFilePath(sessionId: string, yearMonth: string): string {
|
|
174
|
+
return join(this.config.summariesDir, yearMonth, `summary-${sessionId}.json`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* 获取类型索引路径
|
|
179
|
+
*/
|
|
180
|
+
getTypeIndexPath(sessionType: string): string {
|
|
181
|
+
return join(this.config.analysisDir, 'by-type', sessionType, 'sessions.json');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* 获取目录索引路径
|
|
186
|
+
*/
|
|
187
|
+
getDirectoryIndexPath(dirHash: string): string {
|
|
188
|
+
return join(this.config.analysisDir, 'by-directory', dirHash, 'sessions.json');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* 获取全局元数据路径
|
|
193
|
+
*/
|
|
194
|
+
getMetadataPath(): string {
|
|
195
|
+
return join(this.config.indexDir, 'metadata.json');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* 获取年月字符串
|
|
200
|
+
*/
|
|
201
|
+
getYearMonth(date: Date = new Date()): string {
|
|
202
|
+
return date.toISOString().slice(0, 7);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* 重新加载配置
|
|
207
|
+
*/
|
|
208
|
+
reload(): void {
|
|
209
|
+
this.config = this.loadConfig();
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* 全局配置实例
|
|
215
|
+
*/
|
|
216
|
+
export const config = new ConfigManager();
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* 便捷函数
|
|
220
|
+
*/
|
|
221
|
+
export function getConfig(): SessionConfig {
|
|
222
|
+
return config.get();
|
|
223
|
+
}
|
package/lib/errors.ts
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* errors.ts
|
|
3
|
+
* 统一的错误处理模块
|
|
4
|
+
*
|
|
5
|
+
* 功能:
|
|
6
|
+
* - 自定义错误类型
|
|
7
|
+
* - 错误恢复策略
|
|
8
|
+
* - 友好的错误消息
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { logger } from './logger.ts';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 基础错误类
|
|
15
|
+
*/
|
|
16
|
+
export class SessionError extends Error {
|
|
17
|
+
constructor(
|
|
18
|
+
message: string,
|
|
19
|
+
public readonly code: string,
|
|
20
|
+
public readonly recoverable: boolean = true,
|
|
21
|
+
public readonly context?: Record<string, any>
|
|
22
|
+
) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.name = 'SessionError';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
toJSON() {
|
|
28
|
+
return {
|
|
29
|
+
name: this.name,
|
|
30
|
+
message: this.message,
|
|
31
|
+
code: this.code,
|
|
32
|
+
recoverable: this.recoverable,
|
|
33
|
+
context: this.context,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 文件系统错误
|
|
40
|
+
*/
|
|
41
|
+
export class FileSystemError extends SessionError {
|
|
42
|
+
constructor(message: string, filePath: string, operation: string) {
|
|
43
|
+
super(message, 'FS_ERROR', true, { filePath, operation });
|
|
44
|
+
this.name = 'FileSystemError';
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 解析错误
|
|
50
|
+
*/
|
|
51
|
+
export class ParseError extends SessionError {
|
|
52
|
+
constructor(message: string, data: string) {
|
|
53
|
+
super(message, 'PARSE_ERROR', true, { data: data.slice(0, 100) });
|
|
54
|
+
this.name = 'ParseError';
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 配置错误
|
|
60
|
+
*/
|
|
61
|
+
export class ConfigError extends SessionError {
|
|
62
|
+
constructor(message: string, configKey?: string) {
|
|
63
|
+
super(message, 'CONFIG_ERROR', false, { configKey });
|
|
64
|
+
this.name = 'ConfigError';
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 超时错误
|
|
70
|
+
*/
|
|
71
|
+
export class TimeoutError extends SessionError {
|
|
72
|
+
constructor(operation: string, timeout: number) {
|
|
73
|
+
super(`Operation timed out: ${operation}`, 'TIMEOUT_ERROR', true, {
|
|
74
|
+
operation,
|
|
75
|
+
timeout,
|
|
76
|
+
});
|
|
77
|
+
this.name = 'TimeoutError';
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 执行带超时的异步操作
|
|
83
|
+
*/
|
|
84
|
+
export async function withTimeout<T>(
|
|
85
|
+
promise: Promise<T>,
|
|
86
|
+
timeout: number,
|
|
87
|
+
operation: string
|
|
88
|
+
): Promise<T> {
|
|
89
|
+
return Promise.race([
|
|
90
|
+
promise,
|
|
91
|
+
new Promise<T>((_, reject) =>
|
|
92
|
+
setTimeout(() => reject(new TimeoutError(operation, timeout)), timeout)
|
|
93
|
+
),
|
|
94
|
+
]);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 安全执行函数(捕获并记录错误,但不抛出)
|
|
99
|
+
*/
|
|
100
|
+
export async function safeExecute<T>(
|
|
101
|
+
fn: () => Promise<T> | T,
|
|
102
|
+
fallback: T,
|
|
103
|
+
context: string
|
|
104
|
+
): Promise<T> {
|
|
105
|
+
try {
|
|
106
|
+
return await fn();
|
|
107
|
+
} catch (error) {
|
|
108
|
+
logger.error(`Error in ${context}`, {
|
|
109
|
+
error: error instanceof Error ? error.message : String(error),
|
|
110
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
111
|
+
});
|
|
112
|
+
return fallback;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* 重试执行函数
|
|
118
|
+
*/
|
|
119
|
+
export async function retry<T>(
|
|
120
|
+
fn: () => Promise<T>,
|
|
121
|
+
options: {
|
|
122
|
+
maxAttempts?: number;
|
|
123
|
+
delay?: number;
|
|
124
|
+
backoff?: number;
|
|
125
|
+
context?: string;
|
|
126
|
+
} = {}
|
|
127
|
+
): Promise<T> {
|
|
128
|
+
const { maxAttempts = 3, delay = 1000, backoff = 2, context = 'operation' } = options;
|
|
129
|
+
|
|
130
|
+
let lastError: Error | undefined;
|
|
131
|
+
|
|
132
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
133
|
+
try {
|
|
134
|
+
return await fn();
|
|
135
|
+
} catch (error) {
|
|
136
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
137
|
+
|
|
138
|
+
if (attempt < maxAttempts) {
|
|
139
|
+
const waitTime = delay * Math.pow(backoff, attempt - 1);
|
|
140
|
+
logger.warn(`Retry ${attempt}/${maxAttempts} for ${context}`, {
|
|
141
|
+
error: lastError.message,
|
|
142
|
+
nextRetryIn: waitTime,
|
|
143
|
+
});
|
|
144
|
+
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
throw lastError || new Error(`Failed after ${maxAttempts} attempts`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Hook 错误处理包装器
|
|
154
|
+
* 确保 Hook 永远不会阻塞 Claude Code
|
|
155
|
+
*/
|
|
156
|
+
export function hookErrorHandler(hookName: string) {
|
|
157
|
+
return (error: unknown): void => {
|
|
158
|
+
// 记录错误但不抛出
|
|
159
|
+
logger.error(`Hook ${hookName} failed`, {
|
|
160
|
+
error: error instanceof Error ? error.message : String(error),
|
|
161
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
162
|
+
recoverable: error instanceof SessionError ? error.recoverable : true,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// Hook 失败不应阻塞 Claude Code
|
|
166
|
+
// 只记录到 stderr,让系统继续运行
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* 验证函数
|
|
172
|
+
*/
|
|
173
|
+
export function validateRequired<T>(
|
|
174
|
+
value: T | null | undefined,
|
|
175
|
+
fieldName: string
|
|
176
|
+
): T {
|
|
177
|
+
if (value === null || value === undefined) {
|
|
178
|
+
throw new ConfigError(`Required field missing: ${fieldName}`, fieldName);
|
|
179
|
+
}
|
|
180
|
+
return value;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* 验证文件路径
|
|
185
|
+
*/
|
|
186
|
+
export function validatePath(path: string, fieldName: string = 'path'): string {
|
|
187
|
+
if (!path || typeof path !== 'string') {
|
|
188
|
+
throw new ConfigError(`Invalid path: ${fieldName}`, fieldName);
|
|
189
|
+
}
|
|
190
|
+
if (path.includes('\0')) {
|
|
191
|
+
throw new ConfigError(`Path contains null bytes: ${fieldName}`, fieldName);
|
|
192
|
+
}
|
|
193
|
+
return path;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* 安全的 JSON 解析
|
|
198
|
+
*/
|
|
199
|
+
export function safeJSONParse<T>(
|
|
200
|
+
data: string,
|
|
201
|
+
fallback: T,
|
|
202
|
+
context: string = 'JSON'
|
|
203
|
+
): T {
|
|
204
|
+
try {
|
|
205
|
+
return JSON.parse(data) as T;
|
|
206
|
+
} catch (error) {
|
|
207
|
+
logger.warn(`Failed to parse ${context}`, {
|
|
208
|
+
error: error instanceof Error ? error.message : String(error),
|
|
209
|
+
dataPreview: data.slice(0, 100),
|
|
210
|
+
});
|
|
211
|
+
return fallback;
|
|
212
|
+
}
|
|
213
|
+
}
|