principles-disciple 1.5.4 → 1.6.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/dist/commands/context.d.ts +5 -0
- package/dist/commands/context.js +308 -0
- package/dist/commands/focus.d.ts +14 -0
- package/dist/commands/focus.js +579 -0
- package/dist/commands/pain.js +135 -6
- package/dist/commands/rollback.d.ts +19 -0
- package/dist/commands/rollback.js +119 -0
- package/dist/core/config.d.ts +32 -0
- package/dist/core/config.js +47 -0
- package/dist/core/event-log.d.ts +21 -1
- package/dist/core/event-log.js +316 -0
- package/dist/core/focus-history.d.ts +65 -0
- package/dist/core/focus-history.js +266 -0
- package/dist/core/init.js +30 -7
- package/dist/core/migration.js +0 -2
- package/dist/core/path-resolver.d.ts +3 -0
- package/dist/core/path-resolver.js +20 -0
- package/dist/hooks/gate.js +203 -1
- package/dist/hooks/llm.d.ts +8 -0
- package/dist/hooks/llm.js +234 -1
- package/dist/hooks/message-sanitize.d.ts +3 -0
- package/dist/hooks/message-sanitize.js +37 -0
- package/dist/hooks/prompt.d.ts +12 -0
- package/dist/hooks/prompt.js +309 -135
- package/dist/hooks/subagent.d.ts +9 -2
- package/dist/hooks/subagent.js +13 -2
- package/dist/i18n/commands.js +32 -20
- package/dist/index.js +181 -4
- package/dist/service/empathy-observer-manager.d.ts +42 -0
- package/dist/service/empathy-observer-manager.js +147 -0
- package/dist/service/evolution-worker.d.ts +1 -0
- package/dist/service/evolution-worker.js +4 -2
- package/dist/tools/deep-reflect.js +80 -0
- package/dist/types/event-types.d.ts +77 -2
- package/dist/types/event-types.js +33 -0
- package/dist/types.d.ts +42 -0
- package/dist/types.js +19 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +3 -3
- package/templates/langs/zh/core/HEARTBEAT.md +28 -4
- package/templates/pain_settings.json +54 -2
- package/templates/workspace/.principles/PROFILE.json +2 -0
- package/templates/workspace/okr/CURRENT_FOCUS.md +57 -0
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /pd-focus 命令 - 管理 CURRENT_FOCUS.md
|
|
3
|
+
*
|
|
4
|
+
* 功能:
|
|
5
|
+
* - status: 查看当前状态和历史版本
|
|
6
|
+
* - compress: 手动压缩并备份
|
|
7
|
+
* - history: 查看历史版本列表
|
|
8
|
+
* - rollback: 回滚到指定历史版本
|
|
9
|
+
*/
|
|
10
|
+
import * as fs from 'fs';
|
|
11
|
+
import * as path from 'path';
|
|
12
|
+
import { WorkspaceContext } from '../core/workspace-context.js';
|
|
13
|
+
import { getHistoryDir, backupToHistory, cleanupHistory, extractVersion, extractDate, } from '../core/focus-history.js';
|
|
14
|
+
import { agentSpawnTool } from '../tools/agent-spawn.js';
|
|
15
|
+
/**
|
|
16
|
+
* 清理 Markdown 代码块围栏
|
|
17
|
+
* 移除开头的 ```lang 和结尾的 ```
|
|
18
|
+
*/
|
|
19
|
+
function stripMarkdownFence(content) {
|
|
20
|
+
let result = content.trim();
|
|
21
|
+
// 移除开头的代码块标记(如 ```markdown, ```text 等)
|
|
22
|
+
result = result.replace(/^```[\w]*\n?/, '');
|
|
23
|
+
// 移除结尾的代码块标记
|
|
24
|
+
result = result.replace(/\n?```$/, '');
|
|
25
|
+
return result.trim();
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* 获取工作区目录
|
|
29
|
+
*/
|
|
30
|
+
function getWorkspaceDir(ctx) {
|
|
31
|
+
const workspaceDir = ctx.config?.workspaceDir;
|
|
32
|
+
if (!workspaceDir) {
|
|
33
|
+
throw new Error('[PD:Focus] workspaceDir is required but not provided');
|
|
34
|
+
}
|
|
35
|
+
return workspaceDir;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* 压缩 CURRENT_FOCUS内容
|
|
39
|
+
*
|
|
40
|
+
* 规则:
|
|
41
|
+
* - 保留:标题、元数据、状态快照
|
|
42
|
+
* - 保留:下一步章节(完整)
|
|
43
|
+
* - 保留:当前任务中未完成的项(- [ ])
|
|
44
|
+
* - 移除:当前任务中已完成的项(- [x])超过 3 个时
|
|
45
|
+
* - 移除:P0 章节如果全部完成
|
|
46
|
+
* - 保留:参考章节
|
|
47
|
+
*/
|
|
48
|
+
function compressFocusContent(content) {
|
|
49
|
+
const lines = content.split('\n');
|
|
50
|
+
const result = [];
|
|
51
|
+
let currentSection = '';
|
|
52
|
+
let inP0Section = false;
|
|
53
|
+
let p0AllCompleted = true;
|
|
54
|
+
let p0Lines = [];
|
|
55
|
+
let completedCount = 0;
|
|
56
|
+
// 辅助函数:刷新 P0 章节缓存
|
|
57
|
+
const flushP0Lines = (skipIfCompleted) => {
|
|
58
|
+
if (inP0Section && p0Lines.length > 0) {
|
|
59
|
+
if (!skipIfCompleted || !p0AllCompleted) {
|
|
60
|
+
// P0 有未完成任务,保留 P0 内容
|
|
61
|
+
result.push(...p0Lines);
|
|
62
|
+
}
|
|
63
|
+
// 重置状态
|
|
64
|
+
p0Lines = [];
|
|
65
|
+
inP0Section = false;
|
|
66
|
+
p0AllCompleted = true;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
for (let i = 0; i < lines.length; i++) {
|
|
70
|
+
const line = lines[i];
|
|
71
|
+
const trimmedLine = line.trim();
|
|
72
|
+
// 识别章节
|
|
73
|
+
if (/^#{1,3}\s*.*状态快照|📍/.test(trimmedLine)) {
|
|
74
|
+
flushP0Lines(true); // P0 完成时跳过
|
|
75
|
+
currentSection = 'snapshot';
|
|
76
|
+
}
|
|
77
|
+
else if (/^###\s*P0/i.test(trimmedLine)) {
|
|
78
|
+
currentSection = 'current_p0';
|
|
79
|
+
inP0Section = true;
|
|
80
|
+
p0AllCompleted = true;
|
|
81
|
+
p0Lines = [line];
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
else if (/^###\s*P[1-9]/i.test(trimmedLine)) {
|
|
85
|
+
// 离开 P0 章节
|
|
86
|
+
flushP0Lines(true); // P0 完成时跳过,未完成时保留
|
|
87
|
+
currentSection = 'current';
|
|
88
|
+
result.push(line);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
else if (/^#{1,3}\s*.*当前任务|🔄/.test(trimmedLine)) {
|
|
92
|
+
flushP0Lines(true); // P0 完成时跳过
|
|
93
|
+
currentSection = 'current';
|
|
94
|
+
}
|
|
95
|
+
else if (/^#{1,3}\s*.*下一步|➡️/.test(trimmedLine)) {
|
|
96
|
+
flushP0Lines(false); // 保留未完成的 P0
|
|
97
|
+
currentSection = 'nextSteps';
|
|
98
|
+
}
|
|
99
|
+
else if (/^#{1,3}\s*.*参考|📎/.test(trimmedLine)) {
|
|
100
|
+
flushP0Lines(false); // 保留未完成的 P0
|
|
101
|
+
currentSection = 'reference';
|
|
102
|
+
}
|
|
103
|
+
// 处理P0章节
|
|
104
|
+
if (inP0Section) {
|
|
105
|
+
p0Lines.push(line);
|
|
106
|
+
// 检查是否有未完成任务
|
|
107
|
+
if (/^-\s*\[\s*\]/.test(trimmedLine)) {
|
|
108
|
+
p0AllCompleted = false;
|
|
109
|
+
}
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
// 处理当前任务章节中已完成的项
|
|
113
|
+
if (currentSection === 'current') {
|
|
114
|
+
if (/^-\s*\[x\]/i.test(trimmedLine)) {
|
|
115
|
+
completedCount++;
|
|
116
|
+
// 如果已完成项超过 3 个,跳过
|
|
117
|
+
if (completedCount > 3) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
result.push(line);
|
|
123
|
+
}
|
|
124
|
+
// 循环结束后,刷新剩余的 P0 章节
|
|
125
|
+
flushP0Lines(false); // 保留未完成的 P0
|
|
126
|
+
return result.join('\n');
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* 显示 CURRENT_FOCUS 状态
|
|
130
|
+
*/
|
|
131
|
+
function showStatus(workspaceDir, isZh) {
|
|
132
|
+
const wctx = WorkspaceContext.fromHookContext({ workspaceDir });
|
|
133
|
+
const focusPath = wctx.resolve('CURRENT_FOCUS');
|
|
134
|
+
const historyDir = getHistoryDir(focusPath);
|
|
135
|
+
if (!fs.existsSync(focusPath)) {
|
|
136
|
+
return isZh
|
|
137
|
+
? '⚠️ CURRENT_FOCUS.md 不存在\n\n💡 请先运行 `/pd-init` 初始化工作区'
|
|
138
|
+
: '⚠️ CURRENT_FOCUS.md does not exist\n\n💡 Run `/pd-init` to initialize workspace first';
|
|
139
|
+
}
|
|
140
|
+
const content = fs.readFileSync(focusPath, 'utf-8');
|
|
141
|
+
const version = extractVersion(content);
|
|
142
|
+
const date = extractDate(content);
|
|
143
|
+
const lines = content.split('\n').length;
|
|
144
|
+
// 统计历史版本
|
|
145
|
+
let historyCount = 0;
|
|
146
|
+
if (fs.existsSync(historyDir)) {
|
|
147
|
+
historyCount = fs.readdirSync(historyDir).filter(f => f.startsWith('CURRENT_FOCUS.v')).length;
|
|
148
|
+
}
|
|
149
|
+
if (isZh) {
|
|
150
|
+
return `📄 **CURRENT_FOCUS.md 状态**
|
|
151
|
+
|
|
152
|
+
| 属性 | 值 |
|
|
153
|
+
|------|-----|
|
|
154
|
+
| 版本 | v${version} |
|
|
155
|
+
| 更新日期 | ${date} |
|
|
156
|
+
| 行数 | ${lines} 行 |
|
|
157
|
+
| 历史版本 | ${historyCount} 个 |
|
|
158
|
+
|
|
159
|
+
💡 输入 \`/pd-focus history\` 查看历史版本
|
|
160
|
+
💡 输入 \`/pd-focus compress\` 手动压缩`;
|
|
161
|
+
}
|
|
162
|
+
return `📄 **CURRENT_FOCUS.md Status**
|
|
163
|
+
|
|
164
|
+
| Property | Value |
|
|
165
|
+
|----------|-------|
|
|
166
|
+
| Version | v${version} |
|
|
167
|
+
| Updated | ${date} |
|
|
168
|
+
| Lines | ${lines} lines |
|
|
169
|
+
| History | ${historyCount} versions |
|
|
170
|
+
|
|
171
|
+
💡 Type \`/pd-focus history\` to view history
|
|
172
|
+
💡 Type \`/pd-focus compress\` to compress manually`;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* 显示历史版本列表
|
|
176
|
+
*/
|
|
177
|
+
function showHistory(workspaceDir, isZh) {
|
|
178
|
+
const wctx = WorkspaceContext.fromHookContext({ workspaceDir });
|
|
179
|
+
const focusPath = wctx.resolve('CURRENT_FOCUS');
|
|
180
|
+
const historyDir = getHistoryDir(focusPath);
|
|
181
|
+
if (!fs.existsSync(historyDir)) {
|
|
182
|
+
return isZh
|
|
183
|
+
? '📭 暂无历史版本\n\n💡 历史版本在压缩 CURRENT_FOCUS.md 时自动创建'
|
|
184
|
+
: '📭 No history versions yet\n\n💡 History is created when CURRENT_FOCUS.md is compressed';
|
|
185
|
+
}
|
|
186
|
+
const files = fs.readdirSync(historyDir)
|
|
187
|
+
.filter(f => f.startsWith('CURRENT_FOCUS.v') && f.endsWith('.md'))
|
|
188
|
+
.map(f => {
|
|
189
|
+
const filePath = path.join(historyDir, f);
|
|
190
|
+
const stat = fs.statSync(filePath);
|
|
191
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
192
|
+
return {
|
|
193
|
+
name: f,
|
|
194
|
+
version: extractVersion(content),
|
|
195
|
+
date: extractDate(content),
|
|
196
|
+
mtime: stat.mtime,
|
|
197
|
+
lines: content.split('\n').length,
|
|
198
|
+
};
|
|
199
|
+
})
|
|
200
|
+
.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
|
201
|
+
if (files.length === 0) {
|
|
202
|
+
return isZh ? '📭 暂无历史版本' : '📭 No history versions';
|
|
203
|
+
}
|
|
204
|
+
const lines = files.slice(0, 10).map((f, i) => {
|
|
205
|
+
const num = i + 1;
|
|
206
|
+
return isZh
|
|
207
|
+
? `${num}. \`${f.name}\` - v${f.version} (${f.date}, ${f.lines} 行)`
|
|
208
|
+
: `${num}. \`${f.name}\` - v${f.version} (${f.date}, ${f.lines} lines)`;
|
|
209
|
+
});
|
|
210
|
+
const header = isZh
|
|
211
|
+
? `📚 **历史版本列表** (最近 ${Math.min(files.length, 10)} 个)\n`
|
|
212
|
+
: `📚 **History Versions** (Last ${Math.min(files.length, 10)})\n`;
|
|
213
|
+
const footer = isZh
|
|
214
|
+
? `\n\n💡 输入 \`/pd-focus rollback <序号>\` 回滚到指定版本`
|
|
215
|
+
: `\n\n💡 Type \`/pd-focus rollback <number>\` to rollback`;
|
|
216
|
+
return header + '\n' + lines.join('\n') + footer;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* 手动压缩 CURRENT_FOCUS.md(使用子智能体)
|
|
220
|
+
*/
|
|
221
|
+
async function compressFocus(workspaceDir, isZh, api) {
|
|
222
|
+
const wctx = WorkspaceContext.fromHookContext({ workspaceDir });
|
|
223
|
+
const focusPath = wctx.resolve('CURRENT_FOCUS');
|
|
224
|
+
if (!fs.existsSync(focusPath)) {
|
|
225
|
+
return isZh
|
|
226
|
+
? '❌ CURRENT_FOCUS.md 不存在'
|
|
227
|
+
: '❌ CURRENT_FOCUS.md does not exist';
|
|
228
|
+
}
|
|
229
|
+
const oldContent = fs.readFileSync(focusPath, 'utf-8');
|
|
230
|
+
const oldVersion = extractVersion(oldContent);
|
|
231
|
+
const oldDate = extractDate(oldContent);
|
|
232
|
+
const oldLines = oldContent.split('\n').length;
|
|
233
|
+
// 检查是否需要压缩
|
|
234
|
+
if (oldLines <= 40) {
|
|
235
|
+
return isZh
|
|
236
|
+
? `✅ 当前文件仅 ${oldLines} 行,无需压缩\n\n💡 文件少于 40 行时不建议压缩`
|
|
237
|
+
: `✅ Current file has only ${oldLines} lines, no need to compress\n\n💡 Compression not recommended for files under 40 lines`;
|
|
238
|
+
}
|
|
239
|
+
// 备份当前版本
|
|
240
|
+
const backupPath = backupToHistory(focusPath, oldContent);
|
|
241
|
+
// 清理过期历史
|
|
242
|
+
cleanupHistory(focusPath);
|
|
243
|
+
// 使用子智能体进行智能压缩
|
|
244
|
+
// 获取 MEMORY.md 路径
|
|
245
|
+
const memoryPath = wctx.resolve('MEMORY_MD');
|
|
246
|
+
const compressPrompt = isZh
|
|
247
|
+
? `你是一个专业的项目文档压缩助手。请压缩以下 CURRENT_FOCUS.md 文件内容。
|
|
248
|
+
|
|
249
|
+
**重要:在压缩前,你需要提取重要里程碑信息!**
|
|
250
|
+
|
|
251
|
+
**第一步:提取里程碑**
|
|
252
|
+
从原始内容中识别已完成的里程碑,这些信息需要保存到记忆文件中。
|
|
253
|
+
|
|
254
|
+
**第二步:压缩文件**
|
|
255
|
+
压缩规则:
|
|
256
|
+
1. 保留标题、元数据行(版本、状态、日期)
|
|
257
|
+
2. 保留"📍 状态快照"章节(完整)
|
|
258
|
+
3. 保留"➡️ 下一步"章节(完整,这是最重要的信息)
|
|
259
|
+
4. 保留"📎 参考"章节(完整)
|
|
260
|
+
5. 对于"🔄 当前任务"章节:
|
|
261
|
+
- 如果 P0/P1 等子章节全部完成,合并为简短"已完成里程碑"列表(最多5项)
|
|
262
|
+
- 保留所有未完成任务(- [ ])
|
|
263
|
+
- 已完成任务最多保留 3 个最近的,其余移除
|
|
264
|
+
6. 移除重复信息和冗余描述
|
|
265
|
+
7. 保持 Markdown 格式和语义连贯
|
|
266
|
+
|
|
267
|
+
**目标:** 将文件压缩到 40 行以内。
|
|
268
|
+
|
|
269
|
+
**原始内容:**
|
|
270
|
+
\`\`\`markdown
|
|
271
|
+
${oldContent}
|
|
272
|
+
\`\`\`
|
|
273
|
+
|
|
274
|
+
**输出格式(必须严格遵循):**
|
|
275
|
+
\`\`\`
|
|
276
|
+
===MEMORY===
|
|
277
|
+
[需要追加到 memory/MEMORY.md 的里程碑内容,格式:]
|
|
278
|
+
## {YYYY-MM-DD} 里程碑
|
|
279
|
+
- [里程碑1]
|
|
280
|
+
- [里程碑2]
|
|
281
|
+
...
|
|
282
|
+
===COMPRESSED===
|
|
283
|
+
[压缩后的 CURRENT_FOCUS.md 内容]
|
|
284
|
+
\`\`\`
|
|
285
|
+
|
|
286
|
+
如果没有需要记录的里程碑,===MEMORY=== 部分留空。`
|
|
287
|
+
: `You are a professional project document compression assistant. Please compress the following CURRENT_FOCUS.md file.
|
|
288
|
+
|
|
289
|
+
**IMPORTANT: Extract milestones before compression!**
|
|
290
|
+
|
|
291
|
+
**Step 1: Extract Milestones**
|
|
292
|
+
Identify completed milestones from the original content that should be saved to memory.
|
|
293
|
+
|
|
294
|
+
**Step 2: Compress File**
|
|
295
|
+
Compression Rules:
|
|
296
|
+
1. Keep title, metadata lines (version, status, date)
|
|
297
|
+
2. Keep "📍 Status Snapshot" section (complete)
|
|
298
|
+
3. Keep "➡️ Next Steps" section (complete, this is the most important)
|
|
299
|
+
4. Keep "📎 References" section (complete)
|
|
300
|
+
5. For "🔄 Current Tasks" section:
|
|
301
|
+
- If P0/P1 subsections are all completed, merge into a short "Completed Milestones" list (max 5 items)
|
|
302
|
+
- Keep all incomplete tasks (- [ ])
|
|
303
|
+
- Keep at most 3 recent completed tasks, remove the rest
|
|
304
|
+
6. Remove duplicate info and redundant descriptions
|
|
305
|
+
7. Maintain Markdown format and semantic coherence
|
|
306
|
+
|
|
307
|
+
**Goal:** Compress the file to under 40 lines.
|
|
308
|
+
|
|
309
|
+
**Original Content:**
|
|
310
|
+
\`\`\`markdown
|
|
311
|
+
${oldContent}
|
|
312
|
+
\`\`\`
|
|
313
|
+
|
|
314
|
+
**Output Format (must follow strictly):**
|
|
315
|
+
\`\`\`
|
|
316
|
+
===MEMORY===
|
|
317
|
+
[Content to append to memory/MEMORY.md, format:]
|
|
318
|
+
## {YYYY-MM-DD} Milestones
|
|
319
|
+
- [milestone 1]
|
|
320
|
+
- [milestone 2]
|
|
321
|
+
...
|
|
322
|
+
===COMPRESSED===
|
|
323
|
+
[Compressed CURRENT_FOCUS.md content]
|
|
324
|
+
\`\`\`
|
|
325
|
+
|
|
326
|
+
If no milestones to record, leave ===MEMORY=== section empty.`;
|
|
327
|
+
let compressedContent;
|
|
328
|
+
let usedAI = false;
|
|
329
|
+
let memoryUpdated = false;
|
|
330
|
+
try {
|
|
331
|
+
// 调用子智能体进行压缩
|
|
332
|
+
const result = await agentSpawnTool.execute({
|
|
333
|
+
agentType: 'reporter', // 使用 reporter 类型,适合总结和压缩
|
|
334
|
+
task: compressPrompt,
|
|
335
|
+
}, api);
|
|
336
|
+
// 解析输出,提取 MEMORY 和 COMPRESSED 部分
|
|
337
|
+
if (result && result.trim()) {
|
|
338
|
+
const memoryMatch = result.match(/===MEMORY===([\s\S]*?)===COMPRESSED===/);
|
|
339
|
+
const compressedMatch = result.match(/===COMPRESSED===([\s\S]*?)$/);
|
|
340
|
+
if (compressedMatch && compressedMatch[1].trim()) {
|
|
341
|
+
// 清理 Markdown 代码块围栏
|
|
342
|
+
compressedContent = stripMarkdownFence(compressedMatch[1]);
|
|
343
|
+
usedAI = true;
|
|
344
|
+
// 写入记忆文件(MEMORY.md 在根目录,无需创建目录)
|
|
345
|
+
if (memoryMatch && memoryMatch[1].trim()) {
|
|
346
|
+
// 清理 Markdown 代码块围栏
|
|
347
|
+
const memoryContent = stripMarkdownFence(memoryMatch[1]);
|
|
348
|
+
// 追加到 MEMORY.md
|
|
349
|
+
const existingMemory = fs.existsSync(memoryPath)
|
|
350
|
+
? fs.readFileSync(memoryPath, 'utf-8')
|
|
351
|
+
: '';
|
|
352
|
+
const newMemory = existingMemory
|
|
353
|
+
? `${existingMemory}\n\n${memoryContent}`
|
|
354
|
+
: memoryContent;
|
|
355
|
+
fs.writeFileSync(memoryPath, newMemory, 'utf-8');
|
|
356
|
+
memoryUpdated = true;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
// 无法解析输出,回退到简单压缩
|
|
361
|
+
compressedContent = compressFocusContent(oldContent);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
// 子智能体返回空,回退到简单压缩
|
|
366
|
+
compressedContent = compressFocusContent(oldContent);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
catch (error) {
|
|
370
|
+
// 子智能体失败,回退到简单压缩
|
|
371
|
+
api.logger?.error(`[PD:Focus] AI compression failed, falling back to simple compression: ${String(error)}`);
|
|
372
|
+
compressedContent = compressFocusContent(oldContent);
|
|
373
|
+
}
|
|
374
|
+
// 更新版本号和日期
|
|
375
|
+
const versionParts = oldVersion.split('.');
|
|
376
|
+
const majorVersion = parseInt(versionParts[0], 10) || 1;
|
|
377
|
+
const newVersion = `${majorVersion + 1}`;
|
|
378
|
+
const today = new Date().toISOString().split('T')[0];
|
|
379
|
+
const newContent = compressedContent
|
|
380
|
+
.replace(/\*\*版本\*\*:\s*v[\d.]+/i, `**版本**: v${newVersion}`)
|
|
381
|
+
.replace(/\*\*更新\*\*:\s*\d{4}-\d{2}-\d{2}/, `**更新**: ${today}`);
|
|
382
|
+
const newLines = newContent.split('\n').length;
|
|
383
|
+
const savedLines = oldLines - newLines;
|
|
384
|
+
fs.writeFileSync(focusPath, newContent, 'utf-8');
|
|
385
|
+
const methodNote = usedAI
|
|
386
|
+
? isZh
|
|
387
|
+
? '🤖 使用 AI 智能压缩'
|
|
388
|
+
: '🤖 AI-powered compression'
|
|
389
|
+
: isZh
|
|
390
|
+
? '📋 使用规则压缩'
|
|
391
|
+
: '📋 Rule-based compression';
|
|
392
|
+
const memoryNote = memoryUpdated
|
|
393
|
+
? isZh
|
|
394
|
+
? '📝 已将里程碑写入 MEMORY.md'
|
|
395
|
+
: '📝 Milestones saved to MEMORY.md'
|
|
396
|
+
: '';
|
|
397
|
+
if (isZh) {
|
|
398
|
+
return `✅ **压缩完成**
|
|
399
|
+
|
|
400
|
+
| 操作 | 详情 |
|
|
401
|
+
|------|------|
|
|
402
|
+
| 旧版本 | v${oldVersion} (${oldDate}) |
|
|
403
|
+
| 新版本 | v${newVersion} (${today}) |
|
|
404
|
+
| 压缩前 | ${oldLines} 行 |
|
|
405
|
+
| 压缩后 | ${newLines} 行 |
|
|
406
|
+
| 节省 | ${savedLines} 行 |
|
|
407
|
+
| 备份文件 | ${backupPath ? path.basename(backupPath) : '已存在'} |
|
|
408
|
+
|
|
409
|
+
${methodNote}${memoryNote ? `\n${memoryNote}` : ''}
|
|
410
|
+
|
|
411
|
+
💡 已压缩版本已备份到历史目录
|
|
412
|
+
💡 输入 \`/pd-focus history\` 查看所有历史版本`;
|
|
413
|
+
}
|
|
414
|
+
return `✅ **Compression Complete**
|
|
415
|
+
|
|
416
|
+
| Action | Details |
|
|
417
|
+
|--------|---------|
|
|
418
|
+
| Old Version | v${oldVersion} (${oldDate}) |
|
|
419
|
+
| New Version | v${newVersion} (${today}) |
|
|
420
|
+
| Before | ${oldLines} lines |
|
|
421
|
+
| After | ${newLines} lines |
|
|
422
|
+
| Saved | ${savedLines} lines |
|
|
423
|
+
| Backup File | ${backupPath ? path.basename(backupPath) : 'exists'} |
|
|
424
|
+
|
|
425
|
+
${methodNote}${memoryNote ? `\n${memoryNote}` : ''}
|
|
426
|
+
|
|
427
|
+
💡 Compressed version backed up to history
|
|
428
|
+
💡 Type \`/pd-focus history\` to view all versions`;
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* 回滚到历史版本
|
|
432
|
+
*/
|
|
433
|
+
function rollbackFocus(workspaceDir, index, isZh) {
|
|
434
|
+
const wctx = WorkspaceContext.fromHookContext({ workspaceDir });
|
|
435
|
+
const focusPath = wctx.resolve('CURRENT_FOCUS');
|
|
436
|
+
const historyDir = getHistoryDir(focusPath);
|
|
437
|
+
if (!fs.existsSync(historyDir)) {
|
|
438
|
+
return isZh ? '❌ 暂无历史版本可回滚' : '❌ No history versions to rollback';
|
|
439
|
+
}
|
|
440
|
+
const files = fs.readdirSync(historyDir)
|
|
441
|
+
.filter(f => f.startsWith('CURRENT_FOCUS.v') && f.endsWith('.md'))
|
|
442
|
+
.map(f => ({
|
|
443
|
+
name: f,
|
|
444
|
+
path: path.join(historyDir, f),
|
|
445
|
+
mtime: fs.statSync(path.join(historyDir, f)).mtime.getTime(),
|
|
446
|
+
}))
|
|
447
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
448
|
+
if (index < 1 || index > files.length) {
|
|
449
|
+
return isZh
|
|
450
|
+
? `❌ 无效的序号: ${index}\n\n💡 请输入 1-${files.length} 之间的数字`
|
|
451
|
+
: `❌ Invalid index: ${index}\n\n💡 Please enter a number between 1-${files.length}`;
|
|
452
|
+
}
|
|
453
|
+
const targetFile = files[index - 1];
|
|
454
|
+
const historyContent = fs.readFileSync(targetFile.path, 'utf-8');
|
|
455
|
+
// 备份当前版本
|
|
456
|
+
const currentContent = fs.existsSync(focusPath)
|
|
457
|
+
? fs.readFileSync(focusPath, 'utf-8')
|
|
458
|
+
: '';
|
|
459
|
+
if (currentContent) {
|
|
460
|
+
backupToHistory(focusPath, currentContent);
|
|
461
|
+
}
|
|
462
|
+
// 恢复历史版本
|
|
463
|
+
const restoredVersion = extractVersion(historyContent);
|
|
464
|
+
const restoredDate = extractDate(historyContent);
|
|
465
|
+
const today = new Date().toISOString().split('T')[0];
|
|
466
|
+
// 获取最大版本号(从当前文件或历史文件中)
|
|
467
|
+
let maxVersion = parseFloat(restoredVersion) || 1;
|
|
468
|
+
if (currentContent) {
|
|
469
|
+
const currentVersion = parseFloat(extractVersion(currentContent)) || 1;
|
|
470
|
+
if (currentVersion > maxVersion) {
|
|
471
|
+
maxVersion = currentVersion;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
// 正常递增版本号
|
|
475
|
+
const newVersion = `${maxVersion + 1}`;
|
|
476
|
+
// 添加回滚标记到状态字段
|
|
477
|
+
const restoredContent = historyContent
|
|
478
|
+
.replace(/\*\*版本\*\*:\s*v[\d.]+/i, `**版本**: v${newVersion}`)
|
|
479
|
+
.replace(/\*\*更新\*\*:\s*\d{4}-\d{2}-\d{2}/, `**更新**: ${today}`)
|
|
480
|
+
.replace(/\*\*状态\*\*:\s*[A-Z]+/i, `**状态**: ROLLBACK (from v${restoredVersion})`);
|
|
481
|
+
fs.writeFileSync(focusPath, restoredContent, 'utf-8');
|
|
482
|
+
if (isZh) {
|
|
483
|
+
return `✅ **回滚成功**
|
|
484
|
+
|
|
485
|
+
| 操作 | 详情 |
|
|
486
|
+
|------|------|
|
|
487
|
+
| 恢复自 | ${targetFile.name} |
|
|
488
|
+
| 原版本 | v${restoredVersion} (${restoredDate}) |
|
|
489
|
+
| 新版本 | v${newVersion} (${today}) |
|
|
490
|
+
|
|
491
|
+
💡 当前版本已备份,可再次回滚`;
|
|
492
|
+
}
|
|
493
|
+
return `✅ **Rollback Complete**
|
|
494
|
+
|
|
495
|
+
| Action | Details |
|
|
496
|
+
|--------|---------|
|
|
497
|
+
| Restored From | ${targetFile.name} |
|
|
498
|
+
| Original Version | v${restoredVersion} (${restoredDate}) |
|
|
499
|
+
| New Version | v${newVersion} (${today}) |
|
|
500
|
+
|
|
501
|
+
💡 Current version backed up, you can rollback again`;
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* 显示帮助
|
|
505
|
+
*/
|
|
506
|
+
function showHelp(isZh) {
|
|
507
|
+
if (isZh) {
|
|
508
|
+
return `📖 **/pd-focus 命令帮助**
|
|
509
|
+
|
|
510
|
+
\`/pd-focus status\` - 查看 CURRENT_FOCUS.md 状态
|
|
511
|
+
\`/pd-focus history\` - 查看历史版本列表
|
|
512
|
+
\`/pd-focus compress\` - 手动压缩并备份
|
|
513
|
+
\`/pd-focus rollback <序号>\` - 回滚到指定历史版本
|
|
514
|
+
|
|
515
|
+
**功能说明:**
|
|
516
|
+
- 历史版本在压缩时自动创建
|
|
517
|
+
- 最多保留 10 个历史版本
|
|
518
|
+
- Full 模式会读取当前版本 + 最近 3 个历史版本`;
|
|
519
|
+
}
|
|
520
|
+
return `📖 **/pd-focus Command Help**
|
|
521
|
+
|
|
522
|
+
\`/pd-focus status\` - Show CURRENT_FOCUS.md status
|
|
523
|
+
\`/pd-focus history\` - View history versions list
|
|
524
|
+
\`/pd-focus compress\` - Manually compress and backup
|
|
525
|
+
\`/pd-focus rollback <number>\` - Rollback to specified version
|
|
526
|
+
|
|
527
|
+
**Features:**
|
|
528
|
+
- History versions created during compression
|
|
529
|
+
- Maximum 10 history versions retained
|
|
530
|
+
- Full mode reads current + last 3 history versions`;
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* 处理 /pd-focus 命令
|
|
534
|
+
*/
|
|
535
|
+
export async function handleFocusCommand(ctx, api) {
|
|
536
|
+
const workspaceDir = getWorkspaceDir(ctx);
|
|
537
|
+
const args = ctx.args || [];
|
|
538
|
+
const subCommand = args[0]?.toLowerCase() || 'status';
|
|
539
|
+
// 检测语言(与 context.ts 保持一致)
|
|
540
|
+
const isZh = ctx.config?.language === 'zh';
|
|
541
|
+
let result;
|
|
542
|
+
switch (subCommand) {
|
|
543
|
+
case 'status':
|
|
544
|
+
result = showStatus(workspaceDir, isZh);
|
|
545
|
+
break;
|
|
546
|
+
case 'history':
|
|
547
|
+
case 'hist':
|
|
548
|
+
result = showHistory(workspaceDir, isZh);
|
|
549
|
+
break;
|
|
550
|
+
case 'compress':
|
|
551
|
+
case 'cp':
|
|
552
|
+
result = await compressFocus(workspaceDir, isZh, api);
|
|
553
|
+
break;
|
|
554
|
+
case 'rollback':
|
|
555
|
+
case 'rb':
|
|
556
|
+
const index = parseInt(args[1], 10);
|
|
557
|
+
if (isNaN(index)) {
|
|
558
|
+
result = isZh
|
|
559
|
+
? '❌ 请指定要回滚的版本序号\n\n💡 输入 `/pd-focus history` 查看可用版本'
|
|
560
|
+
: '❌ Please specify version number to rollback\n\n💡 Type `/pd-focus history` to see available versions';
|
|
561
|
+
}
|
|
562
|
+
else {
|
|
563
|
+
result = rollbackFocus(workspaceDir, index, isZh);
|
|
564
|
+
}
|
|
565
|
+
break;
|
|
566
|
+
case 'help':
|
|
567
|
+
case '--help':
|
|
568
|
+
case '-h':
|
|
569
|
+
result = showHelp(isZh);
|
|
570
|
+
break;
|
|
571
|
+
default:
|
|
572
|
+
result = isZh
|
|
573
|
+
? `❌ 未知命令: ${subCommand}\n\n${showHelp(isZh)}`
|
|
574
|
+
: `❌ Unknown command: ${subCommand}\n\n${showHelp(isZh)}`;
|
|
575
|
+
}
|
|
576
|
+
return {
|
|
577
|
+
text: result,
|
|
578
|
+
};
|
|
579
|
+
}
|