@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,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 从 flow.yaml 解析实例的 role / model,供 Composer 多步规划与路由使用。
|
|
3
|
+
*/
|
|
4
|
+
import yaml from "js-yaml";
|
|
5
|
+
import { translateRole, normalizeRoleToKey } from "./i18n.mjs";
|
|
6
|
+
|
|
7
|
+
const VALID_ROLES = ["requirement", "planning", "code", "test", "normal"];
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {string} [flowYaml]
|
|
11
|
+
* @returns {Record<string, { role: string, model?: string, label: string, definitionId?: string, hasScript?: boolean }>}
|
|
12
|
+
*/
|
|
13
|
+
export function parseInstanceRoleModelMap(flowYaml) {
|
|
14
|
+
const map = {};
|
|
15
|
+
if (!flowYaml || typeof flowYaml !== "string" || !flowYaml.trim()) return map;
|
|
16
|
+
try {
|
|
17
|
+
const raw = yaml.load(flowYaml);
|
|
18
|
+
const instances = raw?.instances && typeof raw.instances === "object" ? raw.instances : {};
|
|
19
|
+
for (const [id, inst] of Object.entries(instances)) {
|
|
20
|
+
if (!inst || typeof inst !== "object") continue;
|
|
21
|
+
const rawRole = inst.role != null ? String(inst.role).trim() : "";
|
|
22
|
+
const normalizedRole = normalizeRoleToKey(rawRole);
|
|
23
|
+
const role = VALID_ROLES.includes(normalizedRole) ? normalizedRole : "normal";
|
|
24
|
+
let model = inst.model != null ? String(inst.model).trim() : "";
|
|
25
|
+
if (model === "default") model = "";
|
|
26
|
+
const label = inst.label != null ? String(inst.label) : id;
|
|
27
|
+
const definitionId = inst.definitionId != null ? String(inst.definitionId).trim() : undefined;
|
|
28
|
+
const hasScript = definitionId === "tool_nodejs" && inst.script != null && String(inst.script).trim() !== "";
|
|
29
|
+
map[id] = { role, model: model || undefined, label, definitionId, hasScript };
|
|
30
|
+
}
|
|
31
|
+
} catch {
|
|
32
|
+
/* ignore */
|
|
33
|
+
}
|
|
34
|
+
return map;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 供规划器 user 消息附加:明确实例上的角色、模型与节点类型,便于输出带 instanceId / nodeRole 的步骤。
|
|
39
|
+
* @param {string} [flowYaml]
|
|
40
|
+
* @returns {string}
|
|
41
|
+
*/
|
|
42
|
+
export function formatInstancePlannerHint(flowYaml) {
|
|
43
|
+
const map = parseInstanceRoleModelMap(flowYaml);
|
|
44
|
+
const entries = Object.entries(map);
|
|
45
|
+
if (entries.length === 0) return "";
|
|
46
|
+
|
|
47
|
+
const nodejsMissing = [];
|
|
48
|
+
const lines = entries.map(([id, v]) => {
|
|
49
|
+
const m = v.model ? ` · 模型 \`${v.model}\`` : "";
|
|
50
|
+
const defHint = v.definitionId ? ` · \`${v.definitionId}\`` : "";
|
|
51
|
+
const scriptHint = v.definitionId === "tool_nodejs" && !v.hasScript ? " ⚠️ **缺 script**" : "";
|
|
52
|
+
if (v.definitionId === "tool_nodejs" && !v.hasScript) nodejsMissing.push(id);
|
|
53
|
+
return `- \`${id}\`(${v.label}):角色 **${translateRole(v.role)}**${defHint}${m}${scriptHint}`;
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
let warn = "";
|
|
57
|
+
if (nodejsMissing.length > 0) {
|
|
58
|
+
warn = `\n\n⚠️ **以下 tool_nodejs 节点缺少 script 字段**(无 script 则节点无法执行):${nodejsMissing.map(id => `\`${id}\``).join("、")}。` +
|
|
59
|
+
"必须为它们写入可执行的 `script`(shell/node 命令),或改用 `agent_subAgent`。`body` 不会被执行。";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
"\n## 画布实例(类型 / 角色 / 模型)\n" +
|
|
64
|
+
"agent 步骤请尽量填写 `instanceId`(本步主要操作的实例),`nodeRole` 应与该实例的 role 一致;可选用 `executorModel` 覆盖执行模型(否则使用该实例在 YAML 中的 model,再否则用用户全局模型)。\n" +
|
|
65
|
+
lines.join("\n") +
|
|
66
|
+
warn
|
|
67
|
+
);
|
|
68
|
+
}
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composer 阶段一 skeleton 生成器:在 AI 真正执行前,由脚本写入:
|
|
3
|
+
* 1. flow.yaml — 含 control_start + control_end + 一条主链 edge + nodePositions
|
|
4
|
+
* 2. composer-node-spec.md — 含三段 section 模板(整体框架/节点职责/计划数据槽)
|
|
5
|
+
*
|
|
6
|
+
* 设计原则:
|
|
7
|
+
* - **幂等**:只有当目标文件不存在 / 完全空 / instances 为空时才写入。
|
|
8
|
+
* 如已有内容,**不动**(避免覆盖用户工作)。
|
|
9
|
+
* - **最小骨架**:让 AI 只做"插入节点 + 调整边"而非"从零写 YAML"。
|
|
10
|
+
* - **正确 YAML**:start/end 槽位结构、type、name 严格对齐 schema。
|
|
11
|
+
*/
|
|
12
|
+
import fs from "fs";
|
|
13
|
+
import path from "path";
|
|
14
|
+
import yaml from "js-yaml";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 写入 flow.yaml skeleton(如已有非空 instances 则跳过)。
|
|
18
|
+
* @param {string} flowYamlAbs flow.yaml 绝对路径
|
|
19
|
+
* @returns {{ created: boolean, reason: string }}
|
|
20
|
+
*/
|
|
21
|
+
export function ensureFlowSkeleton(flowYamlAbs) {
|
|
22
|
+
if (!flowYamlAbs) return { created: false, reason: "missing flowYamlAbs" };
|
|
23
|
+
|
|
24
|
+
const exists = fs.existsSync(flowYamlAbs);
|
|
25
|
+
if (exists) {
|
|
26
|
+
try {
|
|
27
|
+
const raw = fs.readFileSync(flowYamlAbs, "utf-8");
|
|
28
|
+
const data = yaml.load(raw);
|
|
29
|
+
const instCount = data && data.instances && typeof data.instances === "object"
|
|
30
|
+
? Object.keys(data.instances).length
|
|
31
|
+
: 0;
|
|
32
|
+
if (instCount > 0) {
|
|
33
|
+
return { created: false, reason: `flow.yaml already has ${instCount} instances` };
|
|
34
|
+
}
|
|
35
|
+
} catch {
|
|
36
|
+
/* 解析失败也覆盖(视为坏文件) */
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
fs.mkdirSync(path.dirname(flowYamlAbs), { recursive: true });
|
|
41
|
+
|
|
42
|
+
const skeleton = {
|
|
43
|
+
instances: {
|
|
44
|
+
control_start: {
|
|
45
|
+
definitionId: "control_start",
|
|
46
|
+
label: "Start",
|
|
47
|
+
input: [],
|
|
48
|
+
output: [
|
|
49
|
+
{ type: "node", name: "next", value: "" },
|
|
50
|
+
],
|
|
51
|
+
body: "流程入口",
|
|
52
|
+
},
|
|
53
|
+
control_end: {
|
|
54
|
+
definitionId: "control_end",
|
|
55
|
+
label: "End",
|
|
56
|
+
input: [
|
|
57
|
+
{ type: "node", name: "prev", value: "" },
|
|
58
|
+
],
|
|
59
|
+
output: [],
|
|
60
|
+
body: "流程出口",
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
edges: [
|
|
64
|
+
{
|
|
65
|
+
source: "control_start",
|
|
66
|
+
target: "control_end",
|
|
67
|
+
sourceHandle: "output-0",
|
|
68
|
+
targetHandle: "input-0",
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
ui: {
|
|
72
|
+
nodePositions: {
|
|
73
|
+
control_start: { x: 100, y: 300 },
|
|
74
|
+
control_end: { x: 380, y: 300 },
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const text = yaml.dump(skeleton, { lineWidth: -1, noRefs: true });
|
|
80
|
+
fs.writeFileSync(flowYamlAbs, text, "utf-8");
|
|
81
|
+
return { created: true, reason: "wrote start+end skeleton" };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 写入 composer-node-spec.md 模板(如已存在则跳过)。
|
|
86
|
+
* @param {string} specMdAbs
|
|
87
|
+
* @param {{ flowId?: string, userRequest?: string }} [meta]
|
|
88
|
+
* @returns {{ created: boolean, reason: string }}
|
|
89
|
+
*/
|
|
90
|
+
export function ensureNodeSpecSkeleton(specMdAbs, meta = {}) {
|
|
91
|
+
if (!specMdAbs) return { created: false, reason: "missing specMdAbs" };
|
|
92
|
+
|
|
93
|
+
if (fs.existsSync(specMdAbs)) {
|
|
94
|
+
try {
|
|
95
|
+
const raw = fs.readFileSync(specMdAbs, "utf-8").trim();
|
|
96
|
+
if (raw.length > 0) {
|
|
97
|
+
return { created: false, reason: "spec.md already exists" };
|
|
98
|
+
}
|
|
99
|
+
} catch { /* fallthrough */ }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
fs.mkdirSync(path.dirname(specMdAbs), { recursive: true });
|
|
103
|
+
|
|
104
|
+
const flowId = meta.flowId || "(unknown)";
|
|
105
|
+
const userRequest = (meta.userRequest || "").trim() || "(未提供)";
|
|
106
|
+
const ts = new Date().toISOString();
|
|
107
|
+
|
|
108
|
+
const tmpl = `# composer-node-spec — ${flowId}
|
|
109
|
+
|
|
110
|
+
> 生成时间:${ts}
|
|
111
|
+
> 用户原始需求:
|
|
112
|
+
${userRequest.split("\n").map((l) => "> " + l).join("\n")}
|
|
113
|
+
|
|
114
|
+
## 整体框架
|
|
115
|
+
|
|
116
|
+
<!--
|
|
117
|
+
AI 在此填写整体设计意图:
|
|
118
|
+
- 主链节点序列(Start → ... → End)
|
|
119
|
+
- 分支结构(control_if 的真假分支去向)
|
|
120
|
+
- 循环结构(control_anyOne + control_toBool + control_if 的环路)
|
|
121
|
+
- 并行结构(如有)
|
|
122
|
+
- 全局数据(SaveKey / LoadKey 跨节点共享的 key)
|
|
123
|
+
-->
|
|
124
|
+
|
|
125
|
+
## 节点职责
|
|
126
|
+
|
|
127
|
+
<!--
|
|
128
|
+
按 instanceId 分小节,**每节点必须写齐 4 项**:
|
|
129
|
+
**职责**:一句话说该节点做什么
|
|
130
|
+
**输入**:列出每个 input 槽 → 语义 + 来源(上游 instanceId.slot)
|
|
131
|
+
**输出**:列出每个 output 槽 → 语义 + 去向(下游 instanceId.slot)
|
|
132
|
+
**实现要点**:tool_nodejs 写脚本路径(scripts/xxx.mjs)、agent_subAgent 写 body 关键点、control_* 写判定/汇合规则、provide_* 写固定值
|
|
133
|
+
说明:
|
|
134
|
+
- **输入/输出**就是阶段三连线的依据——把每个槽位的「上游来源」和「下游去向」写清,连边 sourceHandle/targetHandle 就一目了然
|
|
135
|
+
- 控制流槽(prev/next)也要写,标「控制流:来自 <upstream>.next」即可
|
|
136
|
+
- 业务数据槽(text/file)必须写清语义,让阶段二节点补充时知道 body/script 该用什么 \${slot} 占位符
|
|
137
|
+
- tool_nodejs 的可执行代码留到阶段二写,本阶段只需脚本路径与 I/O 语义
|
|
138
|
+
-->
|
|
139
|
+
|
|
140
|
+
### control_start
|
|
141
|
+
**职责**:流程入口
|
|
142
|
+
**输入**:—
|
|
143
|
+
**输出**:next:node → 给 <下一个节点>.prev(控制流,启动主链)
|
|
144
|
+
|
|
145
|
+
### control_end
|
|
146
|
+
**职责**:流程出口
|
|
147
|
+
**输入**:prev:node ← 来自 <最后一个节点>.next(控制流)
|
|
148
|
+
**输出**:—
|
|
149
|
+
|
|
150
|
+
<!-- 在此后追加每个新节点的小节 -->
|
|
151
|
+
|
|
152
|
+
## 计划数据槽(仅 ★ 可扩展节点需追加的业务槽)
|
|
153
|
+
|
|
154
|
+
<!--
|
|
155
|
+
本 section 是**完整业务数据槽设计**——是 spec.md 的权威记录与阶段二/三对账依据。
|
|
156
|
+
仅对 ★ 可扩展节点(agent_subAgent / tool_nodejs / tool_user_check)列出需要在 input/output 数组**末尾追加**的业务数据槽。
|
|
157
|
+
固定槽位节点(control_*、provide_*、tool_load_key、tool_save_key、tool_get_env、tool_print、tool_user_ask)槽位结构由 schema 锁死,**不在此列出**。
|
|
158
|
+
|
|
159
|
+
**重要:阶段一 AI 应该同时把这些槽落到 flow.yaml**(鼓励一次写到位)。
|
|
160
|
+
- 已落到 yaml → 阶段二按 name 去重跳过,不影响幂等
|
|
161
|
+
- 未落到 yaml → 阶段二脚本/agent 据此 section 补齐
|
|
162
|
+
|
|
163
|
+
格式(机器可解析):
|
|
164
|
+
<instanceId>:
|
|
165
|
+
input += [<name>:<text|file>, ...]
|
|
166
|
+
output += [<name>:<text|file>, ...]
|
|
167
|
+
type 仅可用 \`text\`(短串/路径/JSON)或 \`file\`(大块产物路径),**禁止 node**。
|
|
168
|
+
命名建议与上下游语义对齐(如下游 \`fromapp\` 的入参,上游就用同名 output)便于连线。
|
|
169
|
+
示例:
|
|
170
|
+
agent_analyze:
|
|
171
|
+
input += [fromapp:text, page_name:text]
|
|
172
|
+
output += [analysis:file]
|
|
173
|
+
agent_convert:
|
|
174
|
+
input += [analysis:file, toapp:text]
|
|
175
|
+
output += [converted_code:file]
|
|
176
|
+
agent_check_compile:
|
|
177
|
+
input += [converted_code:file]
|
|
178
|
+
output += [compile_result:text]
|
|
179
|
+
-->
|
|
180
|
+
`;
|
|
181
|
+
|
|
182
|
+
fs.writeFileSync(specMdAbs, tmpl, "utf-8");
|
|
183
|
+
return { created: true, reason: "wrote spec.md template" };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* 阶段一前置:同时确保 flow.yaml + spec.md 两份骨架。
|
|
188
|
+
* @param {{ flowYamlAbs: string, composerSpecAbs?: string, flowId?: string, userRequest?: string }} opts
|
|
189
|
+
* @returns {{ flow: ReturnType<typeof ensureFlowSkeleton>, spec: ReturnType<typeof ensureNodeSpecSkeleton> }}
|
|
190
|
+
*/
|
|
191
|
+
export function ensurePhase1Skeletons(opts) {
|
|
192
|
+
const flow = ensureFlowSkeleton(opts.flowYamlAbs);
|
|
193
|
+
const spec = opts.composerSpecAbs
|
|
194
|
+
? ensureNodeSpecSkeleton(opts.composerSpecAbs, { flowId: opts.flowId, userRequest: opts.userRequest })
|
|
195
|
+
: { created: false, reason: "missing composerSpecAbs" };
|
|
196
|
+
return { flow, spec };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ─── 解析 spec.md「计划数据槽」+ 合并到 flow.yaml ──────────────────────────
|
|
200
|
+
|
|
201
|
+
const VALID_BUSINESS_TYPES = new Set(["text", "file"]);
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* 从 spec.md 文本中解析「计划数据槽」section。
|
|
205
|
+
* 期望格式:
|
|
206
|
+
* ## 计划数据槽(...)
|
|
207
|
+
* <instanceId>:
|
|
208
|
+
* input += [<name>:<type>, <name>:<type>]
|
|
209
|
+
* output += [<name>:<type>]
|
|
210
|
+
* @param {string} specText
|
|
211
|
+
* @returns {Record<string, { input: Array<{name,type}>, output: Array<{name,type}> }>}
|
|
212
|
+
*/
|
|
213
|
+
export function parsePlannedSlotsFromSpec(specText) {
|
|
214
|
+
if (!specText || typeof specText !== "string") return {};
|
|
215
|
+
const idx = specText.search(/^##\s+计划数据槽/m);
|
|
216
|
+
if (idx < 0) return {};
|
|
217
|
+
const section = specText.slice(idx);
|
|
218
|
+
// 截到下一个 ## 或 EOF
|
|
219
|
+
const nextH = section.slice(2).search(/^##\s/m);
|
|
220
|
+
const body = nextH >= 0 ? section.slice(0, nextH + 2) : section;
|
|
221
|
+
|
|
222
|
+
const result = {};
|
|
223
|
+
// 按 instanceId 块分割:行首非缩进的 "<id>:" 起一个块
|
|
224
|
+
const lines = body.split("\n");
|
|
225
|
+
let currentId = null;
|
|
226
|
+
for (const rawLine of lines) {
|
|
227
|
+
const line = rawLine.replace(/\r$/, "");
|
|
228
|
+
// 跳过 HTML 注释、标题、空行
|
|
229
|
+
if (/^\s*$/.test(line)) continue;
|
|
230
|
+
if (/^<!--|-->\s*$|^##\s/.test(line)) continue;
|
|
231
|
+
|
|
232
|
+
const idMatch = /^\s{0,2}([A-Za-z][A-Za-z0-9_]*)\s*:\s*$/.exec(line);
|
|
233
|
+
if (idMatch) {
|
|
234
|
+
currentId = idMatch[1];
|
|
235
|
+
if (!result[currentId]) result[currentId] = { input: [], output: [] };
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
// input += [...] / output += [...]
|
|
239
|
+
const slotMatch = /^\s*(input|output)\s*\+?=\s*\[([^\]]*)\]\s*$/.exec(line);
|
|
240
|
+
if (slotMatch && currentId) {
|
|
241
|
+
const kind = slotMatch[1];
|
|
242
|
+
const items = slotMatch[2].split(",").map((s) => s.trim()).filter(Boolean);
|
|
243
|
+
for (const item of items) {
|
|
244
|
+
const m = /^([A-Za-z][A-Za-z0-9_]*)\s*:\s*([a-z]+)$/.exec(item);
|
|
245
|
+
if (!m) continue;
|
|
246
|
+
const name = m[1];
|
|
247
|
+
const type = m[2];
|
|
248
|
+
if (!VALID_BUSINESS_TYPES.has(type)) continue;
|
|
249
|
+
result[currentId][kind].push({ name, type });
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return result;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* 把 spec.md 解析出的业务槽合并到 flow.yaml(幂等:同 name 跳过)。
|
|
258
|
+
* 仅对 ★ 可扩展节点(agent_subAgent / tool_nodejs / tool_user_check)操作;
|
|
259
|
+
* 其它 definitionId 上的 planned 数据**忽略**(属于 spec.md 误写,不要破坏固定槽位)。
|
|
260
|
+
*
|
|
261
|
+
* @param {string} flowYamlAbs
|
|
262
|
+
* @param {string} specMdAbs
|
|
263
|
+
* @returns {{ ok: boolean, applied: Array<{instanceId, addedInputs: string[], addedOutputs: string[]}>, skipped: Array<{instanceId, reason: string}>, error?: string }}
|
|
264
|
+
*/
|
|
265
|
+
export function applyPlannedSlotsFromSpec(flowYamlAbs, specMdAbs) {
|
|
266
|
+
if (!flowYamlAbs || !fs.existsSync(flowYamlAbs)) return { ok: false, applied: [], skipped: [], error: "flow.yaml not found" };
|
|
267
|
+
if (!specMdAbs || !fs.existsSync(specMdAbs)) return { ok: false, applied: [], skipped: [], error: "spec.md not found" };
|
|
268
|
+
|
|
269
|
+
const EXTENSIBLE = new Set(["agent_subAgent", "tool_nodejs", "tool_user_check"]);
|
|
270
|
+
const applied = [];
|
|
271
|
+
const skipped = [];
|
|
272
|
+
|
|
273
|
+
let flow;
|
|
274
|
+
try {
|
|
275
|
+
flow = yaml.load(fs.readFileSync(flowYamlAbs, "utf-8"));
|
|
276
|
+
} catch (e) {
|
|
277
|
+
return { ok: false, applied: [], skipped: [], error: `failed to parse flow.yaml: ${e.message}` };
|
|
278
|
+
}
|
|
279
|
+
if (!flow || typeof flow !== "object" || !flow.instances) {
|
|
280
|
+
return { ok: false, applied: [], skipped: [], error: "flow.yaml has no instances" };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
let planned;
|
|
284
|
+
try {
|
|
285
|
+
const specText = fs.readFileSync(specMdAbs, "utf-8");
|
|
286
|
+
planned = parsePlannedSlotsFromSpec(specText);
|
|
287
|
+
} catch (e) {
|
|
288
|
+
return { ok: false, applied: [], skipped: [], error: `failed to parse spec.md: ${e.message}` };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
let mutated = false;
|
|
292
|
+
for (const [instanceId, plan] of Object.entries(planned)) {
|
|
293
|
+
const inst = flow.instances[instanceId];
|
|
294
|
+
if (!inst || typeof inst !== "object") {
|
|
295
|
+
skipped.push({ instanceId, reason: "instance not in flow.yaml" });
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
if (!EXTENSIBLE.has(inst.definitionId)) {
|
|
299
|
+
skipped.push({ instanceId, reason: `non-extensible definitionId: ${inst.definitionId}` });
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
if (!Array.isArray(inst.input)) inst.input = [];
|
|
303
|
+
if (!Array.isArray(inst.output)) inst.output = [];
|
|
304
|
+
|
|
305
|
+
const existingInputNames = new Set(inst.input.map((s) => s && s.name).filter(Boolean));
|
|
306
|
+
const existingOutputNames = new Set(inst.output.map((s) => s && s.name).filter(Boolean));
|
|
307
|
+
const addedInputs = [];
|
|
308
|
+
const addedOutputs = [];
|
|
309
|
+
|
|
310
|
+
for (const { name, type } of plan.input) {
|
|
311
|
+
if (existingInputNames.has(name)) continue;
|
|
312
|
+
inst.input.push({ type, name, value: "" });
|
|
313
|
+
existingInputNames.add(name);
|
|
314
|
+
addedInputs.push(`${name}:${type}`);
|
|
315
|
+
mutated = true;
|
|
316
|
+
}
|
|
317
|
+
for (const { name, type } of plan.output) {
|
|
318
|
+
if (existingOutputNames.has(name)) continue;
|
|
319
|
+
inst.output.push({ type, name, value: "" });
|
|
320
|
+
existingOutputNames.add(name);
|
|
321
|
+
addedOutputs.push(`${name}:${type}`);
|
|
322
|
+
mutated = true;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (addedInputs.length > 0 || addedOutputs.length > 0) {
|
|
326
|
+
applied.push({ instanceId, addedInputs, addedOutputs });
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (mutated) {
|
|
331
|
+
fs.writeFileSync(flowYamlAbs, yaml.dump(flow, { lineWidth: -1, noRefs: true }), "utf-8");
|
|
332
|
+
}
|
|
333
|
+
return { ok: true, applied, skipped };
|
|
334
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web UI Composer:对当前 flow.yaml 调用与 CLI 一致的 validate-flow 逻辑。
|
|
3
|
+
* agent 有时会把 flow.yaml 写到 workspace 路径而非 user 路径(或反之),
|
|
4
|
+
* 当原路径找不到时,尝试在 workspace 下的 .workspace/agentflow/pipelines/ 和
|
|
5
|
+
* agentflow/pipelines/ 中查找同名目录。
|
|
6
|
+
*/
|
|
7
|
+
import fs from "fs";
|
|
8
|
+
import path from "path";
|
|
9
|
+
import { runValidateFlow } from "../pipeline/validate-flow.mjs";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {string} flowYamlAbs
|
|
13
|
+
* @param {string} workspaceRoot
|
|
14
|
+
* @returns {{ ok: boolean, errors: string[], warnings: string[] }}
|
|
15
|
+
*/
|
|
16
|
+
export function validateComposerFlowYaml(flowYamlAbs, workspaceRoot) {
|
|
17
|
+
let flowDir = path.dirname(path.resolve(flowYamlAbs));
|
|
18
|
+
const root = path.resolve(workspaceRoot);
|
|
19
|
+
|
|
20
|
+
if (!fs.existsSync(path.join(flowDir, "flow.yaml"))) {
|
|
21
|
+
const flowId = path.basename(flowDir);
|
|
22
|
+
const candidates = [
|
|
23
|
+
path.join(root, ".workspace", "agentflow", "pipelines", flowId),
|
|
24
|
+
path.join(root, "agentflow", "pipelines", flowId),
|
|
25
|
+
path.join(root, ".cursor", "agentflow", "pipelines", flowId),
|
|
26
|
+
];
|
|
27
|
+
for (const c of candidates) {
|
|
28
|
+
if (fs.existsSync(path.join(c, "flow.yaml"))) {
|
|
29
|
+
flowDir = c;
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return runValidateFlow(flowDir, root);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {{ ok?: boolean, errors?: string[] }} result
|
|
40
|
+
* @returns {string}
|
|
41
|
+
*/
|
|
42
|
+
export function formatValidationErrorsBlock(result) {
|
|
43
|
+
if (!result) return "(校验无返回)";
|
|
44
|
+
const errs = Array.isArray(result.errors) ? result.errors : [];
|
|
45
|
+
if (errs.length === 0) return "(无 errors 字段,请检查 flow.yaml 结构)";
|
|
46
|
+
return errs.map((e, i) => `${i + 1}. ${e}`).join("\n");
|
|
47
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composer AI 对话日志记录模块。
|
|
3
|
+
*
|
|
4
|
+
* 将 UI 上 AI 对话生成 flow 的过程持久化到磁盘,便于调试。
|
|
5
|
+
*
|
|
6
|
+
* 日志位置:<workspaceRoot>/.workspace/agentflow/composer-logs/<YYYY-MM>/<YYYY-MM-DD_HHMMSS_<short-uuid>.log>
|
|
7
|
+
* 格式:[ISO8601] [tag] JSON event or message
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import fs from "fs";
|
|
11
|
+
import path from "path";
|
|
12
|
+
|
|
13
|
+
const COMPOSER_LOGS_DIR = "composer-logs";
|
|
14
|
+
|
|
15
|
+
function getComposerLogsRoot(workspaceRoot) {
|
|
16
|
+
const root = workspaceRoot && String(workspaceRoot).trim() !== ""
|
|
17
|
+
? path.resolve(String(workspaceRoot))
|
|
18
|
+
: process.cwd();
|
|
19
|
+
return path.join(root, ".workspace/agentflow", COMPOSER_LOGS_DIR);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function generateShortUuid() {
|
|
23
|
+
return Math.random().toString(36).slice(2, 8);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function formatTimestampForFilename() {
|
|
27
|
+
const now = new Date();
|
|
28
|
+
const yyyy = now.getFullYear();
|
|
29
|
+
const mm = String(now.getMonth() + 1).padStart(2, "0");
|
|
30
|
+
const dd = String(now.getDate()).padStart(2, "0");
|
|
31
|
+
const hh = String(now.getHours()).padStart(2, "0");
|
|
32
|
+
const min = String(now.getMinutes()).padStart(2, "0");
|
|
33
|
+
const ss = String(now.getSeconds()).padStart(2, "0");
|
|
34
|
+
return `${yyyy}-${mm}-${dd}_${hh}${min}${ss}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getMonthDir() {
|
|
38
|
+
const now = new Date();
|
|
39
|
+
const yyyy = now.getFullYear();
|
|
40
|
+
const mm = String(now.getMonth() + 1).padStart(2, "0");
|
|
41
|
+
return `${yyyy}-${mm}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 创建新的 Composer 对话日志会话。
|
|
46
|
+
* @param {string} workspaceRoot 工作区根目录
|
|
47
|
+
* @returns {{ sessionId: string, logPath: string, monthDir: string }}
|
|
48
|
+
*/
|
|
49
|
+
export function createComposerSession(workspaceRoot) {
|
|
50
|
+
const logsRoot = getComposerLogsRoot(workspaceRoot);
|
|
51
|
+
const monthDir = getMonthDir();
|
|
52
|
+
const monthPath = path.join(logsRoot, monthDir);
|
|
53
|
+
|
|
54
|
+
fs.mkdirSync(monthPath, { recursive: true });
|
|
55
|
+
|
|
56
|
+
const ts = formatTimestampForFilename();
|
|
57
|
+
const shortUuid = generateShortUuid();
|
|
58
|
+
const sessionId = `${ts}_${shortUuid}`;
|
|
59
|
+
const logPath = path.join(monthPath, `${sessionId}.log`);
|
|
60
|
+
|
|
61
|
+
return { sessionId, logPath, monthDir };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 向 Composer 日志文件追加一行事件。
|
|
66
|
+
* @param {string} logPath 日志文件绝对路径
|
|
67
|
+
* @param {string} tag 事件标签:composer-start | classify | plan | step-start | step-progress | step-done | validation | phase-plan | phase-complete | phase-auto-continue | natural | error | composer-done
|
|
68
|
+
* @param {object|string} payload 事件内容(对象会 JSON.stringify)
|
|
69
|
+
*/
|
|
70
|
+
export function logComposerEvent(logPath, tag, payload) {
|
|
71
|
+
if (!logPath) return;
|
|
72
|
+
try {
|
|
73
|
+
const ts = new Date().toISOString();
|
|
74
|
+
const text = typeof payload === "string" ? payload : JSON.stringify(payload);
|
|
75
|
+
const line = `[${ts}] [${tag}] ${text}\n`;
|
|
76
|
+
fs.appendFileSync(logPath, line, "utf-8");
|
|
77
|
+
} catch (e) {
|
|
78
|
+
process.stderr.write(`[composer-log] 写入失败: ${e.message}\n`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 截断文本以便日志记录(避免日志文件过大)。
|
|
84
|
+
* @param {string} text 原始文本
|
|
85
|
+
* @param {number} maxLen 最大长度(默认 2000)
|
|
86
|
+
* @returns {string}
|
|
87
|
+
*/
|
|
88
|
+
export function truncateForLog(text, maxLen = 2000) {
|
|
89
|
+
if (!text || typeof text !== "string") return "";
|
|
90
|
+
if (text.length <= maxLen) return text;
|
|
91
|
+
return text.slice(0, maxLen) + "…(截断)";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 获取 Composer 日志目录路径(供外部查询)。
|
|
96
|
+
* @param {string} workspaceRoot 工作区根目录
|
|
97
|
+
* @returns {string}
|
|
98
|
+
*/
|
|
99
|
+
export function getComposerLogsDir(workspaceRoot) {
|
|
100
|
+
return getComposerLogsRoot(workspaceRoot);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 列出最近的 Composer 日志会话。
|
|
105
|
+
* @param {string} workspaceRoot 工作区根目录
|
|
106
|
+
* @param {number} limit 最大数量(默认 20)
|
|
107
|
+
* @returns {Array<{ sessionId: string, logPath: string, monthDir: string, size: number, mtime: Date }>}
|
|
108
|
+
*/
|
|
109
|
+
export function listRecentComposerSessions(workspaceRoot, limit = 20) {
|
|
110
|
+
const logsRoot = getComposerLogsRoot(workspaceRoot);
|
|
111
|
+
if (!fs.existsSync(logsRoot)) return [];
|
|
112
|
+
|
|
113
|
+
const sessions = [];
|
|
114
|
+
const monthDirs = fs.readdirSync(logsRoot).filter((d) => /^\d{4}-\d{2}$/.test(d));
|
|
115
|
+
|
|
116
|
+
for (const monthDir of monthDirs) {
|
|
117
|
+
const monthPath = path.join(logsRoot, monthDir);
|
|
118
|
+
if (!fs.statSync(monthPath).isDirectory()) continue;
|
|
119
|
+
|
|
120
|
+
const files = fs.readdirSync(monthPath).filter((f) => f.endsWith(".log"));
|
|
121
|
+
for (const file of files) {
|
|
122
|
+
const logPath = path.join(monthPath, file);
|
|
123
|
+
const stat = fs.statSync(logPath);
|
|
124
|
+
sessions.push({
|
|
125
|
+
sessionId: file.replace(".log", ""),
|
|
126
|
+
logPath,
|
|
127
|
+
monthDir,
|
|
128
|
+
size: stat.size,
|
|
129
|
+
mtime: stat.mtime,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
sessions.sort((a, b) => b.mtime - a.mtime);
|
|
135
|
+
return sessions.slice(0, limit);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 解析日志文件为事件数组。每行格式:[ISO8601] [tag] {json} 或 string。
|
|
140
|
+
* 多行字段(含换行的 prompt/response)通过 JSON 序列化保留为 \n,所以每行始终是单条事件。
|
|
141
|
+
* @param {string} logPath
|
|
142
|
+
* @returns {Array<{ ts: string, tag: string, payload: any }>}
|
|
143
|
+
*/
|
|
144
|
+
export function parseComposerLogFile(logPath) {
|
|
145
|
+
if (!logPath || !fs.existsSync(logPath)) return [];
|
|
146
|
+
const raw = fs.readFileSync(logPath, "utf-8");
|
|
147
|
+
const events = [];
|
|
148
|
+
const lines = raw.split("\n");
|
|
149
|
+
for (const line of lines) {
|
|
150
|
+
if (!line) continue;
|
|
151
|
+
const m = line.match(/^\[([^\]]+)\]\s+\[([^\]]+)\]\s+(.*)$/);
|
|
152
|
+
if (!m) continue;
|
|
153
|
+
const ts = m[1];
|
|
154
|
+
const tag = m[2];
|
|
155
|
+
const rest = m[3];
|
|
156
|
+
let payload;
|
|
157
|
+
try {
|
|
158
|
+
payload = JSON.parse(rest);
|
|
159
|
+
} catch {
|
|
160
|
+
payload = rest;
|
|
161
|
+
}
|
|
162
|
+
events.push({ ts, tag, payload });
|
|
163
|
+
}
|
|
164
|
+
return events;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* 从日志文件首行尝试提取 composer-start 中的元数据(flowId, model, prompt 等)。
|
|
169
|
+
* @param {string} logPath
|
|
170
|
+
* @returns {{ flowId: string|null, flowSource: string|null, model: string|null, prompt: string|null, sessionId: string|null }}
|
|
171
|
+
*/
|
|
172
|
+
export function readComposerSessionMeta(logPath) {
|
|
173
|
+
const empty = { flowId: null, flowSource: null, model: null, prompt: null, sessionId: null };
|
|
174
|
+
try {
|
|
175
|
+
if (!fs.existsSync(logPath)) return empty;
|
|
176
|
+
const raw = fs.readFileSync(logPath, "utf-8");
|
|
177
|
+
const lines = raw.split("\n");
|
|
178
|
+
for (const line of lines) {
|
|
179
|
+
const m = line.match(/^\[([^\]]+)\]\s+\[composer-start\]\s+(.*)$/);
|
|
180
|
+
if (m) {
|
|
181
|
+
try {
|
|
182
|
+
const obj = JSON.parse(m[2]);
|
|
183
|
+
return {
|
|
184
|
+
flowId: obj.flowId || null,
|
|
185
|
+
flowSource: obj.flowSource || null,
|
|
186
|
+
model: obj.model || null,
|
|
187
|
+
prompt: obj.prompt || null,
|
|
188
|
+
sessionId: obj.sessionId || null,
|
|
189
|
+
};
|
|
190
|
+
} catch {
|
|
191
|
+
return empty;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
} catch { /* ignore */ }
|
|
196
|
+
return empty;
|
|
197
|
+
}
|