@changw98ic/cli 1.0.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/LICENSE +674 -0
- package/README.md +57 -0
- package/dist/commands/adapt.d.ts +8 -0
- package/dist/commands/adapt.d.ts.map +1 -0
- package/dist/commands/adapt.js +108 -0
- package/dist/commands/adapt.js.map +1 -0
- package/dist/commands/archive.d.ts +6 -0
- package/dist/commands/archive.d.ts.map +1 -0
- package/dist/commands/archive.js +66 -0
- package/dist/commands/archive.js.map +1 -0
- package/dist/commands/backup.d.ts +6 -0
- package/dist/commands/backup.d.ts.map +1 -0
- package/dist/commands/backup.js +151 -0
- package/dist/commands/backup.js.map +1 -0
- package/dist/commands/context.d.ts +9 -0
- package/dist/commands/context.d.ts.map +1 -0
- package/dist/commands/context.js +203 -0
- package/dist/commands/context.js.map +1 -0
- package/dist/commands/dashboard.d.ts +6 -0
- package/dist/commands/dashboard.d.ts.map +1 -0
- package/dist/commands/dashboard.js +27 -0
- package/dist/commands/dashboard.js.map +1 -0
- package/dist/commands/entity.d.ts +6 -0
- package/dist/commands/entity.d.ts.map +1 -0
- package/dist/commands/entity.js +173 -0
- package/dist/commands/entity.js.map +1 -0
- package/dist/commands/extract-context.d.ts +9 -0
- package/dist/commands/extract-context.d.ts.map +1 -0
- package/dist/commands/extract-context.js +135 -0
- package/dist/commands/extract-context.js.map +1 -0
- package/dist/commands/index.d.ts +6 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/index.js +69 -0
- package/dist/commands/index.js.map +1 -0
- package/dist/commands/init.d.ts +8 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +37 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/migrate.d.ts +6 -0
- package/dist/commands/migrate.d.ts.map +1 -0
- package/dist/commands/migrate.js +67 -0
- package/dist/commands/migrate.js.map +1 -0
- package/dist/commands/plan.d.ts +6 -0
- package/dist/commands/plan.d.ts.map +1 -0
- package/dist/commands/plan.js +35 -0
- package/dist/commands/plan.js.map +1 -0
- package/dist/commands/preflight.d.ts +6 -0
- package/dist/commands/preflight.d.ts.map +1 -0
- package/dist/commands/preflight.js +80 -0
- package/dist/commands/preflight.js.map +1 -0
- package/dist/commands/query.d.ts +6 -0
- package/dist/commands/query.d.ts.map +1 -0
- package/dist/commands/query.js +82 -0
- package/dist/commands/query.js.map +1 -0
- package/dist/commands/rag.d.ts +6 -0
- package/dist/commands/rag.d.ts.map +1 -0
- package/dist/commands/rag.js +113 -0
- package/dist/commands/rag.js.map +1 -0
- package/dist/commands/review.d.ts +6 -0
- package/dist/commands/review.d.ts.map +1 -0
- package/dist/commands/review.js +38 -0
- package/dist/commands/review.js.map +1 -0
- package/dist/commands/state.d.ts +6 -0
- package/dist/commands/state.d.ts.map +1 -0
- package/dist/commands/state.js +64 -0
- package/dist/commands/state.js.map +1 -0
- package/dist/commands/status.d.ts +6 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +49 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/style.d.ts +9 -0
- package/dist/commands/style.d.ts.map +1 -0
- package/dist/commands/style.js +120 -0
- package/dist/commands/style.js.map +1 -0
- package/dist/commands/update-state.d.ts +6 -0
- package/dist/commands/update-state.d.ts.map +1 -0
- package/dist/commands/update-state.js +92 -0
- package/dist/commands/update-state.js.map +1 -0
- package/dist/commands/use.d.ts +5 -0
- package/dist/commands/use.d.ts.map +1 -0
- package/dist/commands/use.js +33 -0
- package/dist/commands/use.js.map +1 -0
- package/dist/commands/where.d.ts +2 -0
- package/dist/commands/where.d.ts.map +1 -0
- package/dist/commands/where.js +13 -0
- package/dist/commands/where.js.map +1 -0
- package/dist/commands/workflow.d.ts +6 -0
- package/dist/commands/workflow.d.ts.map +1 -0
- package/dist/commands/workflow.js +155 -0
- package/dist/commands/workflow.js.map +1 -0
- package/dist/commands/write.d.ts +7 -0
- package/dist/commands/write.d.ts.map +1 -0
- package/dist/commands/write.js +33 -0
- package/dist/commands/write.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +123 -0
- package/dist/index.js.map +1 -0
- package/dist/utils/api-client.d.ts +61 -0
- package/dist/utils/api-client.d.ts.map +1 -0
- package/dist/utils/api-client.js +390 -0
- package/dist/utils/api-client.js.map +1 -0
- package/dist/utils/api-client.types.d.ts +42 -0
- package/dist/utils/api-client.types.d.ts.map +1 -0
- package/dist/utils/api-client.types.js +5 -0
- package/dist/utils/api-client.types.js.map +1 -0
- package/dist/utils/archive-manager.d.ts +72 -0
- package/dist/utils/archive-manager.d.ts.map +1 -0
- package/dist/utils/archive-manager.js +449 -0
- package/dist/utils/archive-manager.js.map +1 -0
- package/dist/utils/backup-manager.d.ts +65 -0
- package/dist/utils/backup-manager.d.ts.map +1 -0
- package/dist/utils/backup-manager.js +266 -0
- package/dist/utils/backup-manager.js.map +1 -0
- package/dist/utils/chapter-context.d.ts +77 -0
- package/dist/utils/chapter-context.d.ts.map +1 -0
- package/dist/utils/chapter-context.js +280 -0
- package/dist/utils/chapter-context.js.map +1 -0
- package/dist/utils/entity-linker.d.ts +74 -0
- package/dist/utils/entity-linker.d.ts.map +1 -0
- package/dist/utils/entity-linker.js +208 -0
- package/dist/utils/entity-linker.js.map +1 -0
- package/dist/utils/index.d.ts +19 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +22 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/observability.d.ts +145 -0
- package/dist/utils/observability.d.ts.map +1 -0
- package/dist/utils/observability.js +298 -0
- package/dist/utils/observability.js.map +1 -0
- package/dist/utils/output.d.ts +45 -0
- package/dist/utils/output.d.ts.map +1 -0
- package/dist/utils/output.js +60 -0
- package/dist/utils/output.js.map +1 -0
- package/dist/utils/path-compat.d.ts +63 -0
- package/dist/utils/path-compat.d.ts.map +1 -0
- package/dist/utils/path-compat.js +128 -0
- package/dist/utils/path-compat.js.map +1 -0
- package/dist/utils/project-locator.d.ts +47 -0
- package/dist/utils/project-locator.d.ts.map +1 -0
- package/dist/utils/project-locator.js +372 -0
- package/dist/utils/project-locator.js.map +1 -0
- package/dist/utils/runtime-compat.d.ts +39 -0
- package/dist/utils/runtime-compat.d.ts.map +1 -0
- package/dist/utils/runtime-compat.js +113 -0
- package/dist/utils/runtime-compat.js.map +1 -0
- package/dist/utils/security.d.ts +74 -0
- package/dist/utils/security.d.ts.map +1 -0
- package/dist/utils/security.js +291 -0
- package/dist/utils/security.js.map +1 -0
- package/dist/utils/status-reporter.d.ts +66 -0
- package/dist/utils/status-reporter.d.ts.map +1 -0
- package/dist/utils/status-reporter.js +309 -0
- package/dist/utils/status-reporter.js.map +1 -0
- package/dist/utils/style-sampler.d.ts +69 -0
- package/dist/utils/style-sampler.d.ts.map +1 -0
- package/dist/utils/style-sampler.js +206 -0
- package/dist/utils/style-sampler.js.map +1 -0
- package/dist/utils/workflow-manager.d.ts +104 -0
- package/dist/utils/workflow-manager.d.ts.map +1 -0
- package/dist/utils/workflow-manager.js +462 -0
- package/dist/utils/workflow-manager.js.map +1 -0
- package/package.json +66 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 安全工具函数
|
|
3
|
+
*
|
|
4
|
+
* 迁移自 Python security_utils.py
|
|
5
|
+
* 提供文件名清洗、原子写入、Git 操作封装
|
|
6
|
+
*/
|
|
7
|
+
import { spawn, spawnSync } from 'child_process';
|
|
8
|
+
import { existsSync, mkdirSync, renameSync, unlinkSync, copyFileSync, readFileSync, writeFileSync, fsyncSync, openSync, closeSync } from 'fs';
|
|
9
|
+
import { basename, dirname, join, extname } from 'path';
|
|
10
|
+
import { randomUUID } from 'crypto';
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// 文件名清洗
|
|
13
|
+
// ============================================================================
|
|
14
|
+
/**
|
|
15
|
+
* 清理文件名,防止路径遍历攻击 (CWE-22)
|
|
16
|
+
*
|
|
17
|
+
* @param name 原始文件名(可能包含路径遍历字符)
|
|
18
|
+
* @param maxLength 文件名最大长度(默认 100 字符)
|
|
19
|
+
* @returns 安全的文件名
|
|
20
|
+
*/
|
|
21
|
+
export function sanitizeFilename(name, maxLength = 100) {
|
|
22
|
+
// Step 1: 仅保留基础文件名(移除所有路径)
|
|
23
|
+
let safeName = basename(name);
|
|
24
|
+
// Step 2: 移除路径分隔符(双重保险)
|
|
25
|
+
safeName = safeName.replace(/[/\\]/g, '_');
|
|
26
|
+
// Step 3: 只保留安全字符
|
|
27
|
+
// 允许:中文(\u4e00-\u9fff)、字母(a-zA-Z)、数字(0-9)、下划线(_)、连字符(-)
|
|
28
|
+
safeName = safeName.replace(/[^\w\u4e00-\u9fff-]/g, '_');
|
|
29
|
+
// Step 4: 移除连续的下划线(美化)
|
|
30
|
+
safeName = safeName.replace(/_+/g, '_');
|
|
31
|
+
// Step 5: 长度限制
|
|
32
|
+
if (safeName.length > maxLength) {
|
|
33
|
+
safeName = safeName.slice(0, maxLength);
|
|
34
|
+
}
|
|
35
|
+
// Step 6: 移除首尾下划线
|
|
36
|
+
safeName = safeName.replace(/^_+|_+$/g, '');
|
|
37
|
+
// Step 7: 确保非空
|
|
38
|
+
if (!safeName) {
|
|
39
|
+
safeName = 'unnamed_entity';
|
|
40
|
+
}
|
|
41
|
+
return safeName;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* 清理 Git 提交消息,防止命令注入 (CWE-77)
|
|
45
|
+
*
|
|
46
|
+
* @param message 原始提交消息
|
|
47
|
+
* @param maxLength 消息最大长度(默认 200 字符)
|
|
48
|
+
* @returns 安全的提交消息
|
|
49
|
+
*/
|
|
50
|
+
export function sanitizeCommitMessage(message, maxLength = 200) {
|
|
51
|
+
// Step 1: 移除换行符(防止多行参数注入)
|
|
52
|
+
let safeMsg = message.replace(/[\n\r]/g, ' ');
|
|
53
|
+
// Step 2: 移除 Git 特殊标志(--开头的参数)
|
|
54
|
+
safeMsg = safeMsg.replace(/--[\w-]+/g, '');
|
|
55
|
+
// Step 3: 移除引号(防止参数分隔符混淆)
|
|
56
|
+
safeMsg = safeMsg.replace(/['"]/g, '');
|
|
57
|
+
// Step 4: 移除前导的 -(防止单字母标志如 -m)
|
|
58
|
+
safeMsg = safeMsg.replace(/^-+/, '');
|
|
59
|
+
// Step 5: 移除连续空格
|
|
60
|
+
safeMsg = safeMsg.replace(/\s+/g, ' ');
|
|
61
|
+
// Step 6: 长度限制
|
|
62
|
+
if (safeMsg.length > maxLength) {
|
|
63
|
+
safeMsg = safeMsg.slice(0, maxLength);
|
|
64
|
+
}
|
|
65
|
+
// Step 7: 移除首尾空格
|
|
66
|
+
safeMsg = safeMsg.trim();
|
|
67
|
+
// Step 8: 确保非空
|
|
68
|
+
if (!safeMsg) {
|
|
69
|
+
safeMsg = 'Untitled commit';
|
|
70
|
+
}
|
|
71
|
+
return safeMsg;
|
|
72
|
+
}
|
|
73
|
+
// ============================================================================
|
|
74
|
+
// Git 操作
|
|
75
|
+
// ============================================================================
|
|
76
|
+
let gitAvailable = null;
|
|
77
|
+
/**
|
|
78
|
+
* 检测 Git 是否可用
|
|
79
|
+
*/
|
|
80
|
+
export function isGitAvailable() {
|
|
81
|
+
if (gitAvailable !== null) {
|
|
82
|
+
return gitAvailable;
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
const result = spawnSync('git', ['--version'], {
|
|
86
|
+
timeout: 5000,
|
|
87
|
+
encoding: 'utf-8',
|
|
88
|
+
windowsHide: true,
|
|
89
|
+
});
|
|
90
|
+
gitAvailable = result.status === 0;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
gitAvailable = false;
|
|
94
|
+
}
|
|
95
|
+
return gitAvailable;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* 检测指定目录是否是 Git 仓库
|
|
99
|
+
*/
|
|
100
|
+
export function isGitRepo(path) {
|
|
101
|
+
if (!isGitAvailable()) {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
return existsSync(join(path, '.git'));
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* 优雅执行 Git 操作
|
|
108
|
+
*/
|
|
109
|
+
export async function gitGracefulOperation(args, cwd, fallbackMsg = 'Git 不可用,跳过版本控制操作') {
|
|
110
|
+
if (!isGitAvailable()) {
|
|
111
|
+
console.warn(`⚠️ ${fallbackMsg}`);
|
|
112
|
+
return { success: false, output: '', wasSkipped: true };
|
|
113
|
+
}
|
|
114
|
+
return new Promise((resolve) => {
|
|
115
|
+
const proc = spawn('git', args, {
|
|
116
|
+
cwd,
|
|
117
|
+
windowsHide: true,
|
|
118
|
+
});
|
|
119
|
+
let stdout = '';
|
|
120
|
+
let stderr = '';
|
|
121
|
+
proc.stdout?.setEncoding('utf-8');
|
|
122
|
+
proc.stdout?.on('data', (data) => {
|
|
123
|
+
stdout += data;
|
|
124
|
+
});
|
|
125
|
+
proc.stderr?.setEncoding('utf-8');
|
|
126
|
+
proc.stderr?.on('data', (data) => {
|
|
127
|
+
stderr += data;
|
|
128
|
+
});
|
|
129
|
+
proc.on('close', (code) => {
|
|
130
|
+
resolve({
|
|
131
|
+
success: code === 0,
|
|
132
|
+
output: stdout || stderr,
|
|
133
|
+
wasSkipped: false,
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
proc.on('error', (err) => {
|
|
137
|
+
console.warn(`⚠️ Git 操作失败: ${err.message}`);
|
|
138
|
+
resolve({
|
|
139
|
+
success: false,
|
|
140
|
+
output: err.message,
|
|
141
|
+
wasSkipped: false,
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Git add
|
|
148
|
+
*/
|
|
149
|
+
export async function gitAdd(files, cwd) {
|
|
150
|
+
return gitGracefulOperation(['add', ...files], cwd, 'Git 不可用,跳过 add 操作');
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Git commit
|
|
154
|
+
*/
|
|
155
|
+
export async function gitCommit(message, cwd) {
|
|
156
|
+
const safeMessage = sanitizeCommitMessage(message);
|
|
157
|
+
return gitGracefulOperation(['commit', '-m', safeMessage], cwd, 'Git 不可用,跳过 commit 操作');
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Git status
|
|
161
|
+
*/
|
|
162
|
+
export async function gitStatus(cwd) {
|
|
163
|
+
const result = await gitGracefulOperation(['status', '--porcelain'], cwd);
|
|
164
|
+
if (!result.success || !result.output) {
|
|
165
|
+
return [];
|
|
166
|
+
}
|
|
167
|
+
return result.output.trim().split('\n').filter(Boolean);
|
|
168
|
+
}
|
|
169
|
+
// ============================================================================
|
|
170
|
+
// 原子化文件写入
|
|
171
|
+
// ============================================================================
|
|
172
|
+
export class AtomicWriteError extends Error {
|
|
173
|
+
constructor(message) {
|
|
174
|
+
super(message);
|
|
175
|
+
this.name = 'AtomicWriteError';
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* 原子化写入 JSON 文件
|
|
180
|
+
*
|
|
181
|
+
* 实现策略:
|
|
182
|
+
* 1. 写入临时文件(同目录)
|
|
183
|
+
* 2. 可选:备份原文件
|
|
184
|
+
* 3. 原子重命名
|
|
185
|
+
*/
|
|
186
|
+
export function writeJsonAtomic(filePath, data, options) {
|
|
187
|
+
const { backup = true, indent = 2 } = options ?? {};
|
|
188
|
+
const parentDir = dirname(filePath);
|
|
189
|
+
if (!existsSync(parentDir)) {
|
|
190
|
+
mkdirSync(parentDir, { recursive: true });
|
|
191
|
+
}
|
|
192
|
+
// 准备 JSON 内容
|
|
193
|
+
let jsonContent;
|
|
194
|
+
try {
|
|
195
|
+
jsonContent = JSON.stringify(data, null, indent);
|
|
196
|
+
}
|
|
197
|
+
catch (err) {
|
|
198
|
+
throw new AtomicWriteError(`JSON 序列化失败: ${err}`);
|
|
199
|
+
}
|
|
200
|
+
// 创建临时文件
|
|
201
|
+
const tempFileName = `${basename(filePath, extname(filePath))}_${randomUUID()}.tmp`;
|
|
202
|
+
const tempPath = join(parentDir, tempFileName);
|
|
203
|
+
const backupPath = filePath + '.bak';
|
|
204
|
+
try {
|
|
205
|
+
// Step 1: 写入临时文件
|
|
206
|
+
writeFileSync(tempPath, jsonContent, 'utf-8');
|
|
207
|
+
// 确保写入磁盘
|
|
208
|
+
const fd = openSync(tempPath, 'r');
|
|
209
|
+
try {
|
|
210
|
+
fsyncSync(fd);
|
|
211
|
+
}
|
|
212
|
+
finally {
|
|
213
|
+
closeSync(fd);
|
|
214
|
+
}
|
|
215
|
+
// Step 2: 备份原文件(如果存在且启用备份)
|
|
216
|
+
if (backup && existsSync(filePath)) {
|
|
217
|
+
try {
|
|
218
|
+
copyFileSync(filePath, backupPath);
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
// 备份失败不阻止写入
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// Step 3: 原子重命名
|
|
225
|
+
renameSync(tempPath, filePath);
|
|
226
|
+
}
|
|
227
|
+
catch (err) {
|
|
228
|
+
// 清理临时文件
|
|
229
|
+
try {
|
|
230
|
+
if (existsSync(tempPath)) {
|
|
231
|
+
unlinkSync(tempPath);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
// 忽略清理错误
|
|
236
|
+
}
|
|
237
|
+
throw new AtomicWriteError(`原子写入失败: ${err}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* 安全读取 JSON 文件
|
|
242
|
+
*/
|
|
243
|
+
export function readJsonSafe(filePath, defaultValue) {
|
|
244
|
+
const fallback = defaultValue ?? {};
|
|
245
|
+
if (!existsSync(filePath)) {
|
|
246
|
+
return fallback;
|
|
247
|
+
}
|
|
248
|
+
try {
|
|
249
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
250
|
+
return JSON.parse(content);
|
|
251
|
+
}
|
|
252
|
+
catch (err) {
|
|
253
|
+
console.warn(`⚠️ 读取 JSON 失败 (${filePath}): ${err}`);
|
|
254
|
+
return fallback;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* 从备份恢复文件
|
|
259
|
+
*/
|
|
260
|
+
export function restoreFromBackup(filePath) {
|
|
261
|
+
const backupPath = filePath + '.bak';
|
|
262
|
+
if (!existsSync(backupPath)) {
|
|
263
|
+
console.warn(`⚠️ 备份文件不存在: ${backupPath}`);
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
try {
|
|
267
|
+
copyFileSync(backupPath, filePath);
|
|
268
|
+
console.log(`✅ 已从备份恢复: ${filePath}`);
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
catch (err) {
|
|
272
|
+
console.error(`❌ 恢复失败: ${err}`);
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
// ============================================================================
|
|
277
|
+
// 整数验证
|
|
278
|
+
// ============================================================================
|
|
279
|
+
/**
|
|
280
|
+
* 验证并转换整数输入
|
|
281
|
+
*/
|
|
282
|
+
export function validateIntegerInput(value, fieldName) {
|
|
283
|
+
const parsed = Number.parseInt(value, 10);
|
|
284
|
+
if (!Number.isFinite(parsed)) {
|
|
285
|
+
const error = `${fieldName} 必须是整数,收到: ${value}`;
|
|
286
|
+
console.error(`❌ 错误:${error}`);
|
|
287
|
+
throw new Error(`Invalid integer input for ${fieldName}: ${value}`);
|
|
288
|
+
}
|
|
289
|
+
return parsed;
|
|
290
|
+
}
|
|
291
|
+
//# sourceMappingURL=security.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"security.js","sourceRoot":"","sources":["../../src/utils/security.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export interface CharacterActivityResult {
|
|
2
|
+
name: string;
|
|
3
|
+
tier: string;
|
|
4
|
+
lastAppearance: number;
|
|
5
|
+
chaptersSinceLastAppearance: number;
|
|
6
|
+
status: 'healthy' | 'warning' | 'critical';
|
|
7
|
+
}
|
|
8
|
+
export interface ForeshadowingResult {
|
|
9
|
+
id: string;
|
|
10
|
+
content: string;
|
|
11
|
+
tier: string;
|
|
12
|
+
plantedChapter: number | null;
|
|
13
|
+
targetChapter: number | null;
|
|
14
|
+
elapsedChapters: number | null;
|
|
15
|
+
remainingChapters: number | null;
|
|
16
|
+
status: 'healthy' | 'warning' | 'overtime';
|
|
17
|
+
urgency: number;
|
|
18
|
+
}
|
|
19
|
+
export interface PacingResult {
|
|
20
|
+
chapterRange: string;
|
|
21
|
+
coolPointCount: number;
|
|
22
|
+
avgWordsPerCoolPoint: number;
|
|
23
|
+
rating: 'excellent' | 'good' | 'fair' | 'poor';
|
|
24
|
+
}
|
|
25
|
+
export interface BasicStats {
|
|
26
|
+
totalChapters: number;
|
|
27
|
+
totalWords: number;
|
|
28
|
+
avgWordsPerChapter: number;
|
|
29
|
+
progress: number;
|
|
30
|
+
targetWords: number;
|
|
31
|
+
}
|
|
32
|
+
export interface HealthReport {
|
|
33
|
+
generatedAt: string;
|
|
34
|
+
basicStats: BasicStats;
|
|
35
|
+
characterActivity: CharacterActivityResult[];
|
|
36
|
+
foreshadowing: ForeshadowingResult[];
|
|
37
|
+
pacing: PacingResult[];
|
|
38
|
+
}
|
|
39
|
+
export declare class StatusReporter {
|
|
40
|
+
private _projectRoot;
|
|
41
|
+
private indexManager;
|
|
42
|
+
private statePath;
|
|
43
|
+
private bodyDir;
|
|
44
|
+
/** Get the project root directory */
|
|
45
|
+
get projectRoot(): string;
|
|
46
|
+
constructor(projectRoot: string);
|
|
47
|
+
/**
|
|
48
|
+
* 生成健康报告
|
|
49
|
+
*/
|
|
50
|
+
generateHealthReport(options?: {
|
|
51
|
+
focus?: 'characters' | 'foreshadowing' | 'pacing' | 'all';
|
|
52
|
+
}): HealthReport;
|
|
53
|
+
getBasicStats(): BasicStats;
|
|
54
|
+
analyzeCharacterActivity(): CharacterActivityResult[];
|
|
55
|
+
analyzeForeshadowing(): ForeshadowingResult[];
|
|
56
|
+
analyzePacing(): PacingResult[];
|
|
57
|
+
/**
|
|
58
|
+
* 格式化为 Markdown
|
|
59
|
+
*/
|
|
60
|
+
formatAsMarkdown(report: HealthReport): string;
|
|
61
|
+
/**
|
|
62
|
+
* 保存报告到文件
|
|
63
|
+
*/
|
|
64
|
+
saveReport(report: HealthReport, outputPath: string): void;
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=status-reporter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"status-reporter.d.ts","sourceRoot":"","sources":["../../src/utils/status-reporter.ts"],"names":[],"mappings":"AAeA,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,cAAc,EAAE,MAAM,CAAC;IACvB,2BAA2B,EAAE,MAAM,CAAC;IACpC,MAAM,EAAE,SAAS,GAAG,SAAS,GAAG,UAAU,CAAC;CAC5C;AAED,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,MAAM,EAAE,SAAS,GAAG,SAAS,GAAG,UAAU,CAAC;IAC3C,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC3B,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,MAAM,EAAE,WAAW,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;CAChD;AAED,MAAM,WAAW,UAAU;IACzB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,YAAY;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,UAAU,CAAC;IACvB,iBAAiB,EAAE,uBAAuB,EAAE,CAAC;IAC7C,aAAa,EAAE,mBAAmB,EAAE,CAAC;IACrC,MAAM,EAAE,YAAY,EAAE,CAAC;CACxB;AAMD,qBAAa,cAAc;IACzB,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,OAAO,CAAS;IAExB,qCAAqC;IACrC,IAAI,WAAW,IAAI,MAAM,CAExB;gBAEW,WAAW,EAAE,MAAM;IAS/B;;OAEG;IACH,oBAAoB,CAAC,OAAO,CAAC,EAAE;QAC7B,KAAK,CAAC,EAAE,YAAY,GAAG,eAAe,GAAG,QAAQ,GAAG,KAAK,CAAC;KAC3D,GAAG,YAAY;IAoBhB,aAAa,IAAI,UAAU;IA+C3B,wBAAwB,IAAI,uBAAuB,EAAE;IA4CrD,oBAAoB,IAAI,mBAAmB,EAAE;IA2E7C,aAAa,IAAI,YAAY,EAAE;IA+D/B;;OAEG;IACH,gBAAgB,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM;IAyE9C;;OAEG;IACH,UAAU,CAAC,MAAM,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI;CAK3D"}
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 状态报告器
|
|
3
|
+
*
|
|
4
|
+
* 迁移自 Python status_reporter.py
|
|
5
|
+
* 生成健康报告:角色活跃度、伏笔状态、爽点节奏
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync } from 'fs';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
import { IndexManager } from '@changw98ic/data';
|
|
10
|
+
import { readJsonSafe } from './security.js';
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// Status Reporter
|
|
13
|
+
// ============================================================================
|
|
14
|
+
export class StatusReporter {
|
|
15
|
+
_projectRoot;
|
|
16
|
+
indexManager;
|
|
17
|
+
statePath;
|
|
18
|
+
bodyDir;
|
|
19
|
+
/** Get the project root directory */
|
|
20
|
+
get projectRoot() {
|
|
21
|
+
return this._projectRoot;
|
|
22
|
+
}
|
|
23
|
+
constructor(projectRoot) {
|
|
24
|
+
this._projectRoot = projectRoot;
|
|
25
|
+
this.indexManager = new IndexManager({ projectRoot });
|
|
26
|
+
this.statePath = join(projectRoot, '.webnovel', 'state.json');
|
|
27
|
+
this.bodyDir = join(projectRoot, '正文');
|
|
28
|
+
}
|
|
29
|
+
// ==================== Main Report ====================
|
|
30
|
+
/**
|
|
31
|
+
* 生成健康报告
|
|
32
|
+
*/
|
|
33
|
+
generateHealthReport(options) {
|
|
34
|
+
const focus = options?.focus ?? 'all';
|
|
35
|
+
return {
|
|
36
|
+
generatedAt: new Date().toISOString(),
|
|
37
|
+
basicStats: this.getBasicStats(),
|
|
38
|
+
characterActivity: focus === 'all' || focus === 'characters'
|
|
39
|
+
? this.analyzeCharacterActivity()
|
|
40
|
+
: [],
|
|
41
|
+
foreshadowing: focus === 'all' || focus === 'foreshadowing'
|
|
42
|
+
? this.analyzeForeshadowing()
|
|
43
|
+
: [],
|
|
44
|
+
pacing: focus === 'all' || focus === 'pacing'
|
|
45
|
+
? this.analyzePacing()
|
|
46
|
+
: [],
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
// ==================== Basic Stats ====================
|
|
50
|
+
getBasicStats() {
|
|
51
|
+
const state = readJsonSafe(this.statePath, {});
|
|
52
|
+
// Count chapters
|
|
53
|
+
let totalChapters = 0;
|
|
54
|
+
let totalWords = 0;
|
|
55
|
+
if (existsSync(this.bodyDir)) {
|
|
56
|
+
const files = readdirSync(this.bodyDir).filter(f => f.endsWith('.md'));
|
|
57
|
+
totalChapters = files.length;
|
|
58
|
+
for (const file of files) {
|
|
59
|
+
const filePath = join(this.bodyDir, file);
|
|
60
|
+
try {
|
|
61
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
62
|
+
// Count Chinese characters
|
|
63
|
+
const chineseChars = (content.match(/[\u4e00-\u9fff]/g) || []).length;
|
|
64
|
+
totalWords += chineseChars;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const avgWordsPerChapter = totalChapters > 0
|
|
72
|
+
? Math.round(totalWords / totalChapters)
|
|
73
|
+
: 0;
|
|
74
|
+
const targetWords = state.target_words ?? 2000000;
|
|
75
|
+
const progress = targetWords > 0
|
|
76
|
+
? Math.min(100, (totalWords / targetWords) * 100)
|
|
77
|
+
: 0;
|
|
78
|
+
return {
|
|
79
|
+
totalChapters,
|
|
80
|
+
totalWords,
|
|
81
|
+
avgWordsPerChapter,
|
|
82
|
+
progress: Math.round(progress * 10) / 10,
|
|
83
|
+
targetWords,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
// ==================== Character Activity ====================
|
|
87
|
+
analyzeCharacterActivity() {
|
|
88
|
+
const state = readJsonSafe(this.statePath, {});
|
|
89
|
+
const currentChapter = state.progress?.current_chapter ?? 0;
|
|
90
|
+
const entities = this.indexManager.getEntities({ type: '角色' });
|
|
91
|
+
const results = [];
|
|
92
|
+
for (const entity of entities) {
|
|
93
|
+
// Skip decorative characters
|
|
94
|
+
if (entity.tier === '装饰')
|
|
95
|
+
continue;
|
|
96
|
+
const lastAppearance = entity.last_appearance ?? 0;
|
|
97
|
+
const chaptersSince = currentChapter - lastAppearance;
|
|
98
|
+
let status;
|
|
99
|
+
if (entity.is_protagonist) {
|
|
100
|
+
status = chaptersSince > 5 ? 'warning' : 'healthy';
|
|
101
|
+
}
|
|
102
|
+
else if (entity.tier === '核心') {
|
|
103
|
+
status = chaptersSince > 30 ? 'critical' : chaptersSince > 15 ? 'warning' : 'healthy';
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
status = chaptersSince > 50 ? 'critical' : chaptersSince > 30 ? 'warning' : 'healthy';
|
|
107
|
+
}
|
|
108
|
+
results.push({
|
|
109
|
+
name: entity.canonical_name,
|
|
110
|
+
tier: entity.tier,
|
|
111
|
+
lastAppearance,
|
|
112
|
+
chaptersSinceLastAppearance: chaptersSince,
|
|
113
|
+
status,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
// Sort: critical first, then warning, then healthy
|
|
117
|
+
return results.sort((a, b) => {
|
|
118
|
+
const order = { critical: 0, warning: 1, healthy: 2 };
|
|
119
|
+
return order[a.status] - order[b.status];
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
// ==================== Foreshadowing ====================
|
|
123
|
+
analyzeForeshadowing() {
|
|
124
|
+
const state = readJsonSafe(this.statePath, {});
|
|
125
|
+
const currentChapter = state.progress?.current_chapter ?? 0;
|
|
126
|
+
const foreshadowing = state.plot_threads?.foreshadowing ?? [];
|
|
127
|
+
const results = [];
|
|
128
|
+
for (const item of foreshadowing) {
|
|
129
|
+
// Skip resolved items
|
|
130
|
+
if (item.status === 'resolved' || item.status === '已回收')
|
|
131
|
+
continue;
|
|
132
|
+
const plantedChapter = item.planted_chapter ?? null;
|
|
133
|
+
const targetChapter = item.target_chapter ?? null;
|
|
134
|
+
const elapsedChapters = plantedChapter
|
|
135
|
+
? currentChapter - plantedChapter
|
|
136
|
+
: null;
|
|
137
|
+
const remainingChapters = targetChapter
|
|
138
|
+
? targetChapter - currentChapter
|
|
139
|
+
: null;
|
|
140
|
+
let status;
|
|
141
|
+
if (remainingChapters !== null && remainingChapters < 0) {
|
|
142
|
+
status = 'overtime';
|
|
143
|
+
}
|
|
144
|
+
else if (elapsedChapters !== null && elapsedChapters > 100) {
|
|
145
|
+
status = 'warning';
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
status = 'healthy';
|
|
149
|
+
}
|
|
150
|
+
// Calculate urgency
|
|
151
|
+
let urgency = 0;
|
|
152
|
+
const tier = item.tier ?? '支线';
|
|
153
|
+
const tierWeight = tier === '核心' ? 3 : tier === '支线' ? 2 : 1;
|
|
154
|
+
if (elapsedChapters !== null) {
|
|
155
|
+
urgency = tierWeight * (elapsedChapters / 50);
|
|
156
|
+
}
|
|
157
|
+
if (remainingChapters !== null && remainingChapters < 20) {
|
|
158
|
+
urgency += (20 - remainingChapters) * tierWeight;
|
|
159
|
+
}
|
|
160
|
+
results.push({
|
|
161
|
+
id: item.id ?? '',
|
|
162
|
+
content: item.content ?? '[未命名伏笔]',
|
|
163
|
+
tier,
|
|
164
|
+
plantedChapter,
|
|
165
|
+
targetChapter,
|
|
166
|
+
elapsedChapters,
|
|
167
|
+
remainingChapters,
|
|
168
|
+
status,
|
|
169
|
+
urgency: Math.round(urgency * 10) / 10,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
// Sort by urgency descending
|
|
173
|
+
return results.sort((a, b) => b.urgency - a.urgency);
|
|
174
|
+
}
|
|
175
|
+
// ==================== Pacing ====================
|
|
176
|
+
analyzePacing() {
|
|
177
|
+
if (!existsSync(this.bodyDir)) {
|
|
178
|
+
return [];
|
|
179
|
+
}
|
|
180
|
+
const files = readdirSync(this.bodyDir)
|
|
181
|
+
.filter(f => f.endsWith('.md'))
|
|
182
|
+
.sort();
|
|
183
|
+
const results = [];
|
|
184
|
+
const batchSize = 50;
|
|
185
|
+
for (let i = 0; i < files.length; i += batchSize) {
|
|
186
|
+
const batch = files.slice(i, i + batchSize);
|
|
187
|
+
let totalWords = 0;
|
|
188
|
+
let coolPointCount = 0;
|
|
189
|
+
for (const file of batch) {
|
|
190
|
+
const filePath = join(this.bodyDir, file);
|
|
191
|
+
try {
|
|
192
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
193
|
+
const chineseChars = (content.match(/[\u4e00-\u9fff]/g) || []).length;
|
|
194
|
+
totalWords += chineseChars;
|
|
195
|
+
// Count cool points (爽点标记)
|
|
196
|
+
const coolMatches = content.match(/爽点[::]/g) || [];
|
|
197
|
+
coolPointCount += coolMatches.length;
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const avgWordsPerCoolPoint = coolPointCount > 0
|
|
204
|
+
? Math.round(totalWords / coolPointCount)
|
|
205
|
+
: totalWords;
|
|
206
|
+
let rating;
|
|
207
|
+
if (avgWordsPerCoolPoint <= 1200) {
|
|
208
|
+
rating = 'excellent';
|
|
209
|
+
}
|
|
210
|
+
else if (avgWordsPerCoolPoint <= 1800) {
|
|
211
|
+
rating = 'good';
|
|
212
|
+
}
|
|
213
|
+
else if (avgWordsPerCoolPoint <= 2500) {
|
|
214
|
+
rating = 'fair';
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
rating = 'poor';
|
|
218
|
+
}
|
|
219
|
+
const startChapter = i + 1;
|
|
220
|
+
const endChapter = Math.min(i + batchSize, files.length);
|
|
221
|
+
results.push({
|
|
222
|
+
chapterRange: `第 ${startChapter}-${endChapter} 章`,
|
|
223
|
+
coolPointCount,
|
|
224
|
+
avgWordsPerCoolPoint,
|
|
225
|
+
rating,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
return results;
|
|
229
|
+
}
|
|
230
|
+
// ==================== Output ====================
|
|
231
|
+
/**
|
|
232
|
+
* 格式化为 Markdown
|
|
233
|
+
*/
|
|
234
|
+
formatAsMarkdown(report) {
|
|
235
|
+
const lines = [];
|
|
236
|
+
lines.push('# 📊 全书健康报告');
|
|
237
|
+
lines.push('');
|
|
238
|
+
lines.push(`生成时间: ${report.generatedAt}`);
|
|
239
|
+
lines.push('');
|
|
240
|
+
// 基本统计
|
|
241
|
+
lines.push('## 📈 基本数据');
|
|
242
|
+
lines.push('');
|
|
243
|
+
const stats = report.basicStats;
|
|
244
|
+
lines.push(`- **总章节数**: ${stats.totalChapters} 章`);
|
|
245
|
+
lines.push(`- **总字数**: ${stats.totalWords.toLocaleString()} 字`);
|
|
246
|
+
lines.push(`- **平均章节字数**: ${stats.avgWordsPerChapter.toLocaleString()} 字`);
|
|
247
|
+
lines.push(`- **创作进度**: ${stats.progress}%(目标 ${stats.targetWords.toLocaleString()} 字)`);
|
|
248
|
+
lines.push('');
|
|
249
|
+
// 角色活跃度
|
|
250
|
+
if (report.characterActivity.length > 0) {
|
|
251
|
+
const dropped = report.characterActivity.filter(c => c.status !== 'healthy');
|
|
252
|
+
if (dropped.length > 0) {
|
|
253
|
+
lines.push(`## ⚠️ 角色掉线(${dropped.length}人)`);
|
|
254
|
+
lines.push('');
|
|
255
|
+
lines.push('| 角色 | 层级 | 最后出场 | 缺席章节 | 状态 |');
|
|
256
|
+
lines.push('|------|------|---------|---------|------|');
|
|
257
|
+
for (const c of dropped) {
|
|
258
|
+
const statusIcon = c.status === 'critical' ? '🔴' : '🟡';
|
|
259
|
+
const statusText = c.status === 'critical' ? '严重掉线' : '轻度掉线';
|
|
260
|
+
lines.push(`| ${c.name} | ${c.tier} | 第 ${c.lastAppearance} 章 | ${c.chaptersSinceLastAppearance} 章 | ${statusIcon} ${statusText} |`);
|
|
261
|
+
}
|
|
262
|
+
lines.push('');
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
// 伏笔状态
|
|
266
|
+
if (report.foreshadowing.length > 0) {
|
|
267
|
+
const overtime = report.foreshadowing.filter(f => f.status === 'overtime');
|
|
268
|
+
if (overtime.length > 0) {
|
|
269
|
+
lines.push(`## ⚠️ 伏笔超时(${overtime.length}条)`);
|
|
270
|
+
lines.push('');
|
|
271
|
+
lines.push('| 伏笔内容 | 埋设章节 | 已过章节 | 状态 |');
|
|
272
|
+
lines.push('|---------|---------|---------|------|');
|
|
273
|
+
for (const f of overtime) {
|
|
274
|
+
lines.push(`| ${f.content.slice(0, 30)}... | 第 ${f.plantedChapter} 章 | ${f.elapsedChapters} 章 | 🔴 严重超时 |`);
|
|
275
|
+
}
|
|
276
|
+
lines.push('');
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
// 爽点节奏
|
|
280
|
+
if (report.pacing.length > 0) {
|
|
281
|
+
lines.push('## 📈 爽点节奏分布');
|
|
282
|
+
lines.push('');
|
|
283
|
+
lines.push('```');
|
|
284
|
+
for (const p of report.pacing) {
|
|
285
|
+
const barLength = Math.round((3000 - p.avgWordsPerCoolPoint) / 150);
|
|
286
|
+
const bar = '█'.repeat(Math.max(1, barLength));
|
|
287
|
+
const ratingText = {
|
|
288
|
+
excellent: '优秀',
|
|
289
|
+
good: '良好',
|
|
290
|
+
fair: '良好',
|
|
291
|
+
poor: '偏低 ⚠️',
|
|
292
|
+
}[p.rating];
|
|
293
|
+
lines.push(`${p.chapterRange.padEnd(12)} ${bar} ${ratingText}(${p.avgWordsPerCoolPoint}字/爽点)`);
|
|
294
|
+
}
|
|
295
|
+
lines.push('```');
|
|
296
|
+
lines.push('');
|
|
297
|
+
}
|
|
298
|
+
return lines.join('\n');
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* 保存报告到文件
|
|
302
|
+
*/
|
|
303
|
+
saveReport(report, outputPath) {
|
|
304
|
+
const markdown = this.formatAsMarkdown(report);
|
|
305
|
+
writeFileSync(outputPath, markdown, 'utf-8');
|
|
306
|
+
console.log(`✅ 报告已保存: ${outputPath}`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
//# sourceMappingURL=status-reporter.js.map
|