godot-daedalus_backend 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/README.md +101 -0
- package/bin/godot-daedalus-backend.js +4 -0
- package/bin/godot-daedalus-mcp.js +4 -0
- package/bin/godot-daedalus-terminal-mcp.js +4 -0
- package/bin/run-tsx-entry.js +26 -0
- package/package.json +54 -0
- package/scripts/deepseek-tokenizer-server.py +54 -0
- package/src/app-paths.ts +36 -0
- package/src/main.ts +21 -0
- package/src/mcp/content-length-protocol.ts +68 -0
- package/src/mcp/custom-mcp-config-store.ts +397 -0
- package/src/mcp/godot-diagnostics-bridge.ts +1298 -0
- package/src/mcp/godot-editor-bridge.ts +307 -0
- package/src/mcp/godot-mcp-server.ts +3484 -0
- package/src/mcp/godot-paths.ts +151 -0
- package/src/mcp/godot-project-settings.ts +233 -0
- package/src/mcp/godot-tool-registration.ts +46 -0
- package/src/mcp/mcp-config.ts +48 -0
- package/src/mcp/mcp-host.ts +393 -0
- package/src/mcp/mcp-session.ts +81 -0
- package/src/mcp/terminal-mcp-server.ts +576 -0
- package/src/mcp/tscn-tools.ts +302 -0
- package/src/mcp/types.ts +12 -0
- package/src/ping-client.ts +24 -0
- package/src/prompts/registry.ts +97 -0
- package/src/prompts/templates/backend-helper.md +25 -0
- package/src/prompts/templates/gdscript-reviewer.md +19 -0
- package/src/prompts/templates/godot-assistant.md +225 -0
- package/src/prompts/templates/scene-architect.md +15 -0
- package/src/prompts/templates/session-compressor.md +33 -0
- package/src/protocol/schema.ts +486 -0
- package/src/protocol/types.ts +77 -0
- package/src/providers/deepseek-agent.ts +1014 -0
- package/src/providers/deepseek-client.ts +114 -0
- package/src/providers/deepseek-dsml-tools.ts +90 -0
- package/src/providers/deepseek-loose-tools.ts +450 -0
- package/src/providers/provider-config-store.ts +164 -0
- package/src/server/client-session.ts +93 -0
- package/src/server/request-dispatcher.ts +74 -0
- package/src/server/response-helpers.ts +33 -0
- package/src/server/send-json.ts +8 -0
- package/src/server/websocket-server.ts +3997 -0
- package/src/session/session-compressor.ts +68 -0
- package/src/session/session-store.ts +669 -0
- package/src/skills/registry.ts +180 -0
- package/src/skills/templates/backend-helper.md +12 -0
- package/src/skills/templates/file-creator.md +14 -0
- package/src/skills/templates/gdscript-review.md +12 -0
- package/src/skills/templates/godot-project-init.md +29 -0
- package/src/skills/templates/scene-builder.md +12 -0
- package/src/tokens/deepseek-tokenizer-counter.ts +233 -0
- package/src/tokens/model-profiles.ts +38 -0
- package/src/tokens/token-counter-factory.ts +52 -0
- package/src/tokens/token-counter.ts +22 -0
- package/src/tools/approval-gateway.ts +111 -0
- package/src/tools/llm-tools.ts +1415 -0
- package/src/tools/tool-dispatcher.ts +147 -0
- package/src/tools/tool-event-describer.ts +387 -0
- package/src/tools/tool-idempotency.ts +373 -0
- package/src/tools/tool-policy-table.ts +61 -0
- package/src/tools/tool-policy.ts +73 -0
- package/src/workflow/llm-planner.ts +407 -0
- package/src/workflow/planner.ts +201 -0
- package/src/workflow/runner.ts +141 -0
- package/src/workflow/types.ts +69 -0
- package/src/workspace/registry.ts +104 -0
- package/src/workspace/types.ts +7 -0
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { chatWithDeepSeek, type DeepSeekChatOptions } from "../providers/deepseek-client.js";
|
|
3
|
+
import { promptIdSchema } from "../protocol/schema.js";
|
|
4
|
+
import type { AiChatParams, ChatMessage, PromptId } from "../protocol/types.js";
|
|
5
|
+
import { isSkillId, type SkillId } from "../skills/registry.js";
|
|
6
|
+
import type { ToolBudgetLevel } from "../tools/llm-tools.js";
|
|
7
|
+
import { createWorkflowId, createWorkflowTitle, READ_TOOLS, VERIFY_TOOLS, WRITE_TOOLS } from "./planner.js";
|
|
8
|
+
import type {
|
|
9
|
+
WorkflowPhase,
|
|
10
|
+
WorkflowPhaseOutput,
|
|
11
|
+
WorkflowPlan,
|
|
12
|
+
WorkflowTodoItem,
|
|
13
|
+
WorkflowToolGroup
|
|
14
|
+
} from "./types.js";
|
|
15
|
+
|
|
16
|
+
const MAX_LLM_WORKFLOW_STEPS: number = 8;
|
|
17
|
+
const MAX_LLM_WORKFLOW_REVISIONS: number = 3;
|
|
18
|
+
const MAX_PLANNING_CONTEXT_CHARS: number = 8000;
|
|
19
|
+
const MAX_PHASE_INSTRUCTION_CHARS: number = 1200;
|
|
20
|
+
|
|
21
|
+
const toolGroupSchema = z.enum(["read", "write", "verify", "summarize"]);
|
|
22
|
+
|
|
23
|
+
const llmPlanStepSchema = z.object({
|
|
24
|
+
id: z.string().min(1).max(48).optional(),
|
|
25
|
+
title: z.string().min(1).max(80),
|
|
26
|
+
instruction: z.string().min(1).max(2000),
|
|
27
|
+
toolGroup: toolGroupSchema,
|
|
28
|
+
skillId: z.string().nullable().optional(),
|
|
29
|
+
promptId: promptIdSchema.nullable().optional()
|
|
30
|
+
}).strict();
|
|
31
|
+
|
|
32
|
+
const llmPlanSchema = z.object({
|
|
33
|
+
title: z.string().min(1).max(80).optional(),
|
|
34
|
+
steps: z.array(llmPlanStepSchema).min(1).max(MAX_LLM_WORKFLOW_STEPS)
|
|
35
|
+
}).strict();
|
|
36
|
+
|
|
37
|
+
type LlmPlanStep = z.infer<typeof llmPlanStepSchema>;
|
|
38
|
+
type LlmPlan = z.infer<typeof llmPlanSchema>;
|
|
39
|
+
|
|
40
|
+
export async function createLlmWorkflowPlan(
|
|
41
|
+
params: AiChatParams,
|
|
42
|
+
options: DeepSeekChatOptions,
|
|
43
|
+
history: ChatMessage[],
|
|
44
|
+
planningContext: string,
|
|
45
|
+
abortSignal?: AbortSignal | undefined
|
|
46
|
+
): Promise<WorkflowPlan | null> {
|
|
47
|
+
const text: string = await chatWithDeepSeek(
|
|
48
|
+
createPlannerParams(createInitialPlanMessage(params.message, planningContext)),
|
|
49
|
+
options,
|
|
50
|
+
limitPlanningHistory(history),
|
|
51
|
+
createPlannerSystemPrompt(),
|
|
52
|
+
abortSignal
|
|
53
|
+
);
|
|
54
|
+
const rawPlan: LlmPlan = parseLlmPlan(text);
|
|
55
|
+
return createWorkflowPlanFromLlmPlan(rawPlan, params.message);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function reviseLlmWorkflowPlan(
|
|
59
|
+
plan: WorkflowPlan,
|
|
60
|
+
completedPhaseIndex: number,
|
|
61
|
+
originalParams: AiChatParams,
|
|
62
|
+
phaseOutputs: WorkflowPhaseOutput[],
|
|
63
|
+
options: DeepSeekChatOptions,
|
|
64
|
+
history: ChatMessage[],
|
|
65
|
+
planningContext: string,
|
|
66
|
+
abortSignal?: AbortSignal | undefined
|
|
67
|
+
): Promise<WorkflowPlan> {
|
|
68
|
+
if (plan.source !== "llm") {
|
|
69
|
+
return plan;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const revision: number = plan.revision ?? 0;
|
|
73
|
+
const maxRevisions: number = plan.maxRevisions ?? MAX_LLM_WORKFLOW_REVISIONS;
|
|
74
|
+
if (revision >= maxRevisions || completedPhaseIndex >= plan.phases.length - 1) {
|
|
75
|
+
return plan;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const text: string = await chatWithDeepSeek(
|
|
79
|
+
createPlannerParams(createRevisionMessage(plan, completedPhaseIndex, originalParams.message, phaseOutputs, planningContext)),
|
|
80
|
+
options,
|
|
81
|
+
limitPlanningHistory(history),
|
|
82
|
+
createPlannerSystemPrompt(),
|
|
83
|
+
abortSignal
|
|
84
|
+
);
|
|
85
|
+
const rawPlan: LlmPlan = parseLlmPlan(text);
|
|
86
|
+
return mergeRevisedPendingSteps(plan, completedPhaseIndex + 1, rawPlan);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function createPlannerParams(message: string): AiChatParams {
|
|
90
|
+
return {
|
|
91
|
+
message,
|
|
92
|
+
options: {
|
|
93
|
+
temperature: 0.2,
|
|
94
|
+
maxTokens: 2000,
|
|
95
|
+
responseFormat: "json",
|
|
96
|
+
workflow: "single"
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function createPlannerSystemPrompt(): string {
|
|
102
|
+
return [
|
|
103
|
+
"你是 Godot Daedalus 的任务调度器,只负责输出 JSON 计划,不调用工具,不写解释文本。",
|
|
104
|
+
"输出必须是一个 JSON object,格式为:",
|
|
105
|
+
"{\"title\":\"简短任务标题\",\"steps\":[{\"id\":\"stable-id\",\"title\":\"简短 Todo 标题\",\"instruction\":\"给执行模型的具体指令\",\"toolGroup\":\"read|write|verify|summarize\",\"skillId\":null,\"promptId\":\"godot.assistant\"}]}",
|
|
106
|
+
"toolGroup 只能选择:",
|
|
107
|
+
"- read:只读项目上下文。",
|
|
108
|
+
"- write:允许读取和实际写入,写入仍会走后端审批。",
|
|
109
|
+
"- verify:允许读取和运行安全验证。",
|
|
110
|
+
"- summarize:不使用工具,只总结交付。",
|
|
111
|
+
"规则:",
|
|
112
|
+
`- steps 数量 1-${MAX_LLM_WORKFLOW_STEPS}。`,
|
|
113
|
+
"- 每个 title 必须是前端 Todo 可显示的短标题,不要写长描述。",
|
|
114
|
+
"- 复杂修改通常包含 read/write/verify/summarize;简单问答可以只有 summarize。",
|
|
115
|
+
"- 最后一步必须能给用户最终交付总结,优先使用 toolGroup=summarize。",
|
|
116
|
+
"- 如果上下文显示 Godot 编辑器在线,且用户目标指向当前打开场景、选中节点、当前脚本/这几行或 FileSystem Dock 选中项,read/write 步骤应让执行模型优先使用 godot_editor 工具;若编辑器离线、stale 或不匹配,则回退到离线 .tscn/text/headless 工具。",
|
|
117
|
+
"- 如果用户询问运行报错、日志、user://logs/godot.log 或项目设置,read 步骤应收集日志配置/日志内容/当前项目设置;修改项目设置时使用 write 步骤,并要求执行模型先预览再实际写入。",
|
|
118
|
+
"- 如果用户询问 Godot 编辑器设置、主题、字体、最近项目、当前打开场景/脚本或 .godot/editor 状态,read 步骤应收集编辑器配置摘要;除非用户明确要求原始路径/原文,否则保持脱敏读取。",
|
|
119
|
+
"- 修改 GDScript 的任务应包含 verify 步骤,让执行模型优先读取 LSP diagnostics,再运行 Godot check-only;运行时报错排查应优先尝试 DAP last error / stack trace,失败后再回退项目日志。",
|
|
120
|
+
"- 不要输出 tool 名称,后端会根据 toolGroup 决定安全工具集合。"
|
|
121
|
+
].join("\n");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function createInitialPlanMessage(userMessage: string, planningContext: string): string {
|
|
125
|
+
return [
|
|
126
|
+
"请为下面用户需求生成可执行 Todo 计划。",
|
|
127
|
+
"",
|
|
128
|
+
"## 用户需求",
|
|
129
|
+
userMessage,
|
|
130
|
+
"",
|
|
131
|
+
"## 当前后端注入上下文",
|
|
132
|
+
clipPlanningContext(planningContext)
|
|
133
|
+
].join("\n");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function createRevisionMessage(
|
|
137
|
+
plan: WorkflowPlan,
|
|
138
|
+
completedPhaseIndex: number,
|
|
139
|
+
userMessage: string,
|
|
140
|
+
phaseOutputs: WorkflowPhaseOutput[],
|
|
141
|
+
planningContext: string
|
|
142
|
+
): string {
|
|
143
|
+
const completedPhases: WorkflowPhase[] = plan.phases.slice(0, completedPhaseIndex + 1);
|
|
144
|
+
const pendingPhases: WorkflowPhase[] = plan.phases.slice(completedPhaseIndex + 1);
|
|
145
|
+
return [
|
|
146
|
+
"请根据已完成步骤结果,修订后续 pending Todo。只能替换未执行步骤,不能改已完成步骤。",
|
|
147
|
+
"",
|
|
148
|
+
"## 用户原始需求",
|
|
149
|
+
userMessage,
|
|
150
|
+
"",
|
|
151
|
+
"## 已完成步骤",
|
|
152
|
+
JSON.stringify(completedPhases.map((phase: WorkflowPhase) => ({ id: phase.id, title: phase.title, toolGroup: phase.toolGroup ?? null }))),
|
|
153
|
+
"",
|
|
154
|
+
"## 已完成步骤输出",
|
|
155
|
+
phaseOutputs.map(formatPhaseOutputForPlanner).join("\n\n"),
|
|
156
|
+
"",
|
|
157
|
+
"## 当前 pending 步骤",
|
|
158
|
+
JSON.stringify(pendingPhases.map((phase: WorkflowPhase) => ({ id: phase.id, title: phase.title, instruction: phase.instruction, toolGroup: phase.toolGroup ?? null }))),
|
|
159
|
+
"",
|
|
160
|
+
"## 当前后端注入上下文",
|
|
161
|
+
clipPlanningContext(planningContext),
|
|
162
|
+
"",
|
|
163
|
+
"请只输出完整替换后的 pending steps。若无需调整,原样输出 pending steps。不要输出已完成步骤,不要复用已完成步骤 id。"
|
|
164
|
+
].join("\n");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function parseLlmPlan(text: string): LlmPlan {
|
|
168
|
+
const parsed: unknown = parseJsonObject(text);
|
|
169
|
+
return llmPlanSchema.parse(parsed);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function parseJsonObject(text: string): unknown {
|
|
173
|
+
try {
|
|
174
|
+
return JSON.parse(text) as unknown;
|
|
175
|
+
} catch {
|
|
176
|
+
const startIndex: number = text.indexOf("{");
|
|
177
|
+
const endIndex: number = text.lastIndexOf("}");
|
|
178
|
+
if (startIndex >= 0 && endIndex > startIndex) {
|
|
179
|
+
return JSON.parse(text.slice(startIndex, endIndex + 1)) as unknown;
|
|
180
|
+
}
|
|
181
|
+
throw new Error("LLM planner did not return valid JSON");
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function createWorkflowPlanFromLlmPlan(rawPlan: LlmPlan, userMessage: string): WorkflowPlan | null {
|
|
186
|
+
const phases: WorkflowPhase[] = createPhasesFromSteps(rawPlan.steps);
|
|
187
|
+
if (phases.length === 0) {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
id: createWorkflowId(),
|
|
193
|
+
title: rawPlan.title ?? createWorkflowTitle(userMessage),
|
|
194
|
+
phases,
|
|
195
|
+
todos: createTodos(phases),
|
|
196
|
+
source: "llm",
|
|
197
|
+
revision: 0,
|
|
198
|
+
maxRevisions: MAX_LLM_WORKFLOW_REVISIONS
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function createPhasesFromSteps(steps: LlmPlanStep[]): WorkflowPhase[] {
|
|
203
|
+
return createPhasesFromStepsWithReservedIds(steps, new Set());
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function createPhasesFromStepsWithReservedIds(steps: LlmPlanStep[], reservedIds: Set<string>): WorkflowPhase[] {
|
|
207
|
+
const trimmedSteps: LlmPlanStep[] = ensureSummaryStep(steps.slice(0, MAX_LLM_WORKFLOW_STEPS));
|
|
208
|
+
const usedIds: Set<string> = new Set(reservedIds);
|
|
209
|
+
return trimmedSteps.map((step: LlmPlanStep, index: number): WorkflowPhase => createPhaseFromStep(step, index, usedIds));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function ensureSummaryStep(steps: LlmPlanStep[]): LlmPlanStep[] {
|
|
213
|
+
if (steps.length === 0) {
|
|
214
|
+
return [{
|
|
215
|
+
id: "summarize",
|
|
216
|
+
title: "总结交付",
|
|
217
|
+
instruction: "直接回答用户需求,说明结论和必要的后续建议。",
|
|
218
|
+
toolGroup: "summarize",
|
|
219
|
+
promptId: "godot.assistant"
|
|
220
|
+
}];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const lastStep: LlmPlanStep | undefined = steps[steps.length - 1];
|
|
224
|
+
if (lastStep?.toolGroup === "summarize") {
|
|
225
|
+
return steps;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const baseSteps: LlmPlanStep[] = steps.length >= MAX_LLM_WORKFLOW_STEPS
|
|
229
|
+
? steps.slice(0, MAX_LLM_WORKFLOW_STEPS - 1)
|
|
230
|
+
: steps;
|
|
231
|
+
|
|
232
|
+
return [
|
|
233
|
+
...baseSteps,
|
|
234
|
+
{
|
|
235
|
+
id: "summarize",
|
|
236
|
+
title: "总结交付",
|
|
237
|
+
instruction: "基于前面步骤结果给用户最终总结,说明完成内容、验证状态和剩余风险。",
|
|
238
|
+
toolGroup: "summarize",
|
|
239
|
+
promptId: "godot.assistant"
|
|
240
|
+
}
|
|
241
|
+
];
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function createPhaseFromStep(step: LlmPlanStep, index: number, usedIds: Set<string>): WorkflowPhase {
|
|
245
|
+
const toolGroup: WorkflowToolGroup = step.toolGroup;
|
|
246
|
+
const skillId: SkillId | undefined = normalizeSkillId(step.skillId ?? defaultSkillForToolGroup(toolGroup));
|
|
247
|
+
const promptId: PromptId | undefined = step.promptId ?? defaultPromptForToolGroup(toolGroup);
|
|
248
|
+
return {
|
|
249
|
+
id: createUniqueStepId(step.id ?? step.title, index, usedIds),
|
|
250
|
+
title: clipText(step.title, 32),
|
|
251
|
+
toolGroup,
|
|
252
|
+
skillId,
|
|
253
|
+
promptId,
|
|
254
|
+
toolBudget: getToolBudgetForToolGroup(toolGroup),
|
|
255
|
+
allowedTools: getAllowedToolsForToolGroup(toolGroup),
|
|
256
|
+
instruction: clipText(step.instruction, MAX_PHASE_INSTRUCTION_CHARS)
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function mergeRevisedPendingSteps(plan: WorkflowPlan, firstPendingIndex: number, rawPlan: LlmPlan): WorkflowPlan {
|
|
261
|
+
const completedPhases: WorkflowPhase[] = plan.phases.slice(0, firstPendingIndex);
|
|
262
|
+
const completedPhaseIds: Set<string> = new Set(completedPhases.map((phase: WorkflowPhase): string => phase.id));
|
|
263
|
+
const completedPhaseTitles: Set<string> = new Set(completedPhases.map((phase: WorkflowPhase): string => phase.title.toLowerCase()));
|
|
264
|
+
const usableSteps: LlmPlanStep[] = rawPlan.steps.filter((step: LlmPlanStep, index: number): boolean => (
|
|
265
|
+
!doesStepRepeatCompletedPhase(step, index, completedPhaseIds, completedPhaseTitles)
|
|
266
|
+
));
|
|
267
|
+
const previousPendingPhases: WorkflowPhase[] = plan.phases.slice(firstPendingIndex);
|
|
268
|
+
const revisedPendingPhases: WorkflowPhase[] = usableSteps.length > 0
|
|
269
|
+
? createPhasesFromStepsWithReservedIds(usableSteps, completedPhaseIds)
|
|
270
|
+
: previousPendingPhases.map((phase: WorkflowPhase): WorkflowPhase => ({
|
|
271
|
+
...phase,
|
|
272
|
+
allowedTools: [...phase.allowedTools]
|
|
273
|
+
}));
|
|
274
|
+
const phases: WorkflowPhase[] = [...completedPhases, ...revisedPendingPhases];
|
|
275
|
+
const completedTodos: WorkflowTodoItem[] = plan.todos.filter((todo: WorkflowTodoItem): boolean => completedPhaseIds.has(todo.phaseId));
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
...plan,
|
|
279
|
+
title: plan.title,
|
|
280
|
+
phases,
|
|
281
|
+
todos: [
|
|
282
|
+
...completedTodos,
|
|
283
|
+
...createTodos(revisedPendingPhases)
|
|
284
|
+
],
|
|
285
|
+
revision: (plan.revision ?? 0) + 1
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function doesStepRepeatCompletedPhase(
|
|
290
|
+
step: LlmPlanStep,
|
|
291
|
+
index: number,
|
|
292
|
+
completedPhaseIds: Set<string>,
|
|
293
|
+
completedPhaseTitles: Set<string>
|
|
294
|
+
): boolean {
|
|
295
|
+
const stepId: string | undefined = step.id?.trim();
|
|
296
|
+
if (stepId !== undefined && completedPhaseIds.has(normalizeStepId(stepId, index))) {
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (completedPhaseIds.has(normalizeStepId(step.title, index))) {
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return completedPhaseTitles.has(step.title.trim().toLowerCase());
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function createTodos(phases: WorkflowPhase[]): WorkflowTodoItem[] {
|
|
308
|
+
return phases.map((phase: WorkflowPhase): WorkflowTodoItem => ({
|
|
309
|
+
id: `${phase.id}-todo`,
|
|
310
|
+
phaseId: phase.id,
|
|
311
|
+
text: phase.title,
|
|
312
|
+
status: "pending"
|
|
313
|
+
}));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function createUniqueStepId(value: string, index: number, usedIds: Set<string>): string {
|
|
317
|
+
const baseId: string = normalizeStepId(value, index);
|
|
318
|
+
let nextId: string = baseId;
|
|
319
|
+
let suffix: number = 2;
|
|
320
|
+
while (usedIds.has(nextId)) {
|
|
321
|
+
nextId = `${baseId}-${suffix}`;
|
|
322
|
+
suffix += 1;
|
|
323
|
+
}
|
|
324
|
+
usedIds.add(nextId);
|
|
325
|
+
return nextId;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function normalizeStepId(value: string, index: number): string {
|
|
329
|
+
const normalized: string = value
|
|
330
|
+
.toLowerCase()
|
|
331
|
+
.replace(/[^a-z0-9_-]+/g, "-")
|
|
332
|
+
.replace(/^-+|-+$/g, "");
|
|
333
|
+
const fallback: string = `step-${index + 1}`;
|
|
334
|
+
return normalized.length > 0 ? normalized : fallback;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function normalizeSkillId(value: string | null | undefined): SkillId | undefined {
|
|
338
|
+
if (value === null || value === undefined || value.length === 0) {
|
|
339
|
+
return undefined;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return isSkillId(value) ? value : undefined;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function defaultSkillForToolGroup(toolGroup: WorkflowToolGroup): SkillId | undefined {
|
|
346
|
+
if (toolGroup === "write") {
|
|
347
|
+
return "file.creator";
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return undefined;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function defaultPromptForToolGroup(toolGroup: WorkflowToolGroup): PromptId | undefined {
|
|
354
|
+
if (toolGroup === "summarize" || toolGroup === "write") {
|
|
355
|
+
return "godot.assistant";
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return undefined;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function getToolBudgetForToolGroup(toolGroup: WorkflowToolGroup): ToolBudgetLevel {
|
|
362
|
+
if (toolGroup === "write") {
|
|
363
|
+
return "project_edit";
|
|
364
|
+
}
|
|
365
|
+
if (toolGroup === "summarize") {
|
|
366
|
+
return "simple";
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return "normal";
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function getAllowedToolsForToolGroup(toolGroup: WorkflowToolGroup): string[] {
|
|
373
|
+
if (toolGroup === "write") {
|
|
374
|
+
return [...READ_TOOLS, ...WRITE_TOOLS];
|
|
375
|
+
}
|
|
376
|
+
if (toolGroup === "verify") {
|
|
377
|
+
return [...READ_TOOLS, ...VERIFY_TOOLS];
|
|
378
|
+
}
|
|
379
|
+
if (toolGroup === "summarize") {
|
|
380
|
+
return [];
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return [...READ_TOOLS];
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function limitPlanningHistory(history: ChatMessage[]): ChatMessage[] {
|
|
387
|
+
return history.slice(-6);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function clipPlanningContext(context: string): string {
|
|
391
|
+
return clipText(context, MAX_PLANNING_CONTEXT_CHARS);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function clipText(text: string, maxChars: number): string {
|
|
395
|
+
if (text.length <= maxChars) {
|
|
396
|
+
return text;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return `${text.slice(0, maxChars)}\n\n[内容已截断,原始长度 ${text.length} 字符]`;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function formatPhaseOutputForPlanner(output: WorkflowPhaseOutput): string {
|
|
403
|
+
return [
|
|
404
|
+
`### ${output.title}(${output.phaseId})`,
|
|
405
|
+
clipText(output.text, 2000)
|
|
406
|
+
].join("\n");
|
|
407
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import type { AiChatParams } from "../protocol/types.js";
|
|
2
|
+
import { CUSTOM_MCP_TOOLS_SENTINEL } from "../tools/llm-tools.js";
|
|
3
|
+
import type { WorkflowPhase, WorkflowPhaseId, WorkflowPlan, WorkflowTodoItem } from "./types.js";
|
|
4
|
+
|
|
5
|
+
type FixedWorkflowPhaseId = "inspect" | "implement" | "review" | "verify" | "summarize";
|
|
6
|
+
|
|
7
|
+
export const READ_TOOLS: string[] = [
|
|
8
|
+
"mcp_godot_get_project_summary",
|
|
9
|
+
"mcp_godot_list_project_files",
|
|
10
|
+
"mcp_godot_list_scenes",
|
|
11
|
+
"mcp_godot_list_scripts",
|
|
12
|
+
"mcp_godot_read_text_file",
|
|
13
|
+
"mcp_godot_search_text",
|
|
14
|
+
"mcp_godot_get_project_log_config",
|
|
15
|
+
"mcp_godot_list_project_logs",
|
|
16
|
+
"mcp_godot_read_project_log",
|
|
17
|
+
"mcp_godot_get_project_settings",
|
|
18
|
+
"mcp_godot_get_editor_config_summary",
|
|
19
|
+
"mcp_godot_get_editor_settings",
|
|
20
|
+
"mcp_godot_list_editor_config_files",
|
|
21
|
+
"mcp_godot_read_editor_config_file",
|
|
22
|
+
"mcp_godot_get_editor_project_state",
|
|
23
|
+
"mcp_godot_get_recent_projects",
|
|
24
|
+
"mcp_godot_inspect_scene_tree",
|
|
25
|
+
"mcp_godot_editor_get_context",
|
|
26
|
+
"mcp_godot_editor_get_selected_nodes",
|
|
27
|
+
"mcp_godot_editor_inspect_node",
|
|
28
|
+
"mcp_godot_lsp_get_status",
|
|
29
|
+
"mcp_godot_lsp_get_file_diagnostics",
|
|
30
|
+
"mcp_godot_lsp_get_document_symbols",
|
|
31
|
+
"mcp_godot_lsp_hover",
|
|
32
|
+
"mcp_godot_lsp_goto_definition",
|
|
33
|
+
"mcp_godot_dap_get_status",
|
|
34
|
+
"mcp_godot_dap_get_last_error",
|
|
35
|
+
"mcp_godot_dap_get_stack_trace",
|
|
36
|
+
"mcp_godot_dap_get_variables",
|
|
37
|
+
CUSTOM_MCP_TOOLS_SENTINEL
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
export const VERIFY_TOOLS: string[] = [
|
|
41
|
+
"mcp_godot_lsp_get_file_diagnostics",
|
|
42
|
+
"mcp_terminal_get_capabilities",
|
|
43
|
+
"mcp_terminal_run_safe_preset"
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
export const WRITE_TOOLS: string[] = [
|
|
47
|
+
"mcp_godot_create_text_file",
|
|
48
|
+
"mcp_godot_overwrite_text_file",
|
|
49
|
+
"mcp_godot_replace_text_in_file",
|
|
50
|
+
"mcp_godot_create_scene",
|
|
51
|
+
"mcp_godot_add_node_to_scene",
|
|
52
|
+
"mcp_godot_attach_script_to_node",
|
|
53
|
+
"mcp_godot_connect_signal_in_scene",
|
|
54
|
+
"mcp_godot_apply_scene_patch",
|
|
55
|
+
"mcp_godot_editor_apply_scene_patch",
|
|
56
|
+
"mcp_godot_propose_set_project_setting",
|
|
57
|
+
"mcp_godot_set_project_setting",
|
|
58
|
+
"mcp_godot_propose_unset_project_setting",
|
|
59
|
+
"mcp_godot_unset_project_setting",
|
|
60
|
+
"mcp_terminal_run_write_preset",
|
|
61
|
+
"mcp_terminal_run_godot_scene_script"
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
const PHASE_TEMPLATES: Record<FixedWorkflowPhaseId, WorkflowPhase> = {
|
|
65
|
+
inspect: {
|
|
66
|
+
id: "inspect",
|
|
67
|
+
title: "理解上下文",
|
|
68
|
+
toolGroup: "read",
|
|
69
|
+
toolBudget: "normal",
|
|
70
|
+
allowedTools: READ_TOOLS,
|
|
71
|
+
instruction: "读取最小必要上下文,确认相关文件、场景、脚本和项目约束。只做事实收集,不修改文件。"
|
|
72
|
+
},
|
|
73
|
+
implement: {
|
|
74
|
+
id: "implement",
|
|
75
|
+
title: "实现修改",
|
|
76
|
+
toolGroup: "write",
|
|
77
|
+
skillId: "file.creator",
|
|
78
|
+
promptId: "godot.assistant",
|
|
79
|
+
toolBudget: "project_edit",
|
|
80
|
+
allowedTools: [...READ_TOOLS, ...WRITE_TOOLS],
|
|
81
|
+
instruction: "基于已收集上下文完成必要修改。优先小步修改,必须使用 create/overwrite/replace/apply/add/attach/connect/set/unset 等实际写入工具完成修改;这些写入工具会走审批系统。修改项目设置时先用 propose_* 预览,但不要把 propose_* 当作实现结果。"
|
|
82
|
+
},
|
|
83
|
+
review: {
|
|
84
|
+
id: "review",
|
|
85
|
+
title: "审查结果",
|
|
86
|
+
toolGroup: "verify",
|
|
87
|
+
skillId: "gdscript.review",
|
|
88
|
+
promptId: "gdscript.reviewer",
|
|
89
|
+
toolBudget: "normal",
|
|
90
|
+
allowedTools: [...READ_TOOLS, ...VERIFY_TOOLS],
|
|
91
|
+
instruction: "审查修改后的代码、场景和相邻调用。优先指出真实风险、回归和遗漏验证。默认不要写文件。"
|
|
92
|
+
},
|
|
93
|
+
verify: {
|
|
94
|
+
id: "verify",
|
|
95
|
+
title: "运行验证",
|
|
96
|
+
toolGroup: "verify",
|
|
97
|
+
toolBudget: "normal",
|
|
98
|
+
allowedTools: [...READ_TOOLS, ...VERIFY_TOOLS],
|
|
99
|
+
instruction: "运行可用的低成本验证。修改 .gd 后优先读取 LSP diagnostics,再运行 Godot check-only、类型检查或安全预设。记录通过、失败和未覆盖项。"
|
|
100
|
+
},
|
|
101
|
+
summarize: {
|
|
102
|
+
id: "summarize",
|
|
103
|
+
title: "总结交付",
|
|
104
|
+
toolGroup: "summarize",
|
|
105
|
+
promptId: "godot.assistant",
|
|
106
|
+
toolBudget: "simple",
|
|
107
|
+
allowedTools: [],
|
|
108
|
+
instruction: "只基于前面阶段的结果给用户最终总结。说明完成内容、验证状态、剩余风险和是否有审批未完成。不要再调用工具。"
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export function createWorkflowId(): string {
|
|
113
|
+
return `workflow-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function includesAny(text: string, terms: readonly string[]): boolean {
|
|
117
|
+
return terms.some((term: string): boolean => text.includes(term));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function createPhase(phaseId: FixedWorkflowPhaseId): WorkflowPhase {
|
|
121
|
+
const phase: WorkflowPhase = PHASE_TEMPLATES[phaseId];
|
|
122
|
+
return {
|
|
123
|
+
...phase,
|
|
124
|
+
allowedTools: [...phase.allowedTools]
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function createTodos(phases: WorkflowPhase[]): WorkflowTodoItem[] {
|
|
129
|
+
return phases.map((phase: WorkflowPhase): WorkflowTodoItem => ({
|
|
130
|
+
id: `${phase.id}-todo`,
|
|
131
|
+
phaseId: phase.id,
|
|
132
|
+
text: phase.title,
|
|
133
|
+
status: "pending"
|
|
134
|
+
}));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function createPlan(title: string, phaseIds: FixedWorkflowPhaseId[]): WorkflowPlan {
|
|
138
|
+
const phases: WorkflowPhase[] = phaseIds.map(createPhase);
|
|
139
|
+
return {
|
|
140
|
+
id: createWorkflowId(),
|
|
141
|
+
title,
|
|
142
|
+
phases,
|
|
143
|
+
todos: createTodos(phases),
|
|
144
|
+
source: "fixed",
|
|
145
|
+
revision: 0
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function createWorkflowTitle(message: string): string {
|
|
150
|
+
const normalized: string = message.replace(/\s+/g, " ").trim();
|
|
151
|
+
if (normalized.length <= 24) {
|
|
152
|
+
return normalized.length > 0 ? normalized : "多阶段任务";
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return `${normalized.slice(0, 24)}...`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function planWorkflow(params: AiChatParams): WorkflowPlan | null {
|
|
159
|
+
const workflowMode = params.options?.workflow ?? "auto";
|
|
160
|
+
if (workflowMode === "single" || workflowMode === "llm_planned") {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const text: string = params.message.toLowerCase();
|
|
165
|
+
const wantsReview: boolean = includesAny(text, ["审查", "检查", "review", "code review", "复查", "评审"]);
|
|
166
|
+
const wantsImplementation: boolean = includesAny(text, [
|
|
167
|
+
"完善",
|
|
168
|
+
"实现",
|
|
169
|
+
"修改",
|
|
170
|
+
"编写",
|
|
171
|
+
"写一个",
|
|
172
|
+
"写下",
|
|
173
|
+
"创建",
|
|
174
|
+
"新增",
|
|
175
|
+
"修复",
|
|
176
|
+
"改一下",
|
|
177
|
+
"生成",
|
|
178
|
+
"搭建",
|
|
179
|
+
"做一个"
|
|
180
|
+
]);
|
|
181
|
+
|
|
182
|
+
const title: string = createWorkflowTitle(params.message);
|
|
183
|
+
|
|
184
|
+
if (workflowMode === "multi_phase" && !wantsReview && !wantsImplementation) {
|
|
185
|
+
return createPlan(title, ["inspect", "implement", "verify", "summarize"]);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (wantsImplementation && wantsReview) {
|
|
189
|
+
return createPlan(title, ["inspect", "implement", "review", "verify", "summarize"]);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (wantsReview) {
|
|
193
|
+
return createPlan(title, ["inspect", "review", "summarize"]);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (wantsImplementation) {
|
|
197
|
+
return createPlan(title, ["inspect", "implement", "verify", "summarize"]);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return null;
|
|
201
|
+
}
|