@hmj-ai/cflow 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DESIGN.md +2 -1
- package/README.md +7 -6
- package/dist/public/assets/index-CV7qQRWx.css +1 -0
- package/dist/public/assets/index-DGu2YJI7.js +16 -0
- package/dist/public/cflow-lockup.svg +35 -0
- package/dist/public/cflow-mark.svg +23 -0
- package/dist/public/index.html +4 -4
- package/dist/src/compiler.js +30 -0
- package/dist/src/db.js +2 -2
- package/dist/src/runtime-manifest.js +88 -20
- package/dist/src/runtime-process.js +152 -88
- package/dist/src/runtime.js +160 -120
- package/dist/src/server.js +114 -115
- package/dist/src/workspace-files.js +82 -0
- package/dist/src/workspace.js +43 -2
- package/package.json +2 -1
- package/dist/public/assets/index-BqTfYp5s.js +0 -15
- package/dist/public/assets/index-D0BpmA_V.css +0 -1
package/dist/src/server.js
CHANGED
|
@@ -3,22 +3,23 @@ import Fastify from 'fastify';
|
|
|
3
3
|
import fastifyStatic from '@fastify/static';
|
|
4
4
|
import fastifyMultipart from '@fastify/multipart';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
|
-
import {
|
|
7
|
-
import { cp, mkdir,
|
|
8
|
-
import {
|
|
9
|
-
import { dirname, isAbsolute, join, sep } from 'node:path';
|
|
6
|
+
import { existsSync, realpathSync } from 'node:fs';
|
|
7
|
+
import { cp, mkdir, rm } from 'node:fs/promises';
|
|
8
|
+
import { dirname, join, sep } from 'node:path';
|
|
10
9
|
import { Store } from './db.js';
|
|
11
|
-
import { compileCF, compileFlow } from './compiler.js';
|
|
10
|
+
import { assertFileReferences, compileCF, compileFlow } from './compiler.js';
|
|
12
11
|
import { Engine, builtins, newRunId } from './engine.js';
|
|
13
12
|
import { RuntimeManager } from './runtime.js';
|
|
14
13
|
import { sha256 } from './hash.js';
|
|
15
|
-
import {
|
|
14
|
+
import { initializeWorkspace, validateWorkspaceRoot } from './workspace.js';
|
|
15
|
+
import { missingWorkspaceFiles, searchWorkspaceFiles } from './workspace-files.js';
|
|
16
16
|
import { collectGroundingFailures, groundingError, assertAttachmentGrounding, applyFlowRevision, buildProposalGraph, flowProposalOutputSchema, flowProposalPrompt, matchPublishedCapabilities, prepareSkillAttachments, } from './proposal.js';
|
|
17
17
|
import { flowAgentContext, flowAgentFallback, flowAgentOutputSchema, flowAgentPrompt, normalizeAgentResponse, } from './flow-agent.js';
|
|
18
|
-
export function createApp(store = new Store()) {
|
|
18
|
+
export function createApp(store = new Store(), requestedWorkspaceRoot = process.cwd()) {
|
|
19
|
+
const workspaceRoot = validateWorkspaceRoot(requestedWorkspaceRoot, 'WORKSPACE_UNAVAILABLE');
|
|
19
20
|
const versions = () => store.list('cf_versions');
|
|
20
21
|
const flowCompilations = () => store.flowCompilations();
|
|
21
|
-
const runtimes = new RuntimeManager(store);
|
|
22
|
+
const runtimes = new RuntimeManager(store, { projectRoot: workspaceRoot });
|
|
22
23
|
const testPlans = new Map();
|
|
23
24
|
const testCatalogs = new Map();
|
|
24
25
|
const executorRegistry = builtins();
|
|
@@ -27,42 +28,7 @@ export function createApp(store = new Store()) {
|
|
|
27
28
|
...versions(),
|
|
28
29
|
...[...testCatalogs.values()].flat(),
|
|
29
30
|
]);
|
|
30
|
-
const
|
|
31
|
-
const draft = store.get('flow_drafts', flowId);
|
|
32
|
-
if (draft)
|
|
33
|
-
return requireWorkspaceRoot(draft.workspaceRoot);
|
|
34
|
-
const plan = store.list('flow_versions').find((value) => value.flowId === flowId);
|
|
35
|
-
return plan ? requireWorkspaceRoot(plan.workspaceRoot) : undefined;
|
|
36
|
-
};
|
|
37
|
-
const assertFlowWorkspaceImmutable = (draft) => {
|
|
38
|
-
const requested = requireWorkspaceRoot(draft.workspaceRoot);
|
|
39
|
-
const stored = storedFlowWorkspace(draft.flowId);
|
|
40
|
-
if (stored !== undefined && stored !== requested) {
|
|
41
|
-
try {
|
|
42
|
-
if (validateWorkspaceRoot(requested) !== stored)
|
|
43
|
-
throw new Error('FLOW_WORKSPACE_IMMUTABLE');
|
|
44
|
-
}
|
|
45
|
-
catch {
|
|
46
|
-
throw new Error('FLOW_WORKSPACE_IMMUTABLE');
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
return stored ?? requested;
|
|
50
|
-
};
|
|
51
|
-
const operationalWorkspace = (draft) => {
|
|
52
|
-
const requested = assertFlowWorkspaceImmutable(draft);
|
|
53
|
-
const normalized = validateWorkspaceRoot(requested);
|
|
54
|
-
if (normalized !== requested)
|
|
55
|
-
throw new Error('FLOW_WORKSPACE_IMMUTABLE');
|
|
56
|
-
return normalized;
|
|
57
|
-
};
|
|
58
|
-
const normalizeNewFlowWorkspace = (draft) => {
|
|
59
|
-
const stored = storedFlowWorkspace(draft.flowId);
|
|
60
|
-
if (stored) {
|
|
61
|
-
assertFlowWorkspaceImmutable(draft);
|
|
62
|
-
return draft;
|
|
63
|
-
}
|
|
64
|
-
return { ...draft, workspaceRoot: validateWorkspaceRoot(draft.workspaceRoot) };
|
|
65
|
-
};
|
|
31
|
+
const scopeFlowDraft = (draft) => ({ ...draft, workspaceRoot });
|
|
66
32
|
const app = Fastify({ logger: true });
|
|
67
33
|
app.register(fastifyMultipart, {
|
|
68
34
|
// Do not silently truncate large skill bundles; files are streamed to disk.
|
|
@@ -110,7 +76,7 @@ export function createApp(store = new Store()) {
|
|
|
110
76
|
nodes: draft.nodes.map((node) => node.kind === 'cf-call' && !node.executor ? { ...node, executor: selected } : node),
|
|
111
77
|
};
|
|
112
78
|
};
|
|
113
|
-
const saveCompilationSnapshot = (mode, flowDraft, plan, programs, runId) => {
|
|
79
|
+
const saveCompilationSnapshot = (mode, flowDraft, plan, programs, runId, warnings) => {
|
|
114
80
|
const snapshot = {
|
|
115
81
|
id: `${flowDraft.flowId}@${flowDraft.revision}:${mode}`,
|
|
116
82
|
flowId: flowDraft.flowId,
|
|
@@ -119,6 +85,7 @@ export function createApp(store = new Store()) {
|
|
|
119
85
|
flowDraft,
|
|
120
86
|
plan,
|
|
121
87
|
programs,
|
|
88
|
+
...(warnings?.length ? { warnings } : {}),
|
|
122
89
|
runId,
|
|
123
90
|
createdAt: new Date().toISOString(),
|
|
124
91
|
};
|
|
@@ -159,7 +126,7 @@ export function createApp(store = new Store()) {
|
|
|
159
126
|
await rm(attachments.root, { recursive: true, force: true });
|
|
160
127
|
return proposal;
|
|
161
128
|
}
|
|
162
|
-
const draft =
|
|
129
|
+
const draft = scopeFlowDraft(proposal.flowDraft);
|
|
163
130
|
let archivePath;
|
|
164
131
|
if (attachments) {
|
|
165
132
|
archivePath = join('.cflow', 'flows', draft.flowId, 'attachments');
|
|
@@ -180,7 +147,11 @@ export function createApp(store = new Store()) {
|
|
|
180
147
|
await rm(attachments.root, { recursive: true, force: true });
|
|
181
148
|
}
|
|
182
149
|
}
|
|
183
|
-
store.
|
|
150
|
+
store.db.transaction(() => {
|
|
151
|
+
store.save('flow_drafts', draft.flowId, draft);
|
|
152
|
+
for (const cfDraft of proposal.cfDrafts ?? [])
|
|
153
|
+
store.save('cf_drafts', cfDraft.cfId, cfDraft);
|
|
154
|
+
})();
|
|
184
155
|
return {
|
|
185
156
|
...proposal,
|
|
186
157
|
flowDraft: draft,
|
|
@@ -191,49 +162,10 @@ export function createApp(store = new Store()) {
|
|
|
191
162
|
};
|
|
192
163
|
app.register(fastifyStatic, { root: runtimePublicRoot });
|
|
193
164
|
app.get('/', async (_, reply) => reply.sendFile('index.html'));
|
|
165
|
+
app.get('/api/workspace', async () => ({ root: workspaceRoot }));
|
|
166
|
+
app.post('/api/workspace/files/search', async (req) => searchWorkspaceFiles(workspaceRoot, req.body ?? {}));
|
|
194
167
|
app.get('/api/settings', async () => runtimes.settings());
|
|
195
168
|
app.put('/api/settings', async (req) => runtimes.updateSettings(req.body ?? {}));
|
|
196
|
-
app.get('/api/directories', async (req) => {
|
|
197
|
-
const requested = req.query.path?.trim() || homedir();
|
|
198
|
-
if (!isAbsolute(requested))
|
|
199
|
-
throw new Error('DIRECTORY_PATH_NOT_ABSOLUTE');
|
|
200
|
-
let path;
|
|
201
|
-
try {
|
|
202
|
-
path = await realpath(requested);
|
|
203
|
-
if (!(await stat(path)).isDirectory())
|
|
204
|
-
throw new Error('DIRECTORY_NOT_FOUND');
|
|
205
|
-
await access(path, constants.R_OK);
|
|
206
|
-
}
|
|
207
|
-
catch {
|
|
208
|
-
throw new Error('DIRECTORY_NOT_READABLE');
|
|
209
|
-
}
|
|
210
|
-
const children = await readdir(path, { withFileTypes: true });
|
|
211
|
-
const directories = (await Promise.all(children.map(async (entry) => {
|
|
212
|
-
const candidate = join(path, entry.name);
|
|
213
|
-
try {
|
|
214
|
-
const normalized = await realpath(candidate);
|
|
215
|
-
if (!(await stat(normalized)).isDirectory())
|
|
216
|
-
return null;
|
|
217
|
-
await access(normalized, constants.R_OK);
|
|
218
|
-
// Flag dotfiles so the picker can hide developer directories by default.
|
|
219
|
-
return { name: entry.name, path: normalized, hidden: entry.name.startsWith('.') };
|
|
220
|
-
}
|
|
221
|
-
catch {
|
|
222
|
-
return null;
|
|
223
|
-
}
|
|
224
|
-
})))
|
|
225
|
-
.filter((entry) => Boolean(entry))
|
|
226
|
-
.sort((a, b) => a.name.localeCompare(b.name));
|
|
227
|
-
const parentCandidate = dirname(path);
|
|
228
|
-
return {
|
|
229
|
-
path,
|
|
230
|
-
parentPath: parentCandidate === path ? null : await realpath(parentCandidate),
|
|
231
|
-
directories,
|
|
232
|
-
};
|
|
233
|
-
});
|
|
234
|
-
app.post('/api/directories/validate', async (req) => ({
|
|
235
|
-
path: validateWorkspaceRoot(req.body?.path, 'DIRECTORY_NOT_READ_WRITE'),
|
|
236
|
-
}));
|
|
237
169
|
const runtimeCatalog = () => Promise.all(runtimes.profiles().map(async (profile) => ({
|
|
238
170
|
...profile,
|
|
239
171
|
health: await runtimes.health(profile.id),
|
|
@@ -284,13 +216,32 @@ export function createApp(store = new Store()) {
|
|
|
284
216
|
app.get('/api/flows', async () => store.list('flow_versions'));
|
|
285
217
|
app.get('/api/flow-drafts', async () => store.list('flow_drafts'));
|
|
286
218
|
app.put('/api/flow-drafts/:id', async (req) => {
|
|
287
|
-
|
|
219
|
+
const { flowDraft, cfDrafts } = req.body ?? {};
|
|
220
|
+
if (req.params.id !== flowDraft?.flowId)
|
|
288
221
|
throw new Error('FLOW_DRAFT_ID_MISMATCH');
|
|
289
|
-
if (!
|
|
222
|
+
if (!flowDraft.flowId?.trim() || !flowDraft.name?.trim() || !flowDraft.objective?.trim())
|
|
290
223
|
throw new Error('FLOW_DRAFT_INVALID');
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
224
|
+
if (!Array.isArray(cfDrafts))
|
|
225
|
+
throw new Error('CF_DRAFTS_REQUIRED');
|
|
226
|
+
const ids = new Set();
|
|
227
|
+
for (const cfDraft of cfDrafts) {
|
|
228
|
+
if (!cfDraft.cfId?.trim() || !Number.isInteger(cfDraft.revision) || cfDraft.revision < 1)
|
|
229
|
+
throw new Error('CF_DRAFT_INVALID');
|
|
230
|
+
if (ids.has(cfDraft.cfId))
|
|
231
|
+
throw new Error('CF_DRAFT_DUPLICATE');
|
|
232
|
+
ids.add(cfDraft.cfId);
|
|
233
|
+
assertFileReferences(cfDraft.fileReferences);
|
|
234
|
+
}
|
|
235
|
+
const draft = scopeFlowDraft(flowDraft);
|
|
236
|
+
const stored = store.get('flow_drafts', draft.flowId);
|
|
237
|
+
if (stored && stored.revision > draft.revision)
|
|
238
|
+
throw new Error('FLOW_DRAFT_STALE');
|
|
239
|
+
store.db.transaction(() => {
|
|
240
|
+
store.save('flow_drafts', draft.flowId, draft);
|
|
241
|
+
for (const cfDraft of cfDrafts)
|
|
242
|
+
store.save('cf_drafts', cfDraft.cfId, cfDraft);
|
|
243
|
+
})();
|
|
244
|
+
return { flowDraft: draft, cfDrafts };
|
|
294
245
|
});
|
|
295
246
|
app.delete('/api/flow-drafts/:id', async (req, reply) => {
|
|
296
247
|
const result = store.db.prepare('DELETE FROM flow_drafts WHERE id=?').run(req.params.id);
|
|
@@ -308,26 +259,58 @@ export function createApp(store = new Store()) {
|
|
|
308
259
|
app.delete('/api/flows/:id', async (req, reply) => deletePublishedFlow(req.params.id, reply));
|
|
309
260
|
app.delete('/api/flows/:flowId/:flowVersion', async (req, reply) => deletePublishedFlow(`${req.params.flowId}@${req.params.flowVersion}`, reply));
|
|
310
261
|
app.post('/api/flow-compilations', async (req) => {
|
|
311
|
-
|
|
262
|
+
const scopedDraft = scopeFlowDraft(req.body.flowDraft);
|
|
312
263
|
const candidates = (req.body.cfDrafts ?? []).map(compileCF);
|
|
313
264
|
const catalog = new Map([...versions(), ...candidates].map((version) => [
|
|
314
265
|
`${version.cfId}@${version.version}`,
|
|
315
266
|
version,
|
|
316
267
|
]));
|
|
317
|
-
const selectedDraft = applyCompileRuntime(
|
|
268
|
+
const selectedDraft = applyCompileRuntime(scopedDraft, req.body.runtimeId);
|
|
318
269
|
const compiled = compileFlow(selectedDraft, catalog);
|
|
319
270
|
const plan = req.body.runtimeId
|
|
320
271
|
? await pinRuntimeProfiles(compiled, [...versions(), ...candidates])
|
|
321
272
|
: compiled;
|
|
273
|
+
const warnings = [];
|
|
274
|
+
for (const node of selectedDraft.nodes) {
|
|
275
|
+
if (node.kind !== 'cf-call')
|
|
276
|
+
continue;
|
|
277
|
+
const version = catalog.get(`${node.cfRef.cfId}@${node.cfRef.version}`);
|
|
278
|
+
const hasFileAccess = (version?.draft.effects ?? []).some((effect) => effect.type === 'file-read' || effect.type === 'file-write');
|
|
279
|
+
if (!hasFileAccess || !version)
|
|
280
|
+
continue;
|
|
281
|
+
const fileReferences = version.draft.fileReferences ?? [];
|
|
282
|
+
for (const path of await missingWorkspaceFiles(workspaceRoot, fileReferences)) {
|
|
283
|
+
warnings.push({
|
|
284
|
+
code: 'INDEXED_FILE_MISSING',
|
|
285
|
+
cfId: version.cfId,
|
|
286
|
+
nodeId: node.id,
|
|
287
|
+
path,
|
|
288
|
+
message: `引用文件不存在:${path}`,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
const indexed = new Set(fileReferences);
|
|
292
|
+
for (const match of version.draft.does.matchAll(/@\{([^}]+)\}/g)) {
|
|
293
|
+
const path = match[1];
|
|
294
|
+
if (!indexed.has(path))
|
|
295
|
+
warnings.push({
|
|
296
|
+
code: 'FILE_MENTION_NOT_INDEXED',
|
|
297
|
+
cfId: version.cfId,
|
|
298
|
+
nodeId: node.id,
|
|
299
|
+
path,
|
|
300
|
+
message: `任务中的文件没有加入引用列表:${path}`,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
}
|
|
322
304
|
saveCompilationSnapshot('preview', selectedDraft, plan, [...versions(), ...candidates].filter((version) => selectedDraft.nodes.some((node) => node.kind === 'cf-call' &&
|
|
323
305
|
node.cfRef.cfId === version.cfId &&
|
|
324
|
-
node.cfRef.version === version.version)));
|
|
306
|
+
node.cfRef.version === version.version)), undefined, warnings);
|
|
325
307
|
const referenced = new Set(selectedDraft.nodes
|
|
326
308
|
.filter((node) => node.kind === 'cf-call')
|
|
327
309
|
.map((node) => `${node.cfRef.cfId}@${node.cfRef.version}`));
|
|
328
310
|
return {
|
|
329
311
|
plan,
|
|
330
312
|
programs: [...catalog.values()].filter((version) => referenced.has(`${version.cfId}@${version.version}`)),
|
|
313
|
+
warnings,
|
|
331
314
|
};
|
|
332
315
|
});
|
|
333
316
|
app.post('/api/flow-proposals', async (req) => {
|
|
@@ -343,7 +326,6 @@ export function createApp(store = new Store()) {
|
|
|
343
326
|
return new Error(code);
|
|
344
327
|
};
|
|
345
328
|
const body = multipart?.fields ?? req.body;
|
|
346
|
-
const workspaceRoot = validateWorkspaceRoot(body.workspaceRoot, 'FLOW_WORKSPACE_UNAVAILABLE');
|
|
347
329
|
const objective = String(body.objective ?? '').trim();
|
|
348
330
|
if (!objective)
|
|
349
331
|
throw await failing('OBJECTIVE_REQUIRED');
|
|
@@ -455,15 +437,18 @@ export function createApp(store = new Store()) {
|
|
|
455
437
|
const message = req.body.message?.trim();
|
|
456
438
|
if (!message)
|
|
457
439
|
throw new Error('AGENT_MESSAGE_REQUIRED');
|
|
440
|
+
const request = {
|
|
441
|
+
...req.body,
|
|
442
|
+
flowDraft: req.body.flowDraft ? scopeFlowDraft(req.body.flowDraft) : null,
|
|
443
|
+
};
|
|
458
444
|
const catalog = versions();
|
|
459
|
-
const fallback = flowAgentFallback({ ...
|
|
460
|
-
const runtimeId =
|
|
445
|
+
const fallback = flowAgentFallback({ ...request, message }, catalog);
|
|
446
|
+
const runtimeId = request.runtimeId?.trim() || runtimes.settings().defaultRuntimeId;
|
|
461
447
|
const profile = runtimeId ? runtimes.profile(runtimeId) : undefined;
|
|
462
448
|
if (!profile || profile.backend === 'builtin')
|
|
463
449
|
return { ...fallback, fallback: true };
|
|
464
|
-
if (!
|
|
450
|
+
if (!request.flowDraft)
|
|
465
451
|
throw new Error('FLOW_WORKSPACE_REQUIRED');
|
|
466
|
-
const workspaceRoot = operationalWorkspace(req.body.flowDraft);
|
|
467
452
|
const health = await runtimes.health(runtimeId);
|
|
468
453
|
if (health.status !== 'available')
|
|
469
454
|
return { ...fallback, fallback: true };
|
|
@@ -476,13 +461,13 @@ export function createApp(store = new Store()) {
|
|
|
476
461
|
availableRuntimes.push({ id: item.id, name: item.name });
|
|
477
462
|
}
|
|
478
463
|
const grounded = Boolean(req.body.attachments?.length);
|
|
479
|
-
const response = await runtimes.execute(runtimeId, flowAgentPrompt(message, grounded), flowAgentContext({ ...
|
|
464
|
+
const response = await runtimes.execute(runtimeId, flowAgentPrompt(message, grounded), flowAgentContext({ ...request, message }, availableRuntimes, catalog), AbortSignal.timeout(runtimes.settings().testTimeoutMs), [], flowAgentOutputSchema(grounded), { workspaceRoot });
|
|
480
465
|
const normalized = normalizeAgentResponse(response);
|
|
481
466
|
if (normalized.intent === 'answer')
|
|
482
467
|
return { ...normalized, runtimeId };
|
|
483
468
|
if (grounded)
|
|
484
469
|
assertAttachmentGrounding(normalized, req.body.attachments ?? []);
|
|
485
|
-
const revised = applyFlowRevision(
|
|
470
|
+
const revised = applyFlowRevision(request.flowDraft, request.cfDrafts ?? [], normalized.stages, { catalog, runtimeId });
|
|
486
471
|
store.db.transaction(() => {
|
|
487
472
|
for (const draft of revised.cfDrafts)
|
|
488
473
|
store.save('cf_drafts', draft.cfId, draft);
|
|
@@ -516,8 +501,7 @@ export function createApp(store = new Store()) {
|
|
|
516
501
|
return { deleted: true };
|
|
517
502
|
});
|
|
518
503
|
app.post('/api/flows', async (req) => {
|
|
519
|
-
const draft =
|
|
520
|
-
operationalWorkspace(draft);
|
|
504
|
+
const draft = scopeFlowDraft(req.body);
|
|
521
505
|
const catalog = versions();
|
|
522
506
|
const plan = await pinRuntimeProfiles(compileFlow(draft, new Map(catalog.map((v) => [`${v.cfId}@${v.version}`, v]))), catalog);
|
|
523
507
|
store.save('flow_drafts', draft.flowId, draft);
|
|
@@ -525,14 +509,18 @@ export function createApp(store = new Store()) {
|
|
|
525
509
|
return plan;
|
|
526
510
|
});
|
|
527
511
|
app.post('/api/flow-tests', async (req) => {
|
|
528
|
-
|
|
512
|
+
const scopedDraft = scopeFlowDraft(req.body.flowDraft);
|
|
529
513
|
const candidateVersions = (req.body.cfDrafts ?? []).map(compileCF);
|
|
530
514
|
const catalog = new Map([...versions(), ...candidateVersions].map((version) => [
|
|
531
515
|
`${version.cfId}@${version.version}`,
|
|
532
516
|
version,
|
|
533
517
|
]));
|
|
534
|
-
const selectedDraft = applyCompileRuntime(
|
|
535
|
-
store.
|
|
518
|
+
const selectedDraft = applyCompileRuntime(scopedDraft, req.body.runtimeId);
|
|
519
|
+
store.db.transaction(() => {
|
|
520
|
+
store.save('flow_drafts', scopedDraft.flowId, scopedDraft);
|
|
521
|
+
for (const cfDraft of req.body.cfDrafts ?? [])
|
|
522
|
+
store.save('cf_drafts', cfDraft.cfId, cfDraft);
|
|
523
|
+
})();
|
|
536
524
|
const compiled = compileFlow(selectedDraft, catalog);
|
|
537
525
|
const plan = await pinRuntimeProfiles(compiled, [...versions(), ...candidateVersions]);
|
|
538
526
|
const programs = [...versions(), ...candidateVersions].filter((version) => selectedDraft.nodes.some((node) => node.kind === 'cf-call' &&
|
|
@@ -557,7 +545,8 @@ export function createApp(store = new Store()) {
|
|
|
557
545
|
const plan = store.get('flow_versions', `${req.body.flowId}@${req.body.flowVersion}`);
|
|
558
546
|
if (!plan)
|
|
559
547
|
throw new Error('FLOW_VERSION_NOT_FOUND');
|
|
560
|
-
|
|
548
|
+
if (plan.workspaceRoot !== workspaceRoot)
|
|
549
|
+
throw new Error('FLOW_WORKSPACE_MISMATCH');
|
|
561
550
|
const resources = resolveResources(plan, req.body.resourceProfileId);
|
|
562
551
|
const id = newRunId();
|
|
563
552
|
store.createRun(id, `${plan.flowId}@${plan.flowVersion}`, req.body.input ?? {}, resources, req.body.resourceProfileId);
|
|
@@ -643,10 +632,20 @@ export const isDirectExecution = (entryPath = process.argv[1]) => {
|
|
|
643
632
|
}
|
|
644
633
|
};
|
|
645
634
|
if (isDirectExecution()) {
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
635
|
+
let app;
|
|
636
|
+
try {
|
|
637
|
+
const workspace = initializeWorkspace();
|
|
638
|
+
app = createApp(new Store(workspace.databasePath), workspace.root);
|
|
639
|
+
app.log.info({ workspace: workspace.root, database: workspace.databasePath }, 'workspace ready');
|
|
640
|
+
await app.listen({
|
|
641
|
+
host: process.env.HOST ?? '127.0.0.1',
|
|
642
|
+
port: Number(process.env.PORT ?? 3000),
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
catch (error) {
|
|
646
|
+
if (app)
|
|
647
|
+
await app.close();
|
|
648
|
+
console.error(`CFlow 启动失败:${error instanceof Error ? error.message : String(error)}`);
|
|
649
|
+
process.exitCode = 1;
|
|
650
|
+
}
|
|
652
651
|
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { lstat, readdir, realpath } from 'node:fs/promises';
|
|
2
|
+
import { relative, resolve, sep } from 'node:path';
|
|
3
|
+
const EXCLUDED_DIRECTORIES = new Set([
|
|
4
|
+
'.git',
|
|
5
|
+
'.cflow',
|
|
6
|
+
'node_modules',
|
|
7
|
+
'dist',
|
|
8
|
+
'build',
|
|
9
|
+
'coverage',
|
|
10
|
+
]);
|
|
11
|
+
const RESULT_LIMIT = 200;
|
|
12
|
+
function normalizedRelativePath(value) {
|
|
13
|
+
const path = value.trim().replaceAll('\\', '/');
|
|
14
|
+
if (!path || path.startsWith('/') || /^[A-Za-z]:/.test(path))
|
|
15
|
+
return null;
|
|
16
|
+
const parts = path.split('/');
|
|
17
|
+
if (parts.some((part) => !part || part === '.' || part === '..'))
|
|
18
|
+
return null;
|
|
19
|
+
if (parts.some((part) => EXCLUDED_DIRECTORIES.has(part)))
|
|
20
|
+
return null;
|
|
21
|
+
return path;
|
|
22
|
+
}
|
|
23
|
+
async function isWorkspaceFile(root, path) {
|
|
24
|
+
const normalized = normalizedRelativePath(path);
|
|
25
|
+
if (!normalized)
|
|
26
|
+
return false;
|
|
27
|
+
const target = resolve(root, ...normalized.split('/'));
|
|
28
|
+
try {
|
|
29
|
+
const [stat, resolvedTarget] = await Promise.all([lstat(target), realpath(target)]);
|
|
30
|
+
const scope = relative(root, resolvedTarget);
|
|
31
|
+
return (stat.isFile() && scope !== '..' && !scope.startsWith(`..${sep}`) && !scope.startsWith(sep));
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async function indexedFiles(root) {
|
|
38
|
+
const files = [];
|
|
39
|
+
const walk = async (directory, prefix) => {
|
|
40
|
+
const entries = await readdir(directory, { withFileTypes: true }).catch(() => []);
|
|
41
|
+
for (const entry of entries) {
|
|
42
|
+
if (entry.isSymbolicLink())
|
|
43
|
+
continue;
|
|
44
|
+
const path = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
45
|
+
if (entry.isDirectory()) {
|
|
46
|
+
if (!EXCLUDED_DIRECTORIES.has(entry.name))
|
|
47
|
+
await walk(resolve(directory, entry.name), path);
|
|
48
|
+
}
|
|
49
|
+
else if (entry.isFile()) {
|
|
50
|
+
files.push(path);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
await walk(root, '');
|
|
55
|
+
return files.sort((left, right) => left.localeCompare(right));
|
|
56
|
+
}
|
|
57
|
+
export async function searchWorkspaceFiles(root, input) {
|
|
58
|
+
const query = String(input.query ?? '')
|
|
59
|
+
.trim()
|
|
60
|
+
.toLocaleLowerCase();
|
|
61
|
+
const safeSelected = [
|
|
62
|
+
...new Set((input.selected ?? []).map(normalizedRelativePath).filter(Boolean)),
|
|
63
|
+
];
|
|
64
|
+
const files = await indexedFiles(root);
|
|
65
|
+
const filtered = query ? files.filter((path) => path.toLocaleLowerCase().includes(query)) : files;
|
|
66
|
+
const missing = [];
|
|
67
|
+
for (const path of safeSelected)
|
|
68
|
+
if (!(await isWorkspaceFile(root, path)))
|
|
69
|
+
missing.push(path);
|
|
70
|
+
return {
|
|
71
|
+
matches: filtered.slice(0, RESULT_LIMIT),
|
|
72
|
+
missing,
|
|
73
|
+
truncated: filtered.length > RESULT_LIMIT,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
export async function missingWorkspaceFiles(root, paths) {
|
|
77
|
+
const missing = [];
|
|
78
|
+
for (const path of paths)
|
|
79
|
+
if (!(await isWorkspaceFile(root, path)))
|
|
80
|
+
missing.push(path);
|
|
81
|
+
return missing;
|
|
82
|
+
}
|
package/dist/src/workspace.js
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
|
-
import { accessSync, constants, realpathSync, statSync } from 'node:fs';
|
|
2
|
-
import { isAbsolute, relative, resolve, sep } from 'node:path';
|
|
1
|
+
import { accessSync, constants, existsSync, mkdirSync, readFileSync, realpathSync, statSync, writeFileSync, } from 'node:fs';
|
|
2
|
+
import { isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
3
|
+
const gitIgnoreStart = '# cflow:local-data';
|
|
4
|
+
const gitIgnoreEnd = '# /cflow:local-data';
|
|
5
|
+
const managedGitIgnore = `${gitIgnoreStart}
|
|
6
|
+
/cflow.sqlite
|
|
7
|
+
/cflow.sqlite-shm
|
|
8
|
+
/cflow.sqlite-wal
|
|
9
|
+
/cflow.sqlite-journal
|
|
10
|
+
/flows/
|
|
11
|
+
${gitIgnoreEnd}`;
|
|
3
12
|
export function isWithinDirectory(root, candidate) {
|
|
4
13
|
const path = relative(resolve(root), resolve(candidate));
|
|
5
14
|
return path === '' || (path !== '..' && !path.startsWith(`..${sep}`) && !isAbsolute(path));
|
|
@@ -24,3 +33,35 @@ export function requireWorkspaceRoot(value) {
|
|
|
24
33
|
throw new Error('FLOW_WORKSPACE_REQUIRED');
|
|
25
34
|
return value;
|
|
26
35
|
}
|
|
36
|
+
function maintainWorkspaceGitIgnore(dataDirectory) {
|
|
37
|
+
const file = join(dataDirectory, '.gitignore');
|
|
38
|
+
const current = existsSync(file) ? readFileSync(file, 'utf8') : '';
|
|
39
|
+
const start = current.indexOf(gitIgnoreStart);
|
|
40
|
+
const end = current.indexOf(gitIgnoreEnd);
|
|
41
|
+
let next;
|
|
42
|
+
if (start >= 0 && end >= start) {
|
|
43
|
+
next = `${current.slice(0, start)}${managedGitIgnore}${current.slice(end + gitIgnoreEnd.length)}`;
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
next = `${current.trimEnd()}${current.trim() ? '\n\n' : ''}${managedGitIgnore}\n`;
|
|
47
|
+
}
|
|
48
|
+
if (next !== current)
|
|
49
|
+
writeFileSync(file, next);
|
|
50
|
+
}
|
|
51
|
+
export function initializeWorkspace(value = process.cwd(), environment = process.env) {
|
|
52
|
+
if (Object.prototype.hasOwnProperty.call(environment, 'CF_DB'))
|
|
53
|
+
throw new Error('CF_DB_UNSUPPORTED');
|
|
54
|
+
const root = validateWorkspaceRoot(resolve(value), 'WORKSPACE_UNAVAILABLE');
|
|
55
|
+
const requestedDataDirectory = join(root, '.cflow');
|
|
56
|
+
mkdirSync(requestedDataDirectory, { recursive: true });
|
|
57
|
+
const dataDirectory = realpathSync(requestedDataDirectory);
|
|
58
|
+
if (!isWithinDirectory(root, dataDirectory))
|
|
59
|
+
throw new Error('WORKSPACE_DATA_OUTSIDE_ROOT');
|
|
60
|
+
accessSync(dataDirectory, constants.R_OK | constants.W_OK);
|
|
61
|
+
maintainWorkspaceGitIgnore(dataDirectory);
|
|
62
|
+
return {
|
|
63
|
+
root,
|
|
64
|
+
dataDirectory,
|
|
65
|
+
databasePath: join(dataDirectory, 'cflow.sqlite'),
|
|
66
|
+
};
|
|
67
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hmj-ai/cflow",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "以 Flow 为核心的本机多 Agent 编排工作台",
|
|
5
5
|
"main": "dist/src/server.js",
|
|
6
6
|
"bin": {
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
"@xyflow/react": "^12.11.3",
|
|
52
52
|
"ajv": "^8.20.0",
|
|
53
53
|
"better-sqlite3": "^13.0.3",
|
|
54
|
+
"cross-spawn": "^7.0.6",
|
|
54
55
|
"fastify": "^5.12.1",
|
|
55
56
|
"lucide-react": "^1.34.0",
|
|
56
57
|
"react": "^19.2.8",
|