@kkelly-offical/kkcode 0.1.3 → 0.1.7
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/README.md +110 -172
- package/package.json +46 -46
- package/src/agent/agent.mjs +220 -170
- package/src/agent/prompt/bug-hunter.txt +90 -0
- package/src/agent/prompt/frontend-designer.txt +58 -0
- package/src/agent/prompt/longagent-blueprint-agent.txt +83 -0
- package/src/agent/prompt/longagent-coding-agent.txt +37 -0
- package/src/agent/prompt/longagent-debugging-agent.txt +46 -0
- package/src/agent/prompt/longagent-preview-agent.txt +63 -0
- package/src/config/defaults.mjs +260 -195
- package/src/config/schema.mjs +71 -6
- package/src/core/constants.mjs +91 -46
- package/src/index.mjs +1 -1
- package/src/knowledge/frontend-aesthetics.txt +39 -0
- package/src/knowledge/loader.mjs +2 -1
- package/src/knowledge/tailwind.txt +12 -3
- package/src/mcp/client-http.mjs +141 -157
- package/src/mcp/client-sse.mjs +288 -286
- package/src/mcp/client-stdio.mjs +533 -451
- package/src/mcp/constants.mjs +2 -0
- package/src/mcp/registry.mjs +479 -394
- package/src/mcp/stdio-framing.mjs +133 -127
- package/src/mcp/tool-result.mjs +24 -0
- package/src/observability/index.mjs +42 -0
- package/src/observability/metrics.mjs +137 -0
- package/src/observability/tracer.mjs +137 -0
- package/src/orchestration/background-manager.mjs +372 -358
- package/src/orchestration/background-worker.mjs +305 -245
- package/src/orchestration/longagent-manager.mjs +171 -116
- package/src/orchestration/stage-scheduler.mjs +728 -489
- package/src/permission/exec-policy.mjs +9 -11
- package/src/provider/anthropic.mjs +1 -0
- package/src/provider/openai.mjs +340 -339
- package/src/provider/retry-policy.mjs +68 -68
- package/src/provider/router.mjs +241 -228
- package/src/provider/sse.mjs +104 -91
- package/src/repl.mjs +59 -7
- package/src/session/checkpoint.mjs +66 -3
- package/src/session/compaction.mjs +298 -276
- package/src/session/engine.mjs +232 -225
- package/src/session/longagent-4stage.mjs +460 -0
- package/src/session/longagent-hybrid.mjs +1097 -0
- package/src/session/longagent-plan.mjs +365 -329
- package/src/session/longagent-project-memory.mjs +53 -0
- package/src/session/longagent-scaffold.mjs +291 -100
- package/src/session/longagent-task-bus.mjs +54 -0
- package/src/session/longagent-utils.mjs +472 -0
- package/src/session/longagent.mjs +900 -1462
- package/src/session/loop.mjs +65 -40
- package/src/session/project-context.mjs +30 -0
- package/src/session/prompt/agent.txt +25 -0
- package/src/session/prompt/plan.txt +31 -9
- package/src/session/rollback.mjs +196 -0
- package/src/session/store.mjs +519 -503
- package/src/session/system-prompt.mjs +273 -260
- package/src/session/task-validator.mjs +4 -3
- package/src/skill/builtin/design.mjs +76 -0
- package/src/skill/builtin/frontend.mjs +8 -0
- package/src/skill/registry.mjs +390 -336
- package/src/storage/ghost-commit-store.mjs +18 -8
- package/src/tool/executor.mjs +11 -0
- package/src/tool/git-auto.mjs +0 -19
- package/src/tool/question-prompt.mjs +93 -86
- package/src/tool/registry.mjs +71 -37
- package/src/ui/activity-renderer.mjs +664 -410
- package/src/util/git.mjs +23 -0
package/src/session/loop.mjs
CHANGED
|
@@ -29,8 +29,11 @@ import { saveCheckpoint } from "./checkpoint.mjs"
|
|
|
29
29
|
import { askPlanApproval } from "../tool/question-prompt.mjs"
|
|
30
30
|
import { createValidator } from "./task-validator.mjs"
|
|
31
31
|
|
|
32
|
+
// Max chars kept in active context per tool_result — process output beyond this is truncated
|
|
33
|
+
const TOOL_RESULT_ACTIVE_LIMIT = 3000
|
|
34
|
+
|
|
32
35
|
const READ_ONLY_TOOLS = new Set([
|
|
33
|
-
"read", "glob", "grep", "list", "webfetch", "websearch", "codesearch", "background_output", "todowrite", "enter_plan"
|
|
36
|
+
"read", "glob", "grep", "list", "webfetch", "websearch", "codesearch", "background_output", "todowrite", "enter_plan"
|
|
34
37
|
])
|
|
35
38
|
|
|
36
39
|
function addUsage(target, delta) {
|
|
@@ -534,9 +537,16 @@ export async function processTurnLoop({
|
|
|
534
537
|
? `\n被截断的工具调用: ${truncatedToolNames}。请完整重新发起这些工具调用。如果是创建大文件,使用 write(mode="append") 分段追加;如果是修改已有文件的局部内容,使用 patch 按行号范围替换。`
|
|
535
538
|
: `\nTruncated tool calls: ${truncatedToolNames}. Re-issue these tool calls completely. For large file creation, use write(mode="append") to append in chunks. For modifying sections of existing files, use patch to replace by line range.`)
|
|
536
539
|
: ""
|
|
540
|
+
// Anchor: last 200 chars of truncated text so model knows exactly where to resume
|
|
541
|
+
const textTail = response.text ? response.text.slice(-200) : ""
|
|
542
|
+
const anchorHint = textTail
|
|
543
|
+
? (language === "zh"
|
|
544
|
+
? `\n[锚点] 上次输出末尾:...${textTail}`
|
|
545
|
+
: `\n[Anchor] Last output ended with: ...${textTail}`)
|
|
546
|
+
: ""
|
|
537
547
|
const continuePrompt = language === "zh"
|
|
538
|
-
? `[输出被截断 ${continueCount}/${MAX_CONTINUES}] 你的上一条回复在输出 token 上限处被截断。请从你停止的地方精确继续,不要重复已经写过的内容。如果你正在执行工具调用,请完整重新发起。${toolHint}`
|
|
539
|
-
: `[OUTPUT TRUNCATED ${continueCount}/${MAX_CONTINUES}] Your previous response was cut off at the output token limit. Continue EXACTLY from where you stopped. Do not repeat any content you already wrote. If you were in the middle of a tool call, re-issue it completely.${toolHint}`
|
|
548
|
+
? `[输出被截断 ${continueCount}/${MAX_CONTINUES}] 你的上一条回复在输出 token 上限处被截断。请从你停止的地方精确继续,不要重复已经写过的内容。如果你正在执行工具调用,请完整重新发起。${toolHint}${anchorHint}`
|
|
549
|
+
: `[OUTPUT TRUNCATED ${continueCount}/${MAX_CONTINUES}] Your previous response was cut off at the output token limit. Continue EXACTLY from where you stopped. Do not repeat any content you already wrote. If you were in the middle of a tool call, re-issue it completely.${toolHint}${anchorHint}`
|
|
540
550
|
await appendMessage(sessionId, "user", continuePrompt,
|
|
541
551
|
{ mode, model, providerType, step, turnId, synthetic: true }
|
|
542
552
|
)
|
|
@@ -642,42 +652,51 @@ export async function processTurnLoop({
|
|
|
642
652
|
}
|
|
643
653
|
}
|
|
644
654
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
+
// Plan mode enforcement: block write tools when _planMode is active
|
|
656
|
+
if (toolContext._planMode && !READ_ONLY_TOOLS.has(call.name) && call.name !== "exit_plan") {
|
|
657
|
+
result = {
|
|
658
|
+
name: call.name,
|
|
659
|
+
status: "error",
|
|
660
|
+
output: `[PLAN MODE] Cannot execute '${call.name}' in plan mode. Finish your plan outline and call exit_plan to present it for user approval.`
|
|
661
|
+
}
|
|
662
|
+
} else {
|
|
663
|
+
await PermissionEngine.check({
|
|
664
|
+
config: configState.config,
|
|
665
|
+
sessionId,
|
|
666
|
+
tool: call.name,
|
|
667
|
+
mode,
|
|
668
|
+
pattern,
|
|
669
|
+
command,
|
|
670
|
+
risk,
|
|
671
|
+
reason: `tool call from model at step ${step}`
|
|
672
|
+
})
|
|
655
673
|
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
sessionId,
|
|
668
|
-
turnId,
|
|
669
|
-
context: {
|
|
670
|
-
cwd,
|
|
671
|
-
mode,
|
|
672
|
-
delegateTask,
|
|
673
|
-
signal,
|
|
674
|
+
const tool = await ToolRegistry.get(call.name)
|
|
675
|
+
result = !tool
|
|
676
|
+
? {
|
|
677
|
+
name: call.name,
|
|
678
|
+
status: "error",
|
|
679
|
+
output: `unknown tool: ${call.name}`,
|
|
680
|
+
error: `unknown tool: ${call.name}`
|
|
681
|
+
}
|
|
682
|
+
: await executeTool({
|
|
683
|
+
tool,
|
|
684
|
+
args: call.args,
|
|
674
685
|
sessionId,
|
|
675
686
|
turnId,
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
687
|
+
context: {
|
|
688
|
+
cwd,
|
|
689
|
+
mode,
|
|
690
|
+
delegateTask,
|
|
691
|
+
signal,
|
|
692
|
+
sessionId,
|
|
693
|
+
turnId,
|
|
694
|
+
config: configState.config,
|
|
695
|
+
...toolContext
|
|
696
|
+
},
|
|
697
|
+
signal
|
|
698
|
+
})
|
|
699
|
+
}
|
|
681
700
|
} catch (error) {
|
|
682
701
|
result = {
|
|
683
702
|
name: call.name,
|
|
@@ -700,8 +719,10 @@ export async function processTurnLoop({
|
|
|
700
719
|
result = {
|
|
701
720
|
...result,
|
|
702
721
|
output: approval.approved
|
|
703
|
-
? "User APPROVED the plan. Proceed with implementation."
|
|
704
|
-
:
|
|
722
|
+
? "User APPROVED the plan. Proceed with implementation immediately."
|
|
723
|
+
: approval.requestChanges
|
|
724
|
+
? `User requested changes to the plan. Feedback: ${approval.feedback || "no specific feedback"}. Revise your plan and call exit_plan again with the updated plan.`
|
|
725
|
+
: `User REJECTED the plan. Feedback: ${approval.feedback || "no feedback provided"}. Do not proceed — the plan has been cancelled.`,
|
|
705
726
|
metadata: { ...result.metadata, planApprovalResult: approval }
|
|
706
727
|
}
|
|
707
728
|
}
|
|
@@ -798,15 +819,19 @@ export async function processTurnLoop({
|
|
|
798
819
|
})
|
|
799
820
|
|
|
800
821
|
// User message: tool_result blocks (one per tool call, in order)
|
|
822
|
+
// Process output beyond TOOL_RESULT_ACTIVE_LIMIT is truncated to keep context lean
|
|
801
823
|
const resultContent = []
|
|
802
824
|
for (const call of response.toolCalls) {
|
|
803
825
|
const entry = callResults.get(call.id)
|
|
804
|
-
const
|
|
826
|
+
const rawOutput = entry?.result?.output || ""
|
|
805
827
|
const isError = entry?.result?.status === "error"
|
|
828
|
+
const content = rawOutput.length > TOOL_RESULT_ACTIVE_LIMIT
|
|
829
|
+
? `${rawOutput.slice(0, TOOL_RESULT_ACTIVE_LIMIT)}\n[...过程输出已截断,共 ${rawOutput.length} 字符,仅保留前 ${TOOL_RESULT_ACTIVE_LIMIT} 字符]`
|
|
830
|
+
: rawOutput
|
|
806
831
|
resultContent.push({
|
|
807
832
|
type: "tool_result",
|
|
808
833
|
tool_use_id: call.id,
|
|
809
|
-
content
|
|
834
|
+
content,
|
|
810
835
|
is_error: isError
|
|
811
836
|
})
|
|
812
837
|
}
|
|
@@ -79,6 +79,32 @@ function detectFeatures(allDeps) {
|
|
|
79
79
|
return features
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
/** Detect CSS framework used in the project */
|
|
83
|
+
function detectCssFramework(allDeps) {
|
|
84
|
+
if (allDeps.tailwindcss) return "tailwind"
|
|
85
|
+
if (allDeps.unocss || allDeps["@unocss/core"]) return "unocss"
|
|
86
|
+
if (allDeps["styled-components"]) return "styled-components"
|
|
87
|
+
if (allDeps["@emotion/react"]) return "emotion"
|
|
88
|
+
if (allDeps.sass || allDeps["sass-loader"]) return "sass"
|
|
89
|
+
return null
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Detect UI component library */
|
|
93
|
+
function detectComponentLib(allDeps) {
|
|
94
|
+
if (allDeps["@shadcn/ui"] || allDeps["shadcn-ui"]) return "shadcn/ui"
|
|
95
|
+
if (allDeps["antd"]) return "antd"
|
|
96
|
+
if (allDeps["element-plus"]) return "element-plus"
|
|
97
|
+
if (allDeps["@mui/material"]) return "mui"
|
|
98
|
+
if (allDeps["@chakra-ui/react"]) return "chakra-ui"
|
|
99
|
+
if (allDeps["@radix-ui/react-dialog"] || allDeps["@radix-ui/themes"]) return "radix"
|
|
100
|
+
if (allDeps["@headlessui/react"]) return "headless-ui"
|
|
101
|
+
if (allDeps["@mantine/core"]) return "mantine"
|
|
102
|
+
if (allDeps["naive-ui"]) return "naive-ui"
|
|
103
|
+
if (allDeps["vuetify"]) return "vuetify"
|
|
104
|
+
if (allDeps["@arco-design/web-react"] || allDeps["@arco-design/web-vue"]) return "arco-design"
|
|
105
|
+
return null
|
|
106
|
+
}
|
|
107
|
+
|
|
82
108
|
async function detectStructure(cwd) {
|
|
83
109
|
const dirs = []
|
|
84
110
|
try {
|
|
@@ -341,6 +367,10 @@ export async function detectProjectContext(cwd) {
|
|
|
341
367
|
if (structure.length) lines.push(` structure: ${structure.join(", ")}`)
|
|
342
368
|
if (projectType) lines.push(` type: ${projectType}`)
|
|
343
369
|
if (features.length) lines.push(` features: ${features.join(", ")}`)
|
|
370
|
+
const cssFramework = detectCssFramework(allDeps)
|
|
371
|
+
if (cssFramework) lines.push(` css_framework: ${cssFramework}`)
|
|
372
|
+
const componentLib = detectComponentLib(allDeps)
|
|
373
|
+
if (componentLib) lines.push(` component_lib: ${componentLib}`)
|
|
344
374
|
const hasDocker = await exists(path.join(cwd, "Dockerfile"))
|
|
345
375
|
if (hasDocker) lines.push(` docker: true`)
|
|
346
376
|
lines.push("</project>")
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Agent mode active. Full tool access enabled.
|
|
2
|
+
|
|
3
|
+
## When to use plan mode first
|
|
4
|
+
|
|
5
|
+
Call enter_plan PROACTIVELY before making changes when ANY of these apply:
|
|
6
|
+
- Task requires changes to 3+ files
|
|
7
|
+
- Multiple valid implementation approaches exist
|
|
8
|
+
- Architectural decisions need user input
|
|
9
|
+
- The user hasn't specified implementation details
|
|
10
|
+
- The task involves refactoring, new features, or system-level changes
|
|
11
|
+
|
|
12
|
+
Simple tasks can proceed directly without planning:
|
|
13
|
+
- Single-file bug fix with clear solution
|
|
14
|
+
- Typo or minor text correction
|
|
15
|
+
- Adding one small function with clear requirements
|
|
16
|
+
|
|
17
|
+
## Plan → Execute flow
|
|
18
|
+
|
|
19
|
+
1. Call enter_plan (reason: why planning is needed)
|
|
20
|
+
2. Explore the codebase to understand the context (read, glob, grep)
|
|
21
|
+
3. Outline your complete plan in your response text
|
|
22
|
+
4. Call exit_plan(plan, files) to present it for user approval
|
|
23
|
+
5. If approved: proceed with implementation immediately
|
|
24
|
+
6. If changes requested: revise and call exit_plan again
|
|
25
|
+
7. If rejected: stop and ask the user what they want instead
|
|
@@ -1,9 +1,31 @@
|
|
|
1
|
-
Plan mode active.
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
Plan mode active.
|
|
2
|
+
|
|
3
|
+
## Your task
|
|
4
|
+
Create a detailed implementation plan. Do NOT execute any file mutations, bash commands, or write operations in this mode.
|
|
5
|
+
|
|
6
|
+
## Required plan format
|
|
7
|
+
|
|
8
|
+
1. **Goal**: Clear statement of what will be accomplished
|
|
9
|
+
2. **Approach**: High-level strategy and key architectural decisions
|
|
10
|
+
3. **Implementation steps**: Numbered, specific, actionable steps
|
|
11
|
+
4. **Files to create/modify**: List each file with the specific change description
|
|
12
|
+
5. **Validation checklist**: How to verify the implementation is correct (tests, syntax checks, manual steps)
|
|
13
|
+
|
|
14
|
+
## Quality criteria
|
|
15
|
+
- Each step must be specific enough to execute without ambiguity
|
|
16
|
+
- Include file paths, function names, and line numbers where relevant
|
|
17
|
+
- Identify dependencies between steps (step N must complete before step M)
|
|
18
|
+
- Flag risks, trade-offs, or alternative approaches considered
|
|
19
|
+
- Estimate complexity: simple / medium / complex
|
|
20
|
+
|
|
21
|
+
## Allowed actions in plan mode
|
|
22
|
+
- Read files (read, glob, grep)
|
|
23
|
+
- Search the web (websearch, codesearch)
|
|
24
|
+
- Explore the codebase to gather information
|
|
25
|
+
|
|
26
|
+
## After creating your plan
|
|
27
|
+
Call exit_plan with the complete plan text and the list of files to modify.
|
|
28
|
+
The user will then choose:
|
|
29
|
+
- **Approve** → you proceed with implementation immediately
|
|
30
|
+
- **Request Changes** → you revise the plan and call exit_plan again
|
|
31
|
+
- **Reject** → the plan is cancelled, do not proceed
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { isGitRepo } from "../util/git.mjs"
|
|
2
|
+
import { getLatestGhostCommit, listGhostCommits } from "../storage/ghost-commit-store.mjs"
|
|
3
|
+
import { restoreGhostCommit } from "../util/git.mjs"
|
|
4
|
+
import { askQuestionInteractive } from "../tool/question-prompt.mjs"
|
|
5
|
+
import { EventBus } from "../core/events.mjs"
|
|
6
|
+
import { EVENT_TYPES } from "../core/constants.mjs"
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 回溯意图检测关键词
|
|
10
|
+
* 分为中文和英文两组,按置信度排序
|
|
11
|
+
*/
|
|
12
|
+
const ROLLBACK_PATTERNS = [
|
|
13
|
+
// 高置信度 — 明确的回退指令(中文不用 \b,英文保留)
|
|
14
|
+
{ pattern: /(回退|撤销|撤回|回滚|还原)/i, confidence: 0.9 },
|
|
15
|
+
{ pattern: /\b(undo|rollback|revert)\b/i, confidence: 0.9 },
|
|
16
|
+
// 中置信度 — 需要上下文
|
|
17
|
+
{ pattern: /(恢复到|恢复之前|回到之前|退回|取消(刚才|上次|之前)的(修改|更改|变更|操作))/i, confidence: 0.8 },
|
|
18
|
+
{ pattern: /\b(restore previous|go back|undo (last|previous|recent))\b/i, confidence: 0.8 },
|
|
19
|
+
// 低置信度 — 可能是回退也可能不是
|
|
20
|
+
{ pattern: /(不要了|算了不改了|改回去|恢复原样)/i, confidence: 0.7 }
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 检测用户消息中的回溯意图
|
|
25
|
+
* @param {string} text - 用户输入文本
|
|
26
|
+
* @returns {{ isRollback: boolean, confidence: number, matchedPattern: string }}
|
|
27
|
+
*/
|
|
28
|
+
export function detectRollbackIntent(text) {
|
|
29
|
+
if (!text || typeof text !== "string") {
|
|
30
|
+
return { isRollback: false, confidence: 0, matchedPattern: "" }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const normalized = text.trim().toLowerCase()
|
|
34
|
+
// 过短的消息不太可能是回退指令(除非就是 "undo" 这样的单词)
|
|
35
|
+
if (normalized.length > 200) {
|
|
36
|
+
return { isRollback: false, confidence: 0, matchedPattern: "" }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
for (const { pattern, confidence } of ROLLBACK_PATTERNS) {
|
|
40
|
+
const match = normalized.match(pattern)
|
|
41
|
+
if (match) {
|
|
42
|
+
return { isRollback: true, confidence, matchedPattern: match[0] }
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return { isRollback: false, confidence: 0, matchedPattern: "" }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 向用户确认是否执行回滚,并展示可用快照
|
|
51
|
+
* @returns {{ confirmed: boolean, snapshotId: string|null, message: string }}
|
|
52
|
+
*/
|
|
53
|
+
export async function confirmRollback({ cwd, language = "en" }) {
|
|
54
|
+
const inGit = await isGitRepo(cwd)
|
|
55
|
+
if (!inGit) {
|
|
56
|
+
return {
|
|
57
|
+
confirmed: false,
|
|
58
|
+
snapshotId: null,
|
|
59
|
+
message: language === "zh"
|
|
60
|
+
? "当前目录不是 Git 仓库,无法执行代码回滚。"
|
|
61
|
+
: "Not a git repository — cannot rollback code changes."
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const latest = await getLatestGhostCommit(cwd)
|
|
66
|
+
if (!latest) {
|
|
67
|
+
return {
|
|
68
|
+
confirmed: false,
|
|
69
|
+
snapshotId: null,
|
|
70
|
+
message: language === "zh"
|
|
71
|
+
? "没有找到可用的快照。本次会话尚未创建任何代码快照,无法回滚。"
|
|
72
|
+
: "No snapshots found. No code snapshots were created in this session."
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const snapDate = new Date(latest.createdAt).toLocaleString()
|
|
77
|
+
const fileCount = latest.files?.length || 0
|
|
78
|
+
const shortHash = latest.commitHash?.slice(0, 8) || "unknown"
|
|
79
|
+
|
|
80
|
+
const zhWarning = [
|
|
81
|
+
`找到最近的快照: ${shortHash} (${snapDate})`,
|
|
82
|
+
`包含 ${fileCount} 个文件: ${(latest.files || []).slice(0, 5).join(", ")}${fileCount > 5 ? " ..." : ""}`,
|
|
83
|
+
"",
|
|
84
|
+
"⚠ 注意: 回滚只能恢复文件变更。已执行的 bash 命令(如安装依赖、删除文件等)无法自动撤销。"
|
|
85
|
+
].join("\n")
|
|
86
|
+
|
|
87
|
+
const enWarning = [
|
|
88
|
+
`Latest snapshot: ${shortHash} (${snapDate})`,
|
|
89
|
+
`Contains ${fileCount} file(s): ${(latest.files || []).slice(0, 5).join(", ")}${fileCount > 5 ? " ..." : ""}`,
|
|
90
|
+
"",
|
|
91
|
+
"Warning: Rollback only restores file changes. Bash commands (installs, deletions, etc.) cannot be undone."
|
|
92
|
+
].join("\n")
|
|
93
|
+
|
|
94
|
+
const answers = await askQuestionInteractive({
|
|
95
|
+
questions: [{
|
|
96
|
+
id: "rollback_confirm",
|
|
97
|
+
text: language === "zh" ? "确认回滚代码?" : "Confirm code rollback?",
|
|
98
|
+
description: language === "zh" ? zhWarning : enWarning,
|
|
99
|
+
options: [
|
|
100
|
+
{
|
|
101
|
+
label: language === "zh" ? "确认回滚" : "Confirm rollback",
|
|
102
|
+
value: "yes",
|
|
103
|
+
description: language === "zh"
|
|
104
|
+
? "恢复文件到快照状态"
|
|
105
|
+
: "Restore files to snapshot state"
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
label: language === "zh" ? "取消" : "Cancel",
|
|
109
|
+
value: "no",
|
|
110
|
+
description: language === "zh"
|
|
111
|
+
? "不执行回滚,继续当前对话"
|
|
112
|
+
: "Skip rollback, continue conversation"
|
|
113
|
+
}
|
|
114
|
+
],
|
|
115
|
+
allowCustom: false
|
|
116
|
+
}]
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
const answer = String(answers.rollback_confirm || "").toLowerCase().trim()
|
|
120
|
+
const confirmed = ["yes", "confirm", "确认回滚", "1"].includes(answer)
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
confirmed,
|
|
124
|
+
snapshotId: confirmed ? latest.id : null,
|
|
125
|
+
commitHash: confirmed ? latest.commitHash : null,
|
|
126
|
+
message: confirmed
|
|
127
|
+
? (language === "zh" ? `正在回滚到快照 ${shortHash}...` : `Rolling back to snapshot ${shortHash}...`)
|
|
128
|
+
: (language === "zh" ? "已取消回滚。" : "Rollback cancelled.")
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* 执行代码回滚
|
|
134
|
+
* @returns {{ ok: boolean, message: string }}
|
|
135
|
+
*/
|
|
136
|
+
export async function executeRollback({ cwd, commitHash, sessionId, language = "en" }) {
|
|
137
|
+
try {
|
|
138
|
+
const result = await restoreGhostCommit(cwd, commitHash, false)
|
|
139
|
+
if (!result.ok) {
|
|
140
|
+
return {
|
|
141
|
+
ok: false,
|
|
142
|
+
message: language === "zh"
|
|
143
|
+
? `回滚失败: ${result.error}`
|
|
144
|
+
: `Rollback failed: ${result.error}`
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
await EventBus.emit({
|
|
149
|
+
type: EVENT_TYPES.TURN_STEP_FINISH,
|
|
150
|
+
sessionId,
|
|
151
|
+
payload: { action: "rollback", commitHash }
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
ok: true,
|
|
156
|
+
message: language === "zh"
|
|
157
|
+
? `已成功回滚到快照 ${commitHash.slice(0, 8)}。文件已恢复,但已执行的 bash 命令无法撤销。`
|
|
158
|
+
: `Rolled back to snapshot ${commitHash.slice(0, 8)}. Files restored, but executed bash commands cannot be undone.`
|
|
159
|
+
}
|
|
160
|
+
} catch (err) {
|
|
161
|
+
return {
|
|
162
|
+
ok: false,
|
|
163
|
+
message: language === "zh"
|
|
164
|
+
? `回滚异常: ${err.message}`
|
|
165
|
+
: `Rollback error: ${err.message}`
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* 完整的回溯流程:检测 → 确认 → 执行
|
|
172
|
+
* 在 loop.mjs 的 processTurnLoop 入口调用
|
|
173
|
+
*
|
|
174
|
+
* @returns {{ handled: boolean, reply: string }}
|
|
175
|
+
* handled=true 表示消息已被回溯流程处理,不需要再发给模型
|
|
176
|
+
*/
|
|
177
|
+
export async function handleRollbackIfNeeded({ prompt, cwd, sessionId, language = "en" }) {
|
|
178
|
+
const intent = detectRollbackIntent(prompt)
|
|
179
|
+
if (!intent.isRollback) {
|
|
180
|
+
return { handled: false, reply: "" }
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const confirmation = await confirmRollback({ cwd, language })
|
|
184
|
+
if (!confirmation.confirmed) {
|
|
185
|
+
return { handled: true, reply: confirmation.message }
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const result = await executeRollback({
|
|
189
|
+
cwd,
|
|
190
|
+
commitHash: confirmation.commitHash,
|
|
191
|
+
sessionId,
|
|
192
|
+
language
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
return { handled: true, reply: result.message }
|
|
196
|
+
}
|