@fieldwangai/agentflow 0.1.25
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 +21 -0
- package/README.md +201 -0
- package/README.zh-CN.md +201 -0
- package/agents/agentflow-node-executor-code.md +32 -0
- package/agents/agentflow-node-executor-planning.md +32 -0
- package/agents/agentflow-node-executor-requirement.md +32 -0
- package/agents/agentflow-node-executor-test.md +32 -0
- package/agents/agentflow-node-executor-ui.md +32 -0
- package/agents/agentflow-node-executor.md +32 -0
- package/agents/agents.json +8 -0
- package/agents/en/agentflow-node-executor.md +32 -0
- package/agents/zh/agentflow-node-executor.md +32 -0
- package/bin/agentflow.mjs +52 -0
- package/bin/ensure-workspace-reference.mjs +35 -0
- package/bin/lib/agent-runners.mjs +1199 -0
- package/bin/lib/agents-path.mjs +61 -0
- package/bin/lib/api-runner.mjs +361 -0
- package/bin/lib/apply.mjs +852 -0
- package/bin/lib/catalog-agents.mjs +300 -0
- package/bin/lib/catalog-flows.mjs +532 -0
- package/bin/lib/composer-agent.mjs +884 -0
- package/bin/lib/composer-flow-instances.mjs +68 -0
- package/bin/lib/composer-flow-skeleton.mjs +334 -0
- package/bin/lib/composer-flow-validate.mjs +47 -0
- package/bin/lib/composer-log.mjs +197 -0
- package/bin/lib/composer-model-router.mjs +160 -0
- package/bin/lib/composer-node-schema.mjs +299 -0
- package/bin/lib/composer-planner.mjs +749 -0
- package/bin/lib/composer-script-ops.mjs +233 -0
- package/bin/lib/composer-skill-router.mjs +384 -0
- package/bin/lib/flow-import.mjs +305 -0
- package/bin/lib/flow-normalize.mjs +71 -0
- package/bin/lib/flow-write.mjs +395 -0
- package/bin/lib/help.mjs +139 -0
- package/bin/lib/hub-login.mjs +54 -0
- package/bin/lib/hub-publish.mjs +159 -0
- package/bin/lib/hub-remote.mjs +189 -0
- package/bin/lib/hub.mjs +299 -0
- package/bin/lib/i18n.mjs +233 -0
- package/bin/lib/locales/en.json +344 -0
- package/bin/lib/locales/zh.json +344 -0
- package/bin/lib/log.mjs +37 -0
- package/bin/lib/main.mjs +611 -0
- package/bin/lib/model-config.mjs +118 -0
- package/bin/lib/model-lists.mjs +188 -0
- package/bin/lib/node-exec-context.mjs +336 -0
- package/bin/lib/node-execute.mjs +513 -0
- package/bin/lib/normalize-node-tool-command.mjs +97 -0
- package/bin/lib/paths.mjs +216 -0
- package/bin/lib/pipeline-scripts.mjs +41 -0
- package/bin/lib/recent-runs.mjs +173 -0
- package/bin/lib/run-apply-active-lock.mjs +82 -0
- package/bin/lib/run-events.mjs +85 -0
- package/bin/lib/run-node-statuses-from-disk.mjs +85 -0
- package/bin/lib/schedule-config.mjs +227 -0
- package/bin/lib/scheduler.mjs +312 -0
- package/bin/lib/table.mjs +4 -0
- package/bin/lib/terminal.mjs +42 -0
- package/bin/lib/ui-print.mjs +94 -0
- package/bin/lib/ui-server.mjs +2113 -0
- package/bin/lib/workspace-tree.mjs +266 -0
- package/bin/lib/workspace.mjs +180 -0
- package/bin/pipeline/build-node-prompt.mjs +179 -0
- package/bin/pipeline/check-cache.mjs +191 -0
- package/bin/pipeline/check-flow.mjs +543 -0
- package/bin/pipeline/collect-nodes.mjs +212 -0
- package/bin/pipeline/compute-cache-md5.mjs +177 -0
- package/bin/pipeline/ensure-run-dir.mjs +71 -0
- package/bin/pipeline/extract-thinking.mjs +308 -0
- package/bin/pipeline/gc.mjs +129 -0
- package/bin/pipeline/get-env.mjs +83 -0
- package/bin/pipeline/get-exec-id.mjs +145 -0
- package/bin/pipeline/get-ready-nodes.mjs +435 -0
- package/bin/pipeline/get-resolved-values.mjs +337 -0
- package/bin/pipeline/load-key.mjs +62 -0
- package/bin/pipeline/parse-bool.mjs +33 -0
- package/bin/pipeline/parse-flow.mjs +698 -0
- package/bin/pipeline/post-process-control-if.mjs +23 -0
- package/bin/pipeline/post-process-node.mjs +490 -0
- package/bin/pipeline/pre-process-node.mjs +449 -0
- package/bin/pipeline/resolve-inputs.mjs +201 -0
- package/bin/pipeline/run-log.mjs +34 -0
- package/bin/pipeline/run-tool-nodejs.mjs +160 -0
- package/bin/pipeline/save-key.mjs +93 -0
- package/bin/pipeline/snapshot-prior-round.mjs +70 -0
- package/bin/pipeline/validate-flow.mjs +825 -0
- package/bin/pipeline/validate-for-ui.mjs +226 -0
- package/bin/pipeline/validate-script-output.mjs +130 -0
- package/bin/pipeline/write-result.mjs +182 -0
- package/builtin/nodes/agent_subAgent.md +14 -0
- package/builtin/nodes/control_agent_toBool.md +20 -0
- package/builtin/nodes/control_anyOne.md +17 -0
- package/builtin/nodes/control_end.md +11 -0
- package/builtin/nodes/control_if.md +20 -0
- package/builtin/nodes/control_start.md +11 -0
- package/builtin/nodes/control_toBool.md +21 -0
- package/builtin/nodes/provide_file.md +11 -0
- package/builtin/nodes/provide_str.md +11 -0
- package/builtin/nodes/tool_get_env.md +14 -0
- package/builtin/nodes/tool_load_key.md +20 -0
- package/builtin/nodes/tool_nodejs.md +40 -0
- package/builtin/nodes/tool_print.md +14 -0
- package/builtin/nodes/tool_save_key.md +20 -0
- package/builtin/nodes/tool_user_ask.md +23 -0
- package/builtin/nodes/tool_user_check.md +22 -0
- package/builtin/pipelines/module-migrate/flow.yaml +819 -0
- package/builtin/pipelines/module-migrate/scripts/check_imports.mjs +700 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Makefile +362 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/.deps/Release/obj.target/node_modules/node-addon-api/node_addon_api_except.stamp.d +1 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/.deps/Release/obj.target/tree_sitter_kotlin_binding/bindings/node/binding.o.d +17 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/.deps/Release/obj.target/tree_sitter_kotlin_binding/src/parser.o.d +5 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/.deps/Release/obj.target/tree_sitter_kotlin_binding/src/scanner.o.d +8 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/.deps/Release/tree_sitter_kotlin_binding.node.d +1 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/obj.target/node_modules/node-addon-api/node_addon_api_except.stamp +0 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/obj.target/tree_sitter_kotlin_binding/bindings/node/binding.o +0 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/obj.target/tree_sitter_kotlin_binding/src/parser.o +0 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/obj.target/tree_sitter_kotlin_binding/src/scanner.o +0 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/tree_sitter_kotlin_binding.node +0 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/binding.Makefile +6 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/gyp-mac-tool +768 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/node_modules/node-addon-api/node_addon_api.Makefile +6 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/node_modules/node-addon-api/node_addon_api.target.mk +122 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/node_modules/node-addon-api/node_addon_api_except.target.mk +126 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/node_modules/node-addon-api/node_addon_api_maybe.target.mk +122 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/tree_sitter_kotlin_binding.target.mk +203 -0
- package/builtin/pipelines/new/flow.yaml +545 -0
- package/builtin/pipelines/new/scripts/check-flow.mjs +9 -0
- package/builtin/pipelines/new/scripts/collect-nodes.mjs +211 -0
- package/builtin/pipelines/scripts/adjust-node-positions.mjs +113 -0
- package/builtin/web-ui/dist/agentflow-icon.svg +23 -0
- package/builtin/web-ui/dist/assets/index-CZkUPcXE.css +1 -0
- package/builtin/web-ui/dist/assets/index-DkkhNESc.js +190 -0
- package/builtin/web-ui/dist/index.html +24 -0
- package/package.json +67 -0
- package/reference/flow-control-capabilities.md +274 -0
- package/reference/flow-layout.md +84 -0
- package/reference/flow-prompt-handler-check.md +12 -0
- package/reference/flow-result-semantics.md +14 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { PACKAGE_AGENTS_DIR, getUserAgentsDirAbs } from "./paths.mjs";
|
|
4
|
+
|
|
5
|
+
/** 解析 agent 身份 prompt 路径:优先使用包内 agents/<subagent>.md,否则工作区 */
|
|
6
|
+
export function getAgentPath(workspaceRoot, subagent) {
|
|
7
|
+
const packagePath = path.join(PACKAGE_AGENTS_DIR, `${subagent}.md`);
|
|
8
|
+
if (fs.existsSync(packagePath)) return path.resolve(packagePath);
|
|
9
|
+
return path.join(getUserAgentsDirAbs(), `${subagent}.md`);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 读取 agent 文件内容并替换路径占位符为真实路径。
|
|
14
|
+
* 返回替换后的整段文本;若文件不存在则返回空字符串。
|
|
15
|
+
*/
|
|
16
|
+
export function loadAgentPromptWithReplacements(workspaceRoot, subagent, replacements) {
|
|
17
|
+
const agentPath = getAgentPath(workspaceRoot, subagent);
|
|
18
|
+
if (!fs.existsSync(agentPath)) return "";
|
|
19
|
+
let content = fs.readFileSync(agentPath, "utf8");
|
|
20
|
+
for (const [key, value] of Object.entries(replacements)) {
|
|
21
|
+
if (value != null && typeof value === "string") {
|
|
22
|
+
const placeholder = "${" + key + "}";
|
|
23
|
+
content = content.split(placeholder).join(value);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return content;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** 去掉 Markdown 顶部的 YAML frontmatter(--- ... ---),返回正文 */
|
|
30
|
+
export function stripYamlFrontmatter(content) {
|
|
31
|
+
if (!content || typeof content !== "string") return "";
|
|
32
|
+
const first = content.indexOf("---");
|
|
33
|
+
if (first !== 0) return content.trim();
|
|
34
|
+
const afterFirst = content.indexOf("---", 3);
|
|
35
|
+
if (afterFirst === -1) return content.trim();
|
|
36
|
+
return content.slice(afterFirst + 3).trim();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 从 agent .md 文件读取 frontmatter 的 name、description(简易解析) */
|
|
40
|
+
export function readAgentFrontmatter(filePath) {
|
|
41
|
+
if (!fs.existsSync(filePath)) return { name: null, description: null };
|
|
42
|
+
try {
|
|
43
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
44
|
+
const start = raw.indexOf("---");
|
|
45
|
+
if (start === -1) return { name: null, description: null };
|
|
46
|
+
const afterFirst = raw.indexOf("---", start + 3);
|
|
47
|
+
if (afterFirst === -1) return { name: null, description: null };
|
|
48
|
+
const block = raw.slice(start + 3, afterFirst).trim();
|
|
49
|
+
let name = null;
|
|
50
|
+
let description = null;
|
|
51
|
+
for (const line of block.split("\n")) {
|
|
52
|
+
const m = line.match(/^\s*name:\s*(.+)$/);
|
|
53
|
+
if (m) name = m[1].trim().replace(/^["']|["']$/g, "");
|
|
54
|
+
const d = line.match(/^\s*description:\s*(.+)$/);
|
|
55
|
+
if (d) description = d[1].trim().replace(/^["']|["']$/g, "");
|
|
56
|
+
}
|
|
57
|
+
return { name, description };
|
|
58
|
+
} catch (_) {
|
|
59
|
+
return { name: null, description: null };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 直调 AI API 运行器,绕过 Cursor/OpenCode CLI 冷启动开销。
|
|
3
|
+
*
|
|
4
|
+
* 支持的模型格式(在 flow.yaml 或模型配置中指定):
|
|
5
|
+
* api:openai/<model> — OpenAI 或兼容端点(Together/Groq/DeepSeek/Azure 等)
|
|
6
|
+
* api:anthropic/<model> — Anthropic Claude
|
|
7
|
+
*
|
|
8
|
+
* 相关环境变量:
|
|
9
|
+
* OPENAI_API_KEY — OpenAI 或兼容端点的 API key
|
|
10
|
+
* OPENAI_BASE_URL — 兼容端点基础 URL(默认 https://api.openai.com/v1)
|
|
11
|
+
* ANTHROPIC_API_KEY — Anthropic API key
|
|
12
|
+
* AGENTFLOW_API_MAX_ROUNDS — 工具调用最大轮数(默认 30)
|
|
13
|
+
* AGENTFLOW_API_MAX_TOKENS — 单次响应最大 token 数(默认 8192)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import fs from "fs";
|
|
17
|
+
import path from "path";
|
|
18
|
+
import { spawnSync } from "child_process";
|
|
19
|
+
|
|
20
|
+
import { loadAgentPromptWithReplacements, stripYamlFrontmatter } from "./agents-path.mjs";
|
|
21
|
+
import { appendRunLogLine } from "./run-events.mjs";
|
|
22
|
+
|
|
23
|
+
const DEFAULT_OPENAI_BASE = "https://api.openai.com/v1";
|
|
24
|
+
const MAX_TOOL_ROUNDS = parseInt(process.env.AGENTFLOW_API_MAX_ROUNDS ?? "30", 10) || 30;
|
|
25
|
+
const MAX_TOKENS = parseInt(process.env.AGENTFLOW_API_MAX_TOKENS ?? "8192", 10) || 8192;
|
|
26
|
+
|
|
27
|
+
// ─── 工具定义 ────────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
const TOOL_DEFS = [
|
|
30
|
+
{
|
|
31
|
+
name: "read_file",
|
|
32
|
+
description: "Read the contents of a file. Returns the file content as text.",
|
|
33
|
+
parameters: {
|
|
34
|
+
type: "object",
|
|
35
|
+
properties: {
|
|
36
|
+
path: { type: "string", description: "File path (absolute, or relative to workspace root)" },
|
|
37
|
+
},
|
|
38
|
+
required: ["path"],
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: "write_file",
|
|
43
|
+
description: "Write content to a file, creating parent directories as needed. Overwrites existing content.",
|
|
44
|
+
parameters: {
|
|
45
|
+
type: "object",
|
|
46
|
+
properties: {
|
|
47
|
+
path: { type: "string", description: "File path (absolute, or relative to workspace root)" },
|
|
48
|
+
content: { type: "string", description: "Full content to write" },
|
|
49
|
+
},
|
|
50
|
+
required: ["path", "content"],
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: "run_command",
|
|
55
|
+
description: "Run a shell command in the workspace root directory. Returns stdout, stderr and exit code.",
|
|
56
|
+
parameters: {
|
|
57
|
+
type: "object",
|
|
58
|
+
properties: {
|
|
59
|
+
command: { type: "string", description: "Shell command to execute" },
|
|
60
|
+
timeout_ms: { type: "number", description: "Timeout in milliseconds (default: 30000)" },
|
|
61
|
+
},
|
|
62
|
+
required: ["command"],
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: "list_dir",
|
|
67
|
+
description: "List files and subdirectories at a path.",
|
|
68
|
+
parameters: {
|
|
69
|
+
type: "object",
|
|
70
|
+
properties: {
|
|
71
|
+
path: { type: "string", description: "Directory path (absolute, or relative to workspace root)" },
|
|
72
|
+
},
|
|
73
|
+
required: ["path"],
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
/** OpenAI format: tools array */
|
|
79
|
+
function openAiTools() {
|
|
80
|
+
return TOOL_DEFS.map((t) => ({
|
|
81
|
+
type: "function",
|
|
82
|
+
function: { name: t.name, description: t.description, parameters: t.parameters },
|
|
83
|
+
}));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Anthropic format: tools array */
|
|
87
|
+
function anthropicTools() {
|
|
88
|
+
return TOOL_DEFS.map((t) => ({
|
|
89
|
+
name: t.name,
|
|
90
|
+
description: t.description,
|
|
91
|
+
input_schema: t.parameters,
|
|
92
|
+
}));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ─── 工具执行 ────────────────────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
function toAbs(workspaceRoot, p) {
|
|
98
|
+
return path.isAbsolute(p) ? p : path.resolve(workspaceRoot, p);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function executeTool(workspaceRoot, toolName, input) {
|
|
102
|
+
try {
|
|
103
|
+
switch (toolName) {
|
|
104
|
+
case "read_file": {
|
|
105
|
+
const abs = toAbs(workspaceRoot, input.path);
|
|
106
|
+
if (!fs.existsSync(abs)) return { error: `File not found: ${input.path}` };
|
|
107
|
+
const content = fs.readFileSync(abs, "utf-8");
|
|
108
|
+
return { content };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
case "write_file": {
|
|
112
|
+
const abs = toAbs(workspaceRoot, input.path);
|
|
113
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
114
|
+
fs.writeFileSync(abs, input.content, "utf-8");
|
|
115
|
+
return { ok: true, bytes_written: Buffer.byteLength(input.content, "utf-8") };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
case "run_command": {
|
|
119
|
+
const timeoutMs = typeof input.timeout_ms === "number" ? input.timeout_ms : 30000;
|
|
120
|
+
const r = spawnSync(input.command, [], {
|
|
121
|
+
cwd: workspaceRoot,
|
|
122
|
+
shell: true,
|
|
123
|
+
encoding: "utf-8",
|
|
124
|
+
timeout: timeoutMs,
|
|
125
|
+
});
|
|
126
|
+
return {
|
|
127
|
+
exit_code: r.status ?? -1,
|
|
128
|
+
stdout: (r.stdout ?? "").slice(0, 20000),
|
|
129
|
+
stderr: (r.stderr ?? "").slice(0, 5000),
|
|
130
|
+
timed_out: r.signal === "SIGTERM",
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
case "list_dir": {
|
|
135
|
+
const abs = toAbs(workspaceRoot, input.path);
|
|
136
|
+
if (!fs.existsSync(abs)) return { error: `Path not found: ${input.path}` };
|
|
137
|
+
const entries = fs.readdirSync(abs, { withFileTypes: true });
|
|
138
|
+
return {
|
|
139
|
+
items: entries.map((e) => ({ name: e.name, type: e.isDirectory() ? "dir" : "file" })),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
default:
|
|
144
|
+
return { error: `Unknown tool: ${toolName}` };
|
|
145
|
+
}
|
|
146
|
+
} catch (err) {
|
|
147
|
+
return { error: err.message };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ─── API 调用封装 ─────────────────────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
async function fetchOpenAi(apiKey, baseUrl, model, messages) {
|
|
154
|
+
const url = `${baseUrl.replace(/\/$/, "")}/chat/completions`;
|
|
155
|
+
const resp = await fetch(url, {
|
|
156
|
+
method: "POST",
|
|
157
|
+
headers: {
|
|
158
|
+
"Content-Type": "application/json",
|
|
159
|
+
Authorization: `Bearer ${apiKey}`,
|
|
160
|
+
},
|
|
161
|
+
body: JSON.stringify({
|
|
162
|
+
model,
|
|
163
|
+
messages,
|
|
164
|
+
tools: openAiTools(),
|
|
165
|
+
tool_choice: "auto",
|
|
166
|
+
max_tokens: MAX_TOKENS,
|
|
167
|
+
}),
|
|
168
|
+
});
|
|
169
|
+
if (!resp.ok) {
|
|
170
|
+
const txt = await resp.text().catch(() => "");
|
|
171
|
+
throw new Error(`OpenAI API ${resp.status}: ${txt.slice(0, 600)}`);
|
|
172
|
+
}
|
|
173
|
+
return resp.json();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function fetchAnthropic(apiKey, model, messages, systemPrompt) {
|
|
177
|
+
const resp = await fetch("https://api.anthropic.com/v1/messages", {
|
|
178
|
+
method: "POST",
|
|
179
|
+
headers: {
|
|
180
|
+
"Content-Type": "application/json",
|
|
181
|
+
"x-api-key": apiKey,
|
|
182
|
+
"anthropic-version": "2023-06-01",
|
|
183
|
+
},
|
|
184
|
+
body: JSON.stringify({
|
|
185
|
+
model,
|
|
186
|
+
system: systemPrompt,
|
|
187
|
+
messages,
|
|
188
|
+
tools: anthropicTools(),
|
|
189
|
+
max_tokens: MAX_TOKENS,
|
|
190
|
+
}),
|
|
191
|
+
});
|
|
192
|
+
if (!resp.ok) {
|
|
193
|
+
const txt = await resp.text().catch(() => "");
|
|
194
|
+
throw new Error(`Anthropic API ${resp.status}: ${txt.slice(0, 600)}`);
|
|
195
|
+
}
|
|
196
|
+
return resp.json();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ─── 工具调用循环 ─────────────────────────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
async function runOpenAiLoop(apiKey, baseUrl, model, systemPrompt, userContent, workspaceRoot, log, options) {
|
|
202
|
+
const messages = [
|
|
203
|
+
...(systemPrompt ? [{ role: "system", content: systemPrompt }] : []),
|
|
204
|
+
{ role: "user", content: userContent },
|
|
205
|
+
];
|
|
206
|
+
|
|
207
|
+
for (let round = 0; round < MAX_TOOL_ROUNDS; round++) {
|
|
208
|
+
log(`[api/openai] round ${round + 1}`);
|
|
209
|
+
const resp = await fetchOpenAi(apiKey, baseUrl, model, messages);
|
|
210
|
+
const choice = resp.choices?.[0];
|
|
211
|
+
if (!choice) throw new Error("OpenAI: no choices in response");
|
|
212
|
+
|
|
213
|
+
const msg = choice.message;
|
|
214
|
+
messages.push(msg);
|
|
215
|
+
|
|
216
|
+
// 向外广播助手文本(用于 spinner 显示)
|
|
217
|
+
if (options.onToolCall) {
|
|
218
|
+
const txt = typeof msg.content === "string" ? msg.content : "";
|
|
219
|
+
if (txt.trim()) options.onToolCall("assistant", txt.slice(0, 200));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (choice.finish_reason === "stop" || choice.finish_reason === "end_turn" || !msg.tool_calls?.length) {
|
|
223
|
+
log(`[api/openai] finished (${choice.finish_reason ?? "no-tool-calls"})`);
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const toolResults = [];
|
|
228
|
+
for (const tc of msg.tool_calls) {
|
|
229
|
+
const toolName = tc.function.name;
|
|
230
|
+
let toolInput = {};
|
|
231
|
+
try { toolInput = JSON.parse(tc.function.arguments); } catch (_) { /**/ }
|
|
232
|
+
|
|
233
|
+
log(`[api/openai] tool: ${toolName} ${JSON.stringify(toolInput).slice(0, 120)}`);
|
|
234
|
+
if (options.onToolCall) options.onToolCall("tool_call", toolName);
|
|
235
|
+
|
|
236
|
+
const result = executeTool(workspaceRoot, toolName, toolInput);
|
|
237
|
+
log(`[api/openai] tool result: ${JSON.stringify(result).slice(0, 200)}`);
|
|
238
|
+
|
|
239
|
+
toolResults.push({ role: "tool", tool_call_id: tc.id, content: JSON.stringify(result) });
|
|
240
|
+
}
|
|
241
|
+
messages.push(...toolResults);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function runAnthropicLoop(apiKey, model, systemPrompt, userContent, workspaceRoot, log, options) {
|
|
246
|
+
const messages = [{ role: "user", content: userContent }];
|
|
247
|
+
|
|
248
|
+
for (let round = 0; round < MAX_TOOL_ROUNDS; round++) {
|
|
249
|
+
log(`[api/anthropic] round ${round + 1}`);
|
|
250
|
+
const resp = await fetchAnthropic(apiKey, model, messages, systemPrompt);
|
|
251
|
+
|
|
252
|
+
// 助手消息
|
|
253
|
+
messages.push({ role: "assistant", content: resp.content });
|
|
254
|
+
|
|
255
|
+
if (options.onToolCall) {
|
|
256
|
+
const textBlock = resp.content?.find((b) => b.type === "text");
|
|
257
|
+
if (textBlock?.text) options.onToolCall("assistant", textBlock.text.slice(0, 200));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (resp.stop_reason === "end_turn" || resp.stop_reason === "stop_sequence") {
|
|
261
|
+
log(`[api/anthropic] finished (${resp.stop_reason})`);
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const toolUses = (resp.content ?? []).filter((b) => b.type === "tool_use");
|
|
266
|
+
if (!toolUses.length) {
|
|
267
|
+
log("[api/anthropic] finished (no tool_use blocks)");
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const toolResults = [];
|
|
272
|
+
for (const tb of toolUses) {
|
|
273
|
+
log(`[api/anthropic] tool: ${tb.name} ${JSON.stringify(tb.input).slice(0, 120)}`);
|
|
274
|
+
if (options.onToolCall) options.onToolCall("tool_call", tb.name);
|
|
275
|
+
|
|
276
|
+
const result = executeTool(workspaceRoot, tb.name, tb.input ?? {});
|
|
277
|
+
log(`[api/anthropic] tool result: ${JSON.stringify(result).slice(0, 200)}`);
|
|
278
|
+
|
|
279
|
+
toolResults.push({ type: "tool_result", tool_use_id: tb.id, content: JSON.stringify(result) });
|
|
280
|
+
}
|
|
281
|
+
messages.push({ role: "user", content: toolResults });
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ─── 公共解析函数 ─────────────────────────────────────────────────────────────
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* 从模型字符串中解析 provider 和 model。
|
|
289
|
+
* 例:
|
|
290
|
+
* "api:openai/gpt-4o" → { provider: "openai", model: "gpt-4o" }
|
|
291
|
+
* "api:anthropic/claude-opus-4-5" → { provider: "anthropic", model: "claude-opus-4-5" }
|
|
292
|
+
* "api:gpt-4o" → { provider: "openai", model: "gpt-4o" } (默认 openai)
|
|
293
|
+
*/
|
|
294
|
+
export function parseApiModel(str) {
|
|
295
|
+
const s = String(str ?? "").replace(/^api:/, "");
|
|
296
|
+
const slash = s.indexOf("/");
|
|
297
|
+
if (slash < 0) return { provider: "openai", model: s };
|
|
298
|
+
const provider = s.slice(0, slash).toLowerCase();
|
|
299
|
+
const model = s.slice(slash + 1);
|
|
300
|
+
return { provider, model };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// ─── 主入口 ───────────────────────────────────────────────────────────────────
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* 直调 AI API 执行节点,绕过 Cursor/OpenCode CLI。
|
|
307
|
+
*
|
|
308
|
+
* @param {string} workspaceRoot
|
|
309
|
+
* @param {{ promptPath: string, nodeContext?: string, taskBody?: string, subagent?: string, instanceId?: string }} nodeInfo
|
|
310
|
+
* @param {object} options
|
|
311
|
+
* model — "api:openai/gpt-4o" | "api:anthropic/claude-opus-4-5" 等
|
|
312
|
+
* flowName — 用于日志
|
|
313
|
+
* uuid — 用于日志
|
|
314
|
+
* onToolCall — (subtype: string, name: string) => void 供 spinner 展示
|
|
315
|
+
*/
|
|
316
|
+
export async function runApiAgentForNode(workspaceRoot, { promptPath, nodeContext, taskBody, subagent, instanceId }, options = {}) {
|
|
317
|
+
const absRoot = path.resolve(workspaceRoot);
|
|
318
|
+
const flowName = options.flowName ?? null;
|
|
319
|
+
const uuid = options.uuid ?? null;
|
|
320
|
+
|
|
321
|
+
const log = (msg) => {
|
|
322
|
+
if (flowName && uuid) appendRunLogLine(absRoot, flowName, uuid, "api-runner", msg);
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
// ── 解析 provider / model ──────────────────────────────────────────────────
|
|
326
|
+
const modelRaw = String(options.model ?? "api:openai/gpt-4o");
|
|
327
|
+
const { provider, model } = parseApiModel(modelRaw);
|
|
328
|
+
log(`start provider=${provider} model=${model} instanceId=${instanceId ?? "-"}`);
|
|
329
|
+
|
|
330
|
+
// ── 读取 Agent 角色提示,注入 nodeContext/taskBody ────────────────────────
|
|
331
|
+
const replacements = {
|
|
332
|
+
workspaceRoot: absRoot,
|
|
333
|
+
nodeContext: nodeContext ?? "",
|
|
334
|
+
taskBody: taskBody ?? "",
|
|
335
|
+
flowName: flowName ?? "",
|
|
336
|
+
uuid: uuid ?? "",
|
|
337
|
+
instanceId: instanceId ?? "",
|
|
338
|
+
};
|
|
339
|
+
const agentRaw = loadAgentPromptWithReplacements(workspaceRoot, subagent ?? "agentflow-node-executor", replacements);
|
|
340
|
+
const renderedBody = stripYamlFrontmatter(agentRaw);
|
|
341
|
+
|
|
342
|
+
// ── system/user 拆分:## 节点上下文 之前为角色指令,之后为任务内容 ─────────
|
|
343
|
+
const splitMarker = "## 节点上下文";
|
|
344
|
+
const splitIdx = renderedBody.indexOf(splitMarker);
|
|
345
|
+
const systemPrompt = splitIdx > 0 ? renderedBody.slice(0, splitIdx).trim() : renderedBody;
|
|
346
|
+
const userContent = `## 节点上下文\n\n${nodeContext ?? ""}\n\n## 执行任务\n\n${taskBody ?? ""}`;
|
|
347
|
+
|
|
348
|
+
// ── 确认 API Key ───────────────────────────────────────────────────────────
|
|
349
|
+
if (provider === "anthropic") {
|
|
350
|
+
const key = process.env.ANTHROPIC_API_KEY;
|
|
351
|
+
if (!key) throw new Error("[api-runner] ANTHROPIC_API_KEY is required for api:anthropic/* models");
|
|
352
|
+
await runAnthropicLoop(key, model, systemPrompt, userContent, absRoot, log, options);
|
|
353
|
+
} else {
|
|
354
|
+
const key = process.env.OPENAI_API_KEY;
|
|
355
|
+
if (!key) throw new Error("[api-runner] OPENAI_API_KEY is required for api:openai/* models");
|
|
356
|
+
const baseUrl = (process.env.OPENAI_BASE_URL ?? DEFAULT_OPENAI_BASE).trim();
|
|
357
|
+
await runOpenAiLoop(key, baseUrl, model, systemPrompt, userContent, absRoot, log, options);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
log(`done instanceId=${instanceId ?? "-"}`);
|
|
361
|
+
}
|