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,141 @@
|
|
|
1
|
+
import type { AiChatParams } from "../protocol/types.js";
|
|
2
|
+
import type {
|
|
3
|
+
WorkflowPhase,
|
|
4
|
+
WorkflowPhaseOutput,
|
|
5
|
+
WorkflowPlan,
|
|
6
|
+
WorkflowTodoItem,
|
|
7
|
+
WorkflowTodoSnapshot,
|
|
8
|
+
WorkflowTodoStatus
|
|
9
|
+
} from "./types.js";
|
|
10
|
+
|
|
11
|
+
const MAX_PHASE_OUTPUT_CHARS: number = 5000;
|
|
12
|
+
|
|
13
|
+
export function createWorkflowTodoSnapshot(plan: WorkflowPlan): WorkflowTodoSnapshot {
|
|
14
|
+
return {
|
|
15
|
+
workflowId: plan.id,
|
|
16
|
+
title: plan.title,
|
|
17
|
+
source: plan.source,
|
|
18
|
+
revision: plan.revision,
|
|
19
|
+
phases: plan.phases.map((phase: WorkflowPhase) => ({
|
|
20
|
+
id: phase.id,
|
|
21
|
+
title: phase.title,
|
|
22
|
+
status: getPhaseStatus(plan, phase.id)
|
|
23
|
+
})),
|
|
24
|
+
todos: plan.todos.map((todo: WorkflowTodoItem) => ({ ...todo }))
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function updateWorkflowPhaseStatus(plan: WorkflowPlan, phaseId: string, status: WorkflowTodoStatus): WorkflowPlan {
|
|
29
|
+
return {
|
|
30
|
+
...plan,
|
|
31
|
+
todos: plan.todos.map((todo: WorkflowTodoItem): WorkflowTodoItem => (
|
|
32
|
+
todo.phaseId === phaseId ? { ...todo, status } : todo
|
|
33
|
+
))
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function markRemainingWorkflowTodos(plan: WorkflowPlan, status: WorkflowTodoStatus): WorkflowPlan {
|
|
38
|
+
return {
|
|
39
|
+
...plan,
|
|
40
|
+
todos: plan.todos.map((todo: WorkflowTodoItem): WorkflowTodoItem => (
|
|
41
|
+
todo.status === "done" ? todo : { ...todo, status }
|
|
42
|
+
))
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function createPhaseMessage(
|
|
47
|
+
originalParams: AiChatParams,
|
|
48
|
+
plan: WorkflowPlan,
|
|
49
|
+
phase: WorkflowPhase,
|
|
50
|
+
phaseOutputs: WorkflowPhaseOutput[]
|
|
51
|
+
): string {
|
|
52
|
+
const previousResults: string = phaseOutputs.length > 0
|
|
53
|
+
? phaseOutputs.map(formatPhaseOutput).join("\n\n")
|
|
54
|
+
: "(暂无上一阶段结果)";
|
|
55
|
+
|
|
56
|
+
return [
|
|
57
|
+
`当前执行工作流:${plan.title}`,
|
|
58
|
+
`当前阶段:${phase.title}(${phase.id})`,
|
|
59
|
+
"",
|
|
60
|
+
"## 用户原始需求",
|
|
61
|
+
originalParams.message,
|
|
62
|
+
"",
|
|
63
|
+
"## 当前阶段指令",
|
|
64
|
+
phase.instruction,
|
|
65
|
+
"",
|
|
66
|
+
"## 上一阶段结果",
|
|
67
|
+
previousResults,
|
|
68
|
+
"",
|
|
69
|
+
"请只完成当前阶段。不要跳过后续阶段,不要输出最终交付总结,除非当前阶段是最后一步或阶段指令明确要求总结。"
|
|
70
|
+
].join("\n");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function createPhaseParams(originalParams: AiChatParams, phase: WorkflowPhase, message: string, stream: boolean): AiChatParams {
|
|
74
|
+
return {
|
|
75
|
+
...originalParams,
|
|
76
|
+
message,
|
|
77
|
+
promptId: phase.promptId ?? originalParams.promptId,
|
|
78
|
+
skillId: phase.skillId,
|
|
79
|
+
options: {
|
|
80
|
+
...(originalParams.options ?? {}),
|
|
81
|
+
stream,
|
|
82
|
+
toolBudget: phase.toolBudget,
|
|
83
|
+
workflow: "single"
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function createPhasePrompt(phase: WorkflowPhase, skillPrompt: string, mcpSystemContext: string): string {
|
|
89
|
+
return [
|
|
90
|
+
"## 工作流阶段约束",
|
|
91
|
+
`- 当前阶段:${phase.title}(${phase.id})`,
|
|
92
|
+
`- 阶段目标:${phase.instruction}`,
|
|
93
|
+
"- 只完成当前阶段,不要提前总结整个任务。",
|
|
94
|
+
"- 如果需要写入或执行审批工具,按现有审批流程暂停。",
|
|
95
|
+
"- 当前阶段实际可用工具:",
|
|
96
|
+
...phase.allowedTools.map((toolName: string): string => ` - ${toolName}`),
|
|
97
|
+
"",
|
|
98
|
+
skillPrompt,
|
|
99
|
+
mcpSystemContext
|
|
100
|
+
].filter((part: string): boolean => part.length > 0).join("\n\n");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function appendPhaseOutput(outputs: WorkflowPhaseOutput[], phase: WorkflowPhase, text: string): WorkflowPhaseOutput[] {
|
|
104
|
+
const clippedText: string = text.length > MAX_PHASE_OUTPUT_CHARS
|
|
105
|
+
? `${text.slice(0, MAX_PHASE_OUTPUT_CHARS)}\n\n[阶段输出已截断,原始长度 ${text.length} 字符]`
|
|
106
|
+
: text;
|
|
107
|
+
|
|
108
|
+
return [
|
|
109
|
+
...outputs,
|
|
110
|
+
{
|
|
111
|
+
phaseId: phase.id,
|
|
112
|
+
title: phase.title,
|
|
113
|
+
text: clippedText
|
|
114
|
+
}
|
|
115
|
+
];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function formatPhaseOutput(output: WorkflowPhaseOutput): string {
|
|
119
|
+
return [
|
|
120
|
+
`### ${output.title}`,
|
|
121
|
+
output.text
|
|
122
|
+
].join("\n");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function getPhaseStatus(plan: WorkflowPlan, phaseId: string): WorkflowTodoStatus {
|
|
126
|
+
const phaseTodos: WorkflowTodoItem[] = plan.todos.filter((todo: WorkflowTodoItem): boolean => todo.phaseId === phaseId);
|
|
127
|
+
if (phaseTodos.some((todo: WorkflowTodoItem): boolean => todo.status === "failed")) {
|
|
128
|
+
return "failed";
|
|
129
|
+
}
|
|
130
|
+
if (phaseTodos.some((todo: WorkflowTodoItem): boolean => todo.status === "paused")) {
|
|
131
|
+
return "paused";
|
|
132
|
+
}
|
|
133
|
+
if (phaseTodos.some((todo: WorkflowTodoItem): boolean => todo.status === "running")) {
|
|
134
|
+
return "running";
|
|
135
|
+
}
|
|
136
|
+
if (phaseTodos.length > 0 && phaseTodos.every((todo: WorkflowTodoItem): boolean => todo.status === "done")) {
|
|
137
|
+
return "done";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return "pending";
|
|
141
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { AiChatParams, ChatMessage, PromptId } from "../protocol/types.js";
|
|
2
|
+
import type { SkillId } from "../skills/registry.js";
|
|
3
|
+
import type { ToolBudgetLevel } from "../tools/llm-tools.js";
|
|
4
|
+
|
|
5
|
+
export type WorkflowPhaseId = string;
|
|
6
|
+
|
|
7
|
+
export type WorkflowTodoStatus = "pending" | "running" | "done" | "failed" | "paused";
|
|
8
|
+
|
|
9
|
+
export type WorkflowSource = "fixed" | "llm";
|
|
10
|
+
|
|
11
|
+
export type WorkflowToolGroup = "read" | "write" | "verify" | "summarize";
|
|
12
|
+
|
|
13
|
+
export type WorkflowPhase = {
|
|
14
|
+
id: WorkflowPhaseId;
|
|
15
|
+
title: string;
|
|
16
|
+
toolGroup?: WorkflowToolGroup | undefined;
|
|
17
|
+
skillId?: SkillId | undefined;
|
|
18
|
+
promptId?: PromptId | undefined;
|
|
19
|
+
toolBudget: ToolBudgetLevel;
|
|
20
|
+
allowedTools: string[];
|
|
21
|
+
instruction: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type WorkflowTodoItem = {
|
|
25
|
+
id: string;
|
|
26
|
+
phaseId: WorkflowPhaseId;
|
|
27
|
+
text: string;
|
|
28
|
+
status: WorkflowTodoStatus;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type WorkflowPlan = {
|
|
32
|
+
id: string;
|
|
33
|
+
title: string;
|
|
34
|
+
phases: WorkflowPhase[];
|
|
35
|
+
todos: WorkflowTodoItem[];
|
|
36
|
+
source?: WorkflowSource | undefined;
|
|
37
|
+
revision?: number | undefined;
|
|
38
|
+
maxRevisions?: number | undefined;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type WorkflowPhaseOutput = {
|
|
42
|
+
phaseId: WorkflowPhaseId;
|
|
43
|
+
title: string;
|
|
44
|
+
text: string;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type WorkflowRunState = {
|
|
48
|
+
plan: WorkflowPlan;
|
|
49
|
+
phaseIndex: number;
|
|
50
|
+
phaseOutputs: WorkflowPhaseOutput[];
|
|
51
|
+
originalParams: AiChatParams;
|
|
52
|
+
history: ChatMessage[];
|
|
53
|
+
historyBudgetTokens: number;
|
|
54
|
+
planningContext?: string | undefined;
|
|
55
|
+
guidePromptSection?: string | undefined;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export type WorkflowTodoSnapshot = {
|
|
59
|
+
workflowId: string;
|
|
60
|
+
title: string;
|
|
61
|
+
source?: WorkflowSource | undefined;
|
|
62
|
+
revision?: number | undefined;
|
|
63
|
+
phases: Array<{
|
|
64
|
+
id: WorkflowPhaseId;
|
|
65
|
+
title: string;
|
|
66
|
+
status: WorkflowTodoStatus;
|
|
67
|
+
}>;
|
|
68
|
+
todos: WorkflowTodoItem[];
|
|
69
|
+
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { basename, resolve } from "node:path";
|
|
4
|
+
import { getDefaultWorkspaceConfigPath } from "../app-paths.js";
|
|
5
|
+
import type { WorkspaceConfig } from "./types.js";
|
|
6
|
+
|
|
7
|
+
let configuredWorkspaceCache: WorkspaceConfig[] | null = null;
|
|
8
|
+
const runtimeWorkspaces: Map<string, WorkspaceConfig> = new Map();
|
|
9
|
+
|
|
10
|
+
function loadConfiguredWorkspaces(): WorkspaceConfig[] {
|
|
11
|
+
if (configuredWorkspaceCache) {
|
|
12
|
+
return configuredWorkspaceCache;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const path: string = getDefaultWorkspaceConfigPath();
|
|
16
|
+
|
|
17
|
+
if (!existsSync(path)) {
|
|
18
|
+
console.warn(`[workspace] Config file not found: ${path}`);
|
|
19
|
+
configuredWorkspaceCache = [];
|
|
20
|
+
return configuredWorkspaceCache;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const raw: string = readFileSync(path, "utf8");
|
|
24
|
+
const parsed: unknown = JSON.parse(raw) as unknown;
|
|
25
|
+
|
|
26
|
+
if (!Array.isArray(parsed)) {
|
|
27
|
+
throw new Error(`Workspace config must be a JSON array: ${path}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
configuredWorkspaceCache = parsed as WorkspaceConfig[];
|
|
31
|
+
return configuredWorkspaceCache;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function createRuntimeWorkspace(rootPath: string, godotExecutablePath?: string | undefined): WorkspaceConfig {
|
|
35
|
+
const normalizedRootPath: string = resolve(rootPath);
|
|
36
|
+
const hash: string = createHash("sha1").update(normalizedRootPath.toLowerCase()).digest("hex").slice(0, 10);
|
|
37
|
+
const name: string = basename(normalizedRootPath) || normalizedRootPath;
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
id: `runtime-${hash}`,
|
|
41
|
+
name,
|
|
42
|
+
kind: "godot",
|
|
43
|
+
rootPath: normalizedRootPath,
|
|
44
|
+
godotExecutablePath
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function upsertRuntimeWorkspace(workspace: WorkspaceConfig): WorkspaceConfig {
|
|
49
|
+
const existing: WorkspaceConfig | undefined = runtimeWorkspaces.get(workspace.id);
|
|
50
|
+
const next: WorkspaceConfig = {
|
|
51
|
+
...existing,
|
|
52
|
+
...workspace,
|
|
53
|
+
godotExecutablePath: workspace.godotExecutablePath ?? existing?.godotExecutablePath
|
|
54
|
+
};
|
|
55
|
+
runtimeWorkspaces.set(next.id, next);
|
|
56
|
+
return next;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getEnvironmentWorkspace(): WorkspaceConfig | undefined {
|
|
60
|
+
if (!process.env.GODOT_PROJECT_PATH) {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return createRuntimeWorkspace(process.env.GODOT_PROJECT_PATH, process.env.GODOT_EXECUTABLE_PATH);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function loadWorkspaces(): WorkspaceConfig[] {
|
|
68
|
+
const byId: Map<string, WorkspaceConfig> = new Map();
|
|
69
|
+
|
|
70
|
+
for (const workspace of loadConfiguredWorkspaces()) {
|
|
71
|
+
byId.set(workspace.id, workspace);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const environmentWorkspace: WorkspaceConfig | undefined = getEnvironmentWorkspace();
|
|
75
|
+
if (environmentWorkspace && !byId.has(environmentWorkspace.id)) {
|
|
76
|
+
byId.set(environmentWorkspace.id, environmentWorkspace);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
for (const workspace of runtimeWorkspaces.values()) {
|
|
80
|
+
if (!byId.has(workspace.id)) {
|
|
81
|
+
byId.set(workspace.id, workspace);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return Array.from(byId.values());
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function findWorkspace(workspaceId: string): WorkspaceConfig | undefined {
|
|
89
|
+
return loadWorkspaces().find((w: WorkspaceConfig): boolean => w.id === workspaceId);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function getDefaultWorkspace(): WorkspaceConfig | undefined {
|
|
93
|
+
const workspaces: WorkspaceConfig[] = loadWorkspaces();
|
|
94
|
+
const defaultId: string | undefined = process.env.DEFAULT_WORKSPACE;
|
|
95
|
+
|
|
96
|
+
if (defaultId) {
|
|
97
|
+
const found: WorkspaceConfig | undefined = workspaces.find((w: WorkspaceConfig): boolean => w.id === defaultId);
|
|
98
|
+
if (found) {
|
|
99
|
+
return found;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return workspaces[0];
|
|
104
|
+
}
|