@fieldwangai/agentflow 0.1.159 → 0.1.162
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/bin/lib/auth.mjs +231 -14
- package/bin/lib/catalog-flows.mjs +12 -1
- package/bin/lib/cli-auth-page.mjs +103 -0
- package/bin/lib/control-while.mjs +336 -0
- package/bin/lib/flow-dsl/codegen.mjs +74 -11
- package/bin/lib/flow-dsl/ir.mjs +33 -2
- package/bin/lib/flow-dsl/lint.mjs +128 -3
- package/bin/lib/flow-dsl/parser.mjs +168 -4
- package/bin/lib/marketplace.mjs +10 -2
- package/bin/lib/node-package-manifest.mjs +7 -2
- package/bin/lib/node-ui-kit.mjs +156 -0
- package/bin/lib/paths.mjs +2 -0
- package/bin/lib/skill-runtime.mjs +6 -0
- package/bin/lib/table.mjs +2 -3
- package/bin/lib/ui-server.mjs +220 -17
- package/bin/lib/workspace-draft.mjs +74 -0
- package/bin/lib/workspace-flow-store.mjs +11 -2
- package/bin/lib/workspace-graph-merge.mjs +3 -0
- package/bin/lib/workspace-routes.mjs +307 -5
- package/bin/lib/workspace-server.mjs +598 -11
- package/bin/pipeline/validate-flow.mjs +3 -3
- package/builtin/nodes/control_parse_json.md +25 -0
- package/builtin/nodes/control_subflow_call.md +27 -0
- package/builtin/nodes/control_while.md +124 -0
- package/builtin/nodes/provide_json.md +14 -0
- package/builtin/nodes/workspace_subflow_input.md +14 -0
- package/builtin/pipelines/new/workspace.flow.js +1 -1
- package/builtin/pipelines/subflow-preview/workspace.flow.js +29 -0
- package/builtin/pipelines/subflow-preview/workspace.layout.json +38 -0
- package/builtin/pipelines/subflow-preview/workspace.nodes.json +19 -0
- package/builtin/pipelines/subflow-preview/workspace.state.json +93 -0
- package/builtin/pipelines/while-subflow-preview/workspace.flow.js +55 -0
- package/builtin/pipelines/while-subflow-preview/workspace.layout.json +66 -0
- package/builtin/pipelines/while-subflow-preview/workspace.nodes.json +49 -0
- package/builtin/pipelines/while-subflow-preview/workspace.state.json +194 -0
- package/builtin/web-ui/dist/assets/{WorkflowAssistantThread-CY19DsYq.js → WorkflowAssistantThread-CTrXOZ00.js} +1 -1
- package/builtin/web-ui/dist/assets/index-5uJFccdX.css +1 -0
- package/builtin/web-ui/dist/assets/index-BeUfNQRL.js +873 -0
- package/builtin/web-ui/dist/index.html +2 -2
- package/package.json +6 -1
- package/reference/flow-control-capabilities.md +41 -16
- package/shared/slot-types.js +58 -0
- package/skills/agentflow-author-flow/SKILL.md +81 -8
- package/skills/agentflow-author-flow/agents/openai.yaml +3 -3
- package/skills/agentflow-cli/SKILL.md +130 -25
- package/skills/agentflow-cli/agents/openai.yaml +2 -2
- package/skills/agentflow-cli/runtime/bin/lib/skill-runtime.mjs +14765 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/agent_subAgent.md +34 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/control_agent_toBool.md +26 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/control_anyOne.md +19 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/control_cancelled.md +26 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/control_cd_workspace.md +50 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/control_delay.md +31 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/control_end.md +13 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/control_if.md +21 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/control_interval_loop.md +55 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/control_load_mcp.md +24 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/control_load_skills.md +35 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/control_parse_json.md +25 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/control_start.md +13 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/control_subflow_call.md +27 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/control_toBool.md +27 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/control_user_workspace.md +24 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/control_wait_until.md +34 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/control_while.md +124 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/display_ascii.md +24 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/display_chart.md +32 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/display_code.md +39 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/display_html.md +32 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/display_image.md +36 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/display_markdown.md +24 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/display_mermaid.md +24 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/display_react_app.md +53 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/display_table.md +32 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/provide_bool.md +14 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/provide_file.md +14 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/provide_json.md +14 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/provide_password.md +14 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/provide_str.md +14 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/tool_display_share_link.md +47 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/tool_get_env.md +20 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/tool_git_checkout.md +70 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/tool_git_worktree_load.md +73 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/tool_git_worktree_unload.md +52 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/tool_gitlab_create_mr.md +114 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/tool_jenkins_build.md +67 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/tool_load_key.md +26 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/tool_nodejs.md +52 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/tool_print.md +21 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/tool_save_key.md +26 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/tool_set_run_env.md +38 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/tool_user_ask.md +27 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/tool_user_check.md +26 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/tool_wecom_send_app_markdown.md +57 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/tool_wecom_send_group_markdown.md +45 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/workspace_one_click_task.md +44 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/workspace_run.md +16 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/workspace_scheduled_run.md +16 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/workspace_subflow_input.md +14 -0
- package/skills/agentflow-cli/runtime/package.json +6 -0
- package/skills/agentflow-cli/scripts/agentflow-auth-store.mjs +113 -0
- package/skills/agentflow-cli/scripts/agentflow-cli.mjs +393 -28
- package/skills/agentflow-cli/scripts/agentflow-runtime.mjs +11 -4
- package/skills/agentflow-flow-add-instances/SKILL.md +2 -2
- package/skills/agentflow-flow-dsl/SKILL.md +83 -10
- package/skills/agentflow-flow-dsl/agents/openai.yaml +2 -2
- package/skills/agentflow-flow-dsl/references/node-calls.md +3 -0
- package/skills/agentflow-flow-dsl/references/subflow-authoring.md +230 -0
- package/skills/agentflow-flow-edit-node-fields/SKILL.md +3 -3
- package/skills/agentflow-flow-recipes/SKILL.md +1 -1
- package/skills/agentflow-flow-recipes/references/recipes.md +1 -2
- package/skills/agentflow-flow-sync-ui/SKILL.md +1 -1
- package/skills/agentflow-node-dsl/SKILL.md +44 -13
- package/skills/agentflow-node-reference/references/builtin-nodes.md +25 -0
- package/skills/agentflow-workspace-markdown/SKILL.md +1 -1
- package/builtin/web-ui/dist/assets/index-B9ppXv7e.css +0 -1
- package/builtin/web-ui/dist/assets/index-CdEQWRrp.js +0 -872
- package/skills/agentflow-workspace-graph/SKILL.md +0 -85
package/bin/lib/ui-server.mjs
CHANGED
|
@@ -52,6 +52,7 @@ import { packageResolverFor, scanAvailableNodePackages } from "./flow-dsl/packag
|
|
|
52
52
|
import { runStartupStorageMigrations } from "./startup-storage-migrations.mjs";
|
|
53
53
|
import { getPipelineFiles } from "./workspace-tree.mjs";
|
|
54
54
|
import { listExpiredWorkspacePreviews } from "./workspace-preview.mjs";
|
|
55
|
+
import { listExpiredWorkspaceDrafts } from "./workspace-draft.mjs";
|
|
55
56
|
import { LEGACY_FLOW_EXECUTION_DISABLED, LEGACY_FLOW_EXECUTION_MESSAGE } from "./legacy-flow-execution.mjs";
|
|
56
57
|
import {
|
|
57
58
|
listRecentComposerSessions,
|
|
@@ -71,7 +72,11 @@ import {
|
|
|
71
72
|
authSetupRequired,
|
|
72
73
|
buildClearSessionCookie,
|
|
73
74
|
buildSessionCookie,
|
|
75
|
+
createCliAuthorization,
|
|
76
|
+
decideCliAuthorization,
|
|
77
|
+
exchangeCliAuthorization,
|
|
74
78
|
getAuthUserFromRequest,
|
|
79
|
+
getCliAuthorization,
|
|
75
80
|
getSessionTokenFromRequest,
|
|
76
81
|
isAuthUserAllowed,
|
|
77
82
|
listAuthUsers,
|
|
@@ -80,8 +85,13 @@ import {
|
|
|
80
85
|
readAuthUsers,
|
|
81
86
|
readUserAllowlist,
|
|
82
87
|
resetAuthUserPassword,
|
|
88
|
+
revokeSessionToken,
|
|
83
89
|
writeUserAllowlist,
|
|
84
90
|
} from "./auth.mjs";
|
|
91
|
+
import {
|
|
92
|
+
renderCliAuthorizationPage,
|
|
93
|
+
renderCliAuthorizationResult,
|
|
94
|
+
} from "./cli-auth-page.mjs";
|
|
85
95
|
import { readGlobalEnvRows, readUserEnvRows, writeGlobalEnvRows, writeUserEnvRows } from "./user-env.mjs";
|
|
86
96
|
import {
|
|
87
97
|
readAdminBuiltinPipelineConfig,
|
|
@@ -167,7 +177,7 @@ import {
|
|
|
167
177
|
writeDisplayShares,
|
|
168
178
|
writeWorkspaceGraph,
|
|
169
179
|
} from "./workspace-server.mjs";
|
|
170
|
-
import { handleWorkspaceRoutes } from "./workspace-routes.mjs";
|
|
180
|
+
import { handleWorkspaceRoutes, workspaceGraphWithScheduleMode } from "./workspace-routes.mjs";
|
|
171
181
|
|
|
172
182
|
const MIME = {
|
|
173
183
|
".html": "text/html; charset=utf-8",
|
|
@@ -206,10 +216,8 @@ const BUILTIN_SKILL_COLLECTIONS = [
|
|
|
206
216
|
id: "pipeline",
|
|
207
217
|
name: "Pipeline",
|
|
208
218
|
defaultKeys: [
|
|
209
|
-
"agentflow-flow-
|
|
210
|
-
"agentflow-flow-edit-node-fields",
|
|
219
|
+
"agentflow-flow-dsl",
|
|
211
220
|
"agentflow-flow-recipes",
|
|
212
|
-
"agentflow-flow-sync-ui",
|
|
213
221
|
"agentflow-node-reference",
|
|
214
222
|
"agentflow-placeholder-reference",
|
|
215
223
|
"agentflow-runtime-reference",
|
|
@@ -219,7 +227,7 @@ const BUILTIN_SKILL_COLLECTIONS = [
|
|
|
219
227
|
id: "workspace",
|
|
220
228
|
name: "Workspace",
|
|
221
229
|
defaultKeys: [
|
|
222
|
-
"agentflow-
|
|
230
|
+
"agentflow-flow-dsl",
|
|
223
231
|
"agentflow-workspace-markdown",
|
|
224
232
|
"agentflow-workspace-mermaid",
|
|
225
233
|
"agentflow-workspace-ascii",
|
|
@@ -233,7 +241,18 @@ const BUILTIN_SKILL_COLLECTIONS = [
|
|
|
233
241
|
],
|
|
234
242
|
legacyDefaultKeys: [
|
|
235
243
|
[
|
|
236
|
-
"agentflow-workspace-
|
|
244
|
+
"agentflow-workspace-markdown",
|
|
245
|
+
"agentflow-workspace-mermaid",
|
|
246
|
+
"agentflow-workspace-ascii",
|
|
247
|
+
"agentflow-workspace-chart",
|
|
248
|
+
"agentflow-workspace-table",
|
|
249
|
+
"agentflow-workspace-html",
|
|
250
|
+
"agentflow-workspace-image",
|
|
251
|
+
"agentflow-node-reference",
|
|
252
|
+
"agentflow-placeholder-reference",
|
|
253
|
+
"agentflow-runtime-reference",
|
|
254
|
+
],
|
|
255
|
+
[
|
|
237
256
|
"agentflow-workspace-markdown",
|
|
238
257
|
"agentflow-workspace-mermaid",
|
|
239
258
|
"agentflow-workspace-ascii",
|
|
@@ -244,7 +263,6 @@ const BUILTIN_SKILL_COLLECTIONS = [
|
|
|
244
263
|
"agentflow-runtime-reference",
|
|
245
264
|
],
|
|
246
265
|
[
|
|
247
|
-
"agentflow-workspace-graph",
|
|
248
266
|
"agentflow-workspace-markdown",
|
|
249
267
|
"agentflow-workspace-mermaid",
|
|
250
268
|
"agentflow-workspace-ascii",
|
|
@@ -1450,6 +1468,25 @@ function serverPublicBaseUrl(req, host, port, payload = null) {
|
|
|
1450
1468
|
return configuredPublicBaseUrl(payload) || requestPublicBaseUrl(req) || normalizePublicBaseUrl(`http://${host}:${port}`);
|
|
1451
1469
|
}
|
|
1452
1470
|
|
|
1471
|
+
function html(res, status, content, headers = {}) {
|
|
1472
|
+
const body = String(content || "");
|
|
1473
|
+
res.writeHead(status, {
|
|
1474
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
1475
|
+
"Content-Length": Buffer.byteLength(body),
|
|
1476
|
+
"Cache-Control": "no-store, max-age=0",
|
|
1477
|
+
"Content-Security-Policy": "default-src 'none'; style-src 'unsafe-inline'; form-action 'self'; base-uri 'none'; frame-ancestors 'none'",
|
|
1478
|
+
"X-Content-Type-Options": "nosniff",
|
|
1479
|
+
"Referrer-Policy": "no-referrer",
|
|
1480
|
+
...headers,
|
|
1481
|
+
});
|
|
1482
|
+
res.end(body);
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
async function readUrlEncodedBody(req) {
|
|
1486
|
+
const body = await readBody(req);
|
|
1487
|
+
return new URLSearchParams(String(body || ""));
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1453
1490
|
function publicDisplayPayloadFromShare(root, share) {
|
|
1454
1491
|
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
1455
1492
|
flowId: share.flowId || "",
|
|
@@ -1684,7 +1721,7 @@ function bufferLooksLikeZip(buf) {
|
|
|
1684
1721
|
|
|
1685
1722
|
/**
|
|
1686
1723
|
* @param {import('http').IncomingMessage} req
|
|
1687
|
-
* @returns {Promise<{ targetSpace: string, flowIdField: string, file: Buffer, filename: string, gotFile: boolean }>}
|
|
1724
|
+
* @returns {Promise<{ targetSpace: string, flowIdField: string, scheduleMode: string, file: Buffer, filename: string, gotFile: boolean }>}
|
|
1688
1725
|
*/
|
|
1689
1726
|
function parseFlowsImportForm(req) {
|
|
1690
1727
|
return new Promise((resolve, reject) => {
|
|
@@ -1694,6 +1731,7 @@ function parseFlowsImportForm(req) {
|
|
|
1694
1731
|
});
|
|
1695
1732
|
let targetSpace = "user";
|
|
1696
1733
|
let flowIdField = "";
|
|
1734
|
+
let scheduleMode = "disabled";
|
|
1697
1735
|
/** @type {Buffer[]} */
|
|
1698
1736
|
const chunks = [];
|
|
1699
1737
|
let filename = "";
|
|
@@ -1706,6 +1744,9 @@ function parseFlowsImportForm(req) {
|
|
|
1706
1744
|
if (name === "flowId" && typeof val === "string") {
|
|
1707
1745
|
flowIdField = val;
|
|
1708
1746
|
}
|
|
1747
|
+
if (name === "scheduleMode" && typeof val === "string") {
|
|
1748
|
+
scheduleMode = val.trim().toLowerCase();
|
|
1749
|
+
}
|
|
1709
1750
|
});
|
|
1710
1751
|
|
|
1711
1752
|
bb.on("file", (name, file, info) => {
|
|
@@ -1725,6 +1766,7 @@ function parseFlowsImportForm(req) {
|
|
|
1725
1766
|
resolve({
|
|
1726
1767
|
targetSpace,
|
|
1727
1768
|
flowIdField: flowIdField.trim(),
|
|
1769
|
+
scheduleMode,
|
|
1728
1770
|
file: Buffer.concat(chunks),
|
|
1729
1771
|
filename,
|
|
1730
1772
|
gotFile,
|
|
@@ -1748,12 +1790,16 @@ function cleanupExpiredWorkspacePreviews(workspaceRoot = "") {
|
|
|
1748
1790
|
}
|
|
1749
1791
|
let removed = 0;
|
|
1750
1792
|
for (const pipelinesRoot of roots) {
|
|
1751
|
-
|
|
1793
|
+
const expired = [
|
|
1794
|
+
...listExpiredWorkspacePreviews(pipelinesRoot),
|
|
1795
|
+
...listExpiredWorkspaceDrafts(pipelinesRoot),
|
|
1796
|
+
];
|
|
1797
|
+
for (const item of expired) {
|
|
1752
1798
|
try {
|
|
1753
1799
|
fs.rmSync(item.flowDir, { recursive: true, force: true });
|
|
1754
1800
|
removed += 1;
|
|
1755
1801
|
} catch (e) {
|
|
1756
|
-
log.debug(`[workspace-
|
|
1802
|
+
log.debug(`[workspace-temporary] cleanup failed: ${(e && e.message) || String(e)}`);
|
|
1757
1803
|
}
|
|
1758
1804
|
}
|
|
1759
1805
|
}
|
|
@@ -2013,12 +2059,12 @@ function buildComposerPromptWithFlowContext(p) {
|
|
|
2013
2059
|
`- flowSource:${p.flowSource}`,
|
|
2014
2060
|
...builtinExtra,
|
|
2015
2061
|
`- 当前关联的节点实例 ID(顺序:画布选中优先,再输入框 @提及):${idsLine}`,
|
|
2016
|
-
"- 像普通 agent 请求一样处理用户说明:可能只是问问题,也可能要求编辑文件。不要因为存在 flowId
|
|
2062
|
+
"- 像普通 agent 请求一样处理用户说明:可能只是问问题,也可能要求编辑文件。不要因为存在 flowId 就默认修改流程。",
|
|
2017
2063
|
"- 按需使用当前环境可用的 skills;如果用户点名某个 skill,遵循该 skill 的 SKILL.md。",
|
|
2018
2064
|
"- 如果你判断需要编辑 AgentFlow 流程,可按需读取这些本地 skills:",
|
|
2019
|
-
" - `skills/agentflow-flow-
|
|
2020
|
-
" - `skills/agentflow-
|
|
2021
|
-
" - `skills/agentflow-flow
|
|
2065
|
+
" - `skills/agentflow-flow-dsl/SKILL.md`:新建或修改 workspace.flow.js、节点、字段、连线、子流程与受控循环",
|
|
2066
|
+
" - `skills/agentflow-node-dsl/SKILL.md`:仅在需要确定性自定义代码节点时使用",
|
|
2067
|
+
" - `skills/agentflow-author-flow/SKILL.md`:需要 Draft、试运行、动态修改、发布或定时时使用完整生命周期",
|
|
2022
2068
|
"- 如果只是回答问题,不要修改文件。",
|
|
2023
2069
|
"",
|
|
2024
2070
|
...(p.selectedSkillBlock ? [p.selectedSkillBlock, ""] : []),
|
|
@@ -2091,6 +2137,132 @@ export function startUiServer({
|
|
|
2091
2137
|
return;
|
|
2092
2138
|
}
|
|
2093
2139
|
|
|
2140
|
+
if (url.pathname === "/api/auth/cli/device" && req.method === "POST") {
|
|
2141
|
+
let payload = {};
|
|
2142
|
+
try {
|
|
2143
|
+
const raw = await readBody(req);
|
|
2144
|
+
payload = raw ? JSON.parse(raw) : {};
|
|
2145
|
+
} catch {
|
|
2146
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
2147
|
+
return;
|
|
2148
|
+
}
|
|
2149
|
+
const authorization = createCliAuthorization({
|
|
2150
|
+
publicBaseUrl: serverPublicBaseUrl(req, host, uiPort),
|
|
2151
|
+
clientName: payload?.clientName,
|
|
2152
|
+
});
|
|
2153
|
+
json(res, 201, {
|
|
2154
|
+
requestId: authorization.requestId,
|
|
2155
|
+
deviceCode: authorization.deviceCode,
|
|
2156
|
+
userCode: authorization.userCode,
|
|
2157
|
+
verificationUrl: authorization.verificationUrl,
|
|
2158
|
+
expiresAt: authorization.expiresAt,
|
|
2159
|
+
pollInterval: authorization.pollInterval,
|
|
2160
|
+
});
|
|
2161
|
+
return;
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
if (url.pathname === "/api/auth/cli/token" && req.method === "POST") {
|
|
2165
|
+
let payload = {};
|
|
2166
|
+
try {
|
|
2167
|
+
const raw = await readBody(req);
|
|
2168
|
+
payload = raw ? JSON.parse(raw) : {};
|
|
2169
|
+
} catch {
|
|
2170
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
2171
|
+
return;
|
|
2172
|
+
}
|
|
2173
|
+
const result = exchangeCliAuthorization(payload?.deviceCode);
|
|
2174
|
+
if (!result.ok) {
|
|
2175
|
+
json(res, result.status || 400, {
|
|
2176
|
+
error: result.error,
|
|
2177
|
+
code: result.code || "invalid_request",
|
|
2178
|
+
authorization: result.authorization || undefined,
|
|
2179
|
+
});
|
|
2180
|
+
return;
|
|
2181
|
+
}
|
|
2182
|
+
json(res, 200, result);
|
|
2183
|
+
return;
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
if (url.pathname === "/api/auth/cli/revoke" && req.method === "POST") {
|
|
2187
|
+
const token = getSessionTokenFromRequest(req);
|
|
2188
|
+
if (!token || !getAuthUserFromRequest(req)) {
|
|
2189
|
+
json(res, 401, { error: "Unauthorized" });
|
|
2190
|
+
return;
|
|
2191
|
+
}
|
|
2192
|
+
revokeSessionToken(token);
|
|
2193
|
+
json(res, 200, { ok: true });
|
|
2194
|
+
return;
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
if (url.pathname === "/cli/authorize" && req.method === "GET") {
|
|
2198
|
+
const requestId = String(url.searchParams.get("request") || "").trim();
|
|
2199
|
+
const user = getAuthUserFromRequest(req);
|
|
2200
|
+
const result = getCliAuthorization(requestId, { includeApprovalNonce: Boolean(user) });
|
|
2201
|
+
if (result.ok && result.authorization.status !== "pending") {
|
|
2202
|
+
html(res, 200, renderCliAuthorizationResult({
|
|
2203
|
+
approved: result.authorization.status === "approved",
|
|
2204
|
+
clientName: result.authorization.clientName,
|
|
2205
|
+
}));
|
|
2206
|
+
return;
|
|
2207
|
+
}
|
|
2208
|
+
html(res, result.ok ? 200 : result.status || 404, renderCliAuthorizationPage({
|
|
2209
|
+
authorization: result.authorization || null,
|
|
2210
|
+
approvalNonce: result.approvalNonce || "",
|
|
2211
|
+
user,
|
|
2212
|
+
error: result.ok ? "" : result.error,
|
|
2213
|
+
}));
|
|
2214
|
+
return;
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
if (url.pathname === "/cli/authorize/login" && req.method === "POST") {
|
|
2218
|
+
const form = await readUrlEncodedBody(req);
|
|
2219
|
+
const requestId = String(form.get("request") || "").trim();
|
|
2220
|
+
const pending = getCliAuthorization(requestId);
|
|
2221
|
+
if (!pending.ok) {
|
|
2222
|
+
html(res, pending.status || 404, renderCliAuthorizationPage({ error: pending.error }));
|
|
2223
|
+
return;
|
|
2224
|
+
}
|
|
2225
|
+
const result = loginOrCreateUser(form.get("username"), form.get("password"));
|
|
2226
|
+
if (!result.ok) {
|
|
2227
|
+
html(res, result.forbidden ? 403 : 401, renderCliAuthorizationPage({
|
|
2228
|
+
authorization: pending.authorization,
|
|
2229
|
+
error: result.error || "Login failed",
|
|
2230
|
+
}));
|
|
2231
|
+
return;
|
|
2232
|
+
}
|
|
2233
|
+
res.writeHead(303, {
|
|
2234
|
+
Location: `/cli/authorize?request=${encodeURIComponent(requestId)}`,
|
|
2235
|
+
"Set-Cookie": buildSessionCookie(result.token),
|
|
2236
|
+
"Cache-Control": "no-store, max-age=0",
|
|
2237
|
+
});
|
|
2238
|
+
res.end();
|
|
2239
|
+
return;
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2242
|
+
if (url.pathname === "/cli/authorize/decision" && req.method === "POST") {
|
|
2243
|
+
const user = getAuthUserFromRequest(req);
|
|
2244
|
+
if (!user) {
|
|
2245
|
+
html(res, 401, renderCliAuthorizationResult({ error: "登录状态已失效,请重新打开授权链接。" }));
|
|
2246
|
+
return;
|
|
2247
|
+
}
|
|
2248
|
+
const form = await readUrlEncodedBody(req);
|
|
2249
|
+
const requestId = String(form.get("request") || "").trim();
|
|
2250
|
+
const current = getCliAuthorization(requestId);
|
|
2251
|
+
const approved = form.get("decision") === "approve";
|
|
2252
|
+
const result = decideCliAuthorization({
|
|
2253
|
+
requestId,
|
|
2254
|
+
approvalNonce: form.get("approvalNonce"),
|
|
2255
|
+
userId: user.userId,
|
|
2256
|
+
approved,
|
|
2257
|
+
});
|
|
2258
|
+
html(res, result.ok ? 200 : result.status || 400, renderCliAuthorizationResult({
|
|
2259
|
+
approved,
|
|
2260
|
+
clientName: current.authorization?.clientName,
|
|
2261
|
+
error: result.ok ? "" : result.error,
|
|
2262
|
+
}));
|
|
2263
|
+
return;
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2094
2266
|
if (url.pathname === "/api/app-version" && req.method === "GET") {
|
|
2095
2267
|
res.setHeader("Cache-Control", "no-store, max-age=0");
|
|
2096
2268
|
json(res, 200, {
|
|
@@ -2706,6 +2878,10 @@ export function startUiServer({
|
|
|
2706
2878
|
}
|
|
2707
2879
|
const flowId = idCheck.flowId;
|
|
2708
2880
|
const targetSpace = parsed.targetSpace === "workspace" ? "workspace" : "user";
|
|
2881
|
+
if (!["enabled", "disabled", "preserve"].includes(parsed.scheduleMode)) {
|
|
2882
|
+
json(res, 400, { error: "scheduleMode must be enabled, disabled, or preserve" });
|
|
2883
|
+
return;
|
|
2884
|
+
}
|
|
2709
2885
|
const existing = listFlowsJson(root, {
|
|
2710
2886
|
...userCtx,
|
|
2711
2887
|
includeWorkspaceFlows: targetSpace === "workspace",
|
|
@@ -2747,10 +2923,37 @@ export function startUiServer({
|
|
|
2747
2923
|
json(res, 400, { error: w.error });
|
|
2748
2924
|
return;
|
|
2749
2925
|
}
|
|
2750
|
-
|
|
2751
|
-
|
|
2926
|
+
const targetDir = targetSpace === "workspace"
|
|
2927
|
+
? path.join(path.resolve(root), PIPELINES_DIR, flowId)
|
|
2928
|
+
: path.join(getUserPipelinesRoot(userCtx.userId), flowId);
|
|
2929
|
+
let collaborationCreated = false;
|
|
2930
|
+
try {
|
|
2931
|
+
if (targetSpace === "workspace") {
|
|
2932
|
+
ensureWorkspaceCollaboration({ flowId, userId: userCtx.userId });
|
|
2933
|
+
collaborationCreated = true;
|
|
2934
|
+
}
|
|
2935
|
+
const scoped = resolveWorkspaceScopeRoot(root, { flowId, flowSource: targetSpace }, userCtx);
|
|
2936
|
+
if (scoped.error) throw new Error(scoped.error);
|
|
2937
|
+
const importedGraph = readWorkspaceGraph(scoped.root, root).graph;
|
|
2938
|
+
const graph = workspaceGraphWithScheduleMode(importedGraph, parsed.scheduleMode);
|
|
2939
|
+
if (graph !== importedGraph) writeWorkspaceGraph(scoped.root, graph, root);
|
|
2940
|
+
const persisted = readWorkspaceGraph(scoped.root, root).graph;
|
|
2941
|
+
const workspaceSchedules = syncWorkspaceSchedulesForGraph(root, scoped, persisted, authUser, userCtx);
|
|
2942
|
+
json(res, 200, {
|
|
2943
|
+
success: true,
|
|
2944
|
+
flowId,
|
|
2945
|
+
flowSource: targetSpace,
|
|
2946
|
+
scheduleMode: parsed.scheduleMode,
|
|
2947
|
+
workspaceSchedules,
|
|
2948
|
+
});
|
|
2949
|
+
} catch (e) {
|
|
2950
|
+
try { fs.rmSync(targetDir, { recursive: true, force: true }); } catch (_) {}
|
|
2951
|
+
if (collaborationCreated) deleteWorkspaceCollaborationForFlow(flowId, false);
|
|
2952
|
+
json(res, 500, {
|
|
2953
|
+
error: `Flow publication rolled back: ${(e && e.message) || String(e)}`,
|
|
2954
|
+
rolledBack: true,
|
|
2955
|
+
});
|
|
2752
2956
|
}
|
|
2753
|
-
json(res, 200, { success: true, flowId, flowSource: targetSpace });
|
|
2754
2957
|
return;
|
|
2755
2958
|
}
|
|
2756
2959
|
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import crypto from "crypto";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
|
|
5
|
+
import { getUserPipelinesRoot } from "./paths.mjs";
|
|
6
|
+
|
|
7
|
+
export const WORKSPACE_DRAFT_METADATA_FILENAME = ".agentflow-workspace-draft.json";
|
|
8
|
+
export const DEFAULT_WORKSPACE_DRAFT_TTL_MS = 2 * 60 * 60 * 1000;
|
|
9
|
+
export const MAX_WORKSPACE_DRAFT_TTL_MS = 24 * 60 * 60 * 1000;
|
|
10
|
+
|
|
11
|
+
export function safeWorkspaceDraftId(value = "") {
|
|
12
|
+
const raw = String(value || "").trim();
|
|
13
|
+
return /^draft_[a-z0-9_-]{8,100}$/i.test(raw) ? raw : "";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function createWorkspaceDraftId() {
|
|
17
|
+
return `draft_${crypto.randomBytes(10).toString("hex")}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function workspaceDraftFlowDir(draftId, userId = "") {
|
|
21
|
+
const safeId = safeWorkspaceDraftId(draftId);
|
|
22
|
+
if (!safeId) return "";
|
|
23
|
+
return path.join(getUserPipelinesRoot(userId), safeId);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function workspaceDraftMetadataPath(flowDir) {
|
|
27
|
+
return path.join(flowDir, WORKSPACE_DRAFT_METADATA_FILENAME);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function readWorkspaceDraftMetadata(flowDir) {
|
|
31
|
+
try {
|
|
32
|
+
const filePath = workspaceDraftMetadataPath(flowDir);
|
|
33
|
+
if (!fs.existsSync(filePath)) return null;
|
|
34
|
+
const value = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
35
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
36
|
+
const flowId = safeWorkspaceDraftId(value.flowId);
|
|
37
|
+
const ownerId = String(value.ownerId || "").trim();
|
|
38
|
+
const expiresAt = String(value.expiresAt || "").trim();
|
|
39
|
+
if (!flowId || !ownerId || !Number.isFinite(Date.parse(expiresAt))) return null;
|
|
40
|
+
return { ...value, flowId, ownerId, expiresAt };
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function isWorkspaceDraftDir(flowDir) {
|
|
47
|
+
return Boolean(readWorkspaceDraftMetadata(flowDir));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function normalizeWorkspaceDraftTtlMs(value) {
|
|
51
|
+
const requested = Number(value);
|
|
52
|
+
if (!Number.isFinite(requested) || requested <= 0) return DEFAULT_WORKSPACE_DRAFT_TTL_MS;
|
|
53
|
+
return Math.min(MAX_WORKSPACE_DRAFT_TTL_MS, Math.max(60_000, Math.round(requested)));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function writeWorkspaceDraftMetadata(flowDir, metadata) {
|
|
57
|
+
const filePath = workspaceDraftMetadataPath(flowDir);
|
|
58
|
+
const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
59
|
+
fs.writeFileSync(tempPath, `${JSON.stringify(metadata, null, 2)}\n`, "utf8");
|
|
60
|
+
fs.renameSync(tempPath, filePath);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function listExpiredWorkspaceDrafts(userPipelinesRoot, now = Date.now()) {
|
|
64
|
+
if (!fs.existsSync(userPipelinesRoot)) return [];
|
|
65
|
+
const expired = [];
|
|
66
|
+
for (const entry of fs.readdirSync(userPipelinesRoot, { withFileTypes: true })) {
|
|
67
|
+
if (!entry.isDirectory()) continue;
|
|
68
|
+
const flowDir = path.join(userPipelinesRoot, entry.name);
|
|
69
|
+
const metadata = readWorkspaceDraftMetadata(flowDir);
|
|
70
|
+
if (!metadata) continue;
|
|
71
|
+
if (Date.parse(metadata.expiresAt) <= now) expired.push({ flowDir, metadata });
|
|
72
|
+
}
|
|
73
|
+
return expired;
|
|
74
|
+
}
|
|
@@ -108,7 +108,7 @@ function readTextOrNull(file) {
|
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
export function emptyDesignGraph() {
|
|
111
|
-
return { version: 1, instances: {}, edges: [], ui: { nodePositions: {} } };
|
|
111
|
+
return { version: 1, instances: {}, edges: [], subflows: {}, ui: { nodePositions: {} } };
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
function normalizeDesignShape(raw) {
|
|
@@ -120,6 +120,10 @@ function normalizeDesignShape(raw) {
|
|
|
120
120
|
? graph.instances
|
|
121
121
|
: {},
|
|
122
122
|
edges: Array.isArray(graph.edges) ? graph.edges : [],
|
|
123
|
+
subflows:
|
|
124
|
+
graph.subflows && typeof graph.subflows === "object" && !Array.isArray(graph.subflows)
|
|
125
|
+
? graph.subflows
|
|
126
|
+
: {},
|
|
123
127
|
ui: graph.ui && typeof graph.ui === "object" ? graph.ui : { nodePositions: {} },
|
|
124
128
|
};
|
|
125
129
|
}
|
|
@@ -226,7 +230,12 @@ export function designFingerprint(graph) {
|
|
|
226
230
|
.filter((e) => e && design.instances[e.source] && design.instances[e.target])
|
|
227
231
|
.map((e) => `${e.source}|${asText(e.sourceHandle)}|${e.target}|${asText(e.targetHandle)}`)
|
|
228
232
|
.sort();
|
|
229
|
-
return stableStringify({
|
|
233
|
+
return stableStringify({
|
|
234
|
+
instances,
|
|
235
|
+
edges: [...new Set(edges)],
|
|
236
|
+
subflows: design.subflows,
|
|
237
|
+
ui: normalizeUi(design.ui),
|
|
238
|
+
});
|
|
230
239
|
}
|
|
231
240
|
|
|
232
241
|
// ── 读 ──────────────────────────────────────────────────────────────────────
|
|
@@ -34,6 +34,9 @@ function normalizedGraph(graph) {
|
|
|
34
34
|
version: Number(source.version) || 1,
|
|
35
35
|
instances: isPlainObject(source.instances) ? clone(source.instances) : {},
|
|
36
36
|
edges: Array.isArray(source.edges) ? clone(source.edges) : [],
|
|
37
|
+
// Subflows are first-class design metadata. Dropping them here makes every
|
|
38
|
+
// revision-aware canvas save silently erase call contracts and entry roots.
|
|
39
|
+
subflows: isPlainObject(source.subflows) ? clone(source.subflows) : {},
|
|
37
40
|
ui: isPlainObject(source.ui) ? clone(source.ui) : { nodePositions: {} },
|
|
38
41
|
};
|
|
39
42
|
}
|