@hmj-ai/cflow 1.1.0 → 1.2.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-C1_cd_s8.css +1 -0
- package/dist/public/assets/index-DsBDM4p-.js +15 -0
- package/dist/public/index.html +2 -2
- 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 +44 -107
- 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,22 @@ 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
10
|
import { 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';
|
|
16
15
|
import { collectGroundingFailures, groundingError, assertAttachmentGrounding, applyFlowRevision, buildProposalGraph, flowProposalOutputSchema, flowProposalPrompt, matchPublishedCapabilities, prepareSkillAttachments, } from './proposal.js';
|
|
17
16
|
import { flowAgentContext, flowAgentFallback, flowAgentOutputSchema, flowAgentPrompt, normalizeAgentResponse, } from './flow-agent.js';
|
|
18
|
-
export function createApp(store = new Store()) {
|
|
17
|
+
export function createApp(store = new Store(), requestedWorkspaceRoot = process.cwd()) {
|
|
18
|
+
const workspaceRoot = validateWorkspaceRoot(requestedWorkspaceRoot, 'WORKSPACE_UNAVAILABLE');
|
|
19
19
|
const versions = () => store.list('cf_versions');
|
|
20
20
|
const flowCompilations = () => store.flowCompilations();
|
|
21
|
-
const runtimes = new RuntimeManager(store);
|
|
21
|
+
const runtimes = new RuntimeManager(store, { projectRoot: workspaceRoot });
|
|
22
22
|
const testPlans = new Map();
|
|
23
23
|
const testCatalogs = new Map();
|
|
24
24
|
const executorRegistry = builtins();
|
|
@@ -27,42 +27,7 @@ export function createApp(store = new Store()) {
|
|
|
27
27
|
...versions(),
|
|
28
28
|
...[...testCatalogs.values()].flat(),
|
|
29
29
|
]);
|
|
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
|
-
};
|
|
30
|
+
const scopeFlowDraft = (draft) => ({ ...draft, workspaceRoot });
|
|
66
31
|
const app = Fastify({ logger: true });
|
|
67
32
|
app.register(fastifyMultipart, {
|
|
68
33
|
// Do not silently truncate large skill bundles; files are streamed to disk.
|
|
@@ -159,7 +124,7 @@ export function createApp(store = new Store()) {
|
|
|
159
124
|
await rm(attachments.root, { recursive: true, force: true });
|
|
160
125
|
return proposal;
|
|
161
126
|
}
|
|
162
|
-
const draft =
|
|
127
|
+
const draft = scopeFlowDraft(proposal.flowDraft);
|
|
163
128
|
let archivePath;
|
|
164
129
|
if (attachments) {
|
|
165
130
|
archivePath = join('.cflow', 'flows', draft.flowId, 'attachments');
|
|
@@ -191,49 +156,9 @@ export function createApp(store = new Store()) {
|
|
|
191
156
|
};
|
|
192
157
|
app.register(fastifyStatic, { root: runtimePublicRoot });
|
|
193
158
|
app.get('/', async (_, reply) => reply.sendFile('index.html'));
|
|
159
|
+
app.get('/api/workspace', async () => ({ root: workspaceRoot }));
|
|
194
160
|
app.get('/api/settings', async () => runtimes.settings());
|
|
195
161
|
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
162
|
const runtimeCatalog = () => Promise.all(runtimes.profiles().map(async (profile) => ({
|
|
238
163
|
...profile,
|
|
239
164
|
health: await runtimes.health(profile.id),
|
|
@@ -288,7 +213,7 @@ export function createApp(store = new Store()) {
|
|
|
288
213
|
throw new Error('FLOW_DRAFT_ID_MISMATCH');
|
|
289
214
|
if (!req.body.flowId?.trim() || !req.body.name?.trim() || !req.body.objective?.trim())
|
|
290
215
|
throw new Error('FLOW_DRAFT_INVALID');
|
|
291
|
-
const draft =
|
|
216
|
+
const draft = scopeFlowDraft(req.body);
|
|
292
217
|
store.save('flow_drafts', draft.flowId, draft);
|
|
293
218
|
return draft;
|
|
294
219
|
});
|
|
@@ -308,13 +233,13 @@ export function createApp(store = new Store()) {
|
|
|
308
233
|
app.delete('/api/flows/:id', async (req, reply) => deletePublishedFlow(req.params.id, reply));
|
|
309
234
|
app.delete('/api/flows/:flowId/:flowVersion', async (req, reply) => deletePublishedFlow(`${req.params.flowId}@${req.params.flowVersion}`, reply));
|
|
310
235
|
app.post('/api/flow-compilations', async (req) => {
|
|
311
|
-
|
|
236
|
+
const scopedDraft = scopeFlowDraft(req.body.flowDraft);
|
|
312
237
|
const candidates = (req.body.cfDrafts ?? []).map(compileCF);
|
|
313
238
|
const catalog = new Map([...versions(), ...candidates].map((version) => [
|
|
314
239
|
`${version.cfId}@${version.version}`,
|
|
315
240
|
version,
|
|
316
241
|
]));
|
|
317
|
-
const selectedDraft = applyCompileRuntime(
|
|
242
|
+
const selectedDraft = applyCompileRuntime(scopedDraft, req.body.runtimeId);
|
|
318
243
|
const compiled = compileFlow(selectedDraft, catalog);
|
|
319
244
|
const plan = req.body.runtimeId
|
|
320
245
|
? await pinRuntimeProfiles(compiled, [...versions(), ...candidates])
|
|
@@ -343,7 +268,6 @@ export function createApp(store = new Store()) {
|
|
|
343
268
|
return new Error(code);
|
|
344
269
|
};
|
|
345
270
|
const body = multipart?.fields ?? req.body;
|
|
346
|
-
const workspaceRoot = validateWorkspaceRoot(body.workspaceRoot, 'FLOW_WORKSPACE_UNAVAILABLE');
|
|
347
271
|
const objective = String(body.objective ?? '').trim();
|
|
348
272
|
if (!objective)
|
|
349
273
|
throw await failing('OBJECTIVE_REQUIRED');
|
|
@@ -455,15 +379,18 @@ export function createApp(store = new Store()) {
|
|
|
455
379
|
const message = req.body.message?.trim();
|
|
456
380
|
if (!message)
|
|
457
381
|
throw new Error('AGENT_MESSAGE_REQUIRED');
|
|
382
|
+
const request = {
|
|
383
|
+
...req.body,
|
|
384
|
+
flowDraft: req.body.flowDraft ? scopeFlowDraft(req.body.flowDraft) : null,
|
|
385
|
+
};
|
|
458
386
|
const catalog = versions();
|
|
459
|
-
const fallback = flowAgentFallback({ ...
|
|
460
|
-
const runtimeId =
|
|
387
|
+
const fallback = flowAgentFallback({ ...request, message }, catalog);
|
|
388
|
+
const runtimeId = request.runtimeId?.trim() || runtimes.settings().defaultRuntimeId;
|
|
461
389
|
const profile = runtimeId ? runtimes.profile(runtimeId) : undefined;
|
|
462
390
|
if (!profile || profile.backend === 'builtin')
|
|
463
391
|
return { ...fallback, fallback: true };
|
|
464
|
-
if (!
|
|
392
|
+
if (!request.flowDraft)
|
|
465
393
|
throw new Error('FLOW_WORKSPACE_REQUIRED');
|
|
466
|
-
const workspaceRoot = operationalWorkspace(req.body.flowDraft);
|
|
467
394
|
const health = await runtimes.health(runtimeId);
|
|
468
395
|
if (health.status !== 'available')
|
|
469
396
|
return { ...fallback, fallback: true };
|
|
@@ -476,13 +403,13 @@ export function createApp(store = new Store()) {
|
|
|
476
403
|
availableRuntimes.push({ id: item.id, name: item.name });
|
|
477
404
|
}
|
|
478
405
|
const grounded = Boolean(req.body.attachments?.length);
|
|
479
|
-
const response = await runtimes.execute(runtimeId, flowAgentPrompt(message, grounded), flowAgentContext({ ...
|
|
406
|
+
const response = await runtimes.execute(runtimeId, flowAgentPrompt(message, grounded), flowAgentContext({ ...request, message }, availableRuntimes, catalog), AbortSignal.timeout(runtimes.settings().testTimeoutMs), [], flowAgentOutputSchema(grounded), { workspaceRoot });
|
|
480
407
|
const normalized = normalizeAgentResponse(response);
|
|
481
408
|
if (normalized.intent === 'answer')
|
|
482
409
|
return { ...normalized, runtimeId };
|
|
483
410
|
if (grounded)
|
|
484
411
|
assertAttachmentGrounding(normalized, req.body.attachments ?? []);
|
|
485
|
-
const revised = applyFlowRevision(
|
|
412
|
+
const revised = applyFlowRevision(request.flowDraft, request.cfDrafts ?? [], normalized.stages, { catalog, runtimeId });
|
|
486
413
|
store.db.transaction(() => {
|
|
487
414
|
for (const draft of revised.cfDrafts)
|
|
488
415
|
store.save('cf_drafts', draft.cfId, draft);
|
|
@@ -516,8 +443,7 @@ export function createApp(store = new Store()) {
|
|
|
516
443
|
return { deleted: true };
|
|
517
444
|
});
|
|
518
445
|
app.post('/api/flows', async (req) => {
|
|
519
|
-
const draft =
|
|
520
|
-
operationalWorkspace(draft);
|
|
446
|
+
const draft = scopeFlowDraft(req.body);
|
|
521
447
|
const catalog = versions();
|
|
522
448
|
const plan = await pinRuntimeProfiles(compileFlow(draft, new Map(catalog.map((v) => [`${v.cfId}@${v.version}`, v]))), catalog);
|
|
523
449
|
store.save('flow_drafts', draft.flowId, draft);
|
|
@@ -525,14 +451,14 @@ export function createApp(store = new Store()) {
|
|
|
525
451
|
return plan;
|
|
526
452
|
});
|
|
527
453
|
app.post('/api/flow-tests', async (req) => {
|
|
528
|
-
|
|
454
|
+
const scopedDraft = scopeFlowDraft(req.body.flowDraft);
|
|
529
455
|
const candidateVersions = (req.body.cfDrafts ?? []).map(compileCF);
|
|
530
456
|
const catalog = new Map([...versions(), ...candidateVersions].map((version) => [
|
|
531
457
|
`${version.cfId}@${version.version}`,
|
|
532
458
|
version,
|
|
533
459
|
]));
|
|
534
|
-
const selectedDraft = applyCompileRuntime(
|
|
535
|
-
store.save('flow_drafts',
|
|
460
|
+
const selectedDraft = applyCompileRuntime(scopedDraft, req.body.runtimeId);
|
|
461
|
+
store.save('flow_drafts', scopedDraft.flowId, scopedDraft);
|
|
536
462
|
const compiled = compileFlow(selectedDraft, catalog);
|
|
537
463
|
const plan = await pinRuntimeProfiles(compiled, [...versions(), ...candidateVersions]);
|
|
538
464
|
const programs = [...versions(), ...candidateVersions].filter((version) => selectedDraft.nodes.some((node) => node.kind === 'cf-call' &&
|
|
@@ -557,7 +483,8 @@ export function createApp(store = new Store()) {
|
|
|
557
483
|
const plan = store.get('flow_versions', `${req.body.flowId}@${req.body.flowVersion}`);
|
|
558
484
|
if (!plan)
|
|
559
485
|
throw new Error('FLOW_VERSION_NOT_FOUND');
|
|
560
|
-
|
|
486
|
+
if (plan.workspaceRoot !== workspaceRoot)
|
|
487
|
+
throw new Error('FLOW_WORKSPACE_MISMATCH');
|
|
561
488
|
const resources = resolveResources(plan, req.body.resourceProfileId);
|
|
562
489
|
const id = newRunId();
|
|
563
490
|
store.createRun(id, `${plan.flowId}@${plan.flowVersion}`, req.body.input ?? {}, resources, req.body.resourceProfileId);
|
|
@@ -643,10 +570,20 @@ export const isDirectExecution = (entryPath = process.argv[1]) => {
|
|
|
643
570
|
}
|
|
644
571
|
};
|
|
645
572
|
if (isDirectExecution()) {
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
573
|
+
let app;
|
|
574
|
+
try {
|
|
575
|
+
const workspace = initializeWorkspace();
|
|
576
|
+
app = createApp(new Store(workspace.databasePath), workspace.root);
|
|
577
|
+
app.log.info({ workspace: workspace.root, database: workspace.databasePath }, 'workspace ready');
|
|
578
|
+
await app.listen({
|
|
579
|
+
host: process.env.HOST ?? '127.0.0.1',
|
|
580
|
+
port: Number(process.env.PORT ?? 3000),
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
catch (error) {
|
|
584
|
+
if (app)
|
|
585
|
+
await app.close();
|
|
586
|
+
console.error(`CFlow 启动失败:${error instanceof Error ? error.message : String(error)}`);
|
|
587
|
+
process.exitCode = 1;
|
|
588
|
+
}
|
|
652
589
|
}
|
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.2.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",
|