@actiondock/core 2.0.1 → 2.0.3
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/README.md +59 -27
- package/package.json +2 -2
- package/src/build/builder.ts +20 -7
- package/src/catalog/action-index.ts +92 -0
- package/src/catalog/action-resolver.ts +120 -0
- package/src/catalog/index.ts +5 -0
- package/src/catalog/location-registry.ts +134 -0
- package/src/catalog/package-catalog.ts +87 -0
- package/src/catalog/types.ts +83 -0
- package/src/doctor/doctor.ts +59 -33
- package/src/execution/index.ts +2 -0
- package/src/execution/service.ts +282 -0
- package/src/execution/types.ts +73 -0
- package/src/export/skill.ts +7 -6
- package/src/export/templates.ts +1 -1
- package/src/index.ts +2 -0
- package/src/profile/client.ts +252 -2
- package/src/project/index.ts +1 -0
- package/src/project/init.ts +80 -33
- package/src/project/loader.ts +11 -12
- package/src/project/manifest.ts +96 -0
- package/src/project/types.ts +30 -0
- package/src/runtime/clock.ts +41 -0
- package/src/runtime/context.ts +24 -2
- package/src/runtime/events.ts +142 -0
- package/src/runtime/index.ts +4 -0
- package/src/runtime/process.ts +244 -0
- package/src/runtime/runner.ts +37 -3
- package/src/server/runtime-registry.ts +35 -0
- package/src/server/server.ts +994 -74
- package/src/server/types.ts +15 -0
- package/src/storage/driver.ts +126 -0
- package/src/storage/index.ts +4 -3
- package/src/storage/sqlite.ts +365 -258
- package/src/storage/types.ts +42 -22
package/src/profile/client.ts
CHANGED
|
@@ -291,12 +291,262 @@ export async function fetchRemoteActionShow(
|
|
|
291
291
|
|
|
292
292
|
export async function fetchRemoteInfo(
|
|
293
293
|
serverUrl: string,
|
|
294
|
-
token?: string
|
|
294
|
+
token?: string,
|
|
295
|
+
options?: { intent?: string; package?: string; tree?: boolean }
|
|
295
296
|
): Promise<any> {
|
|
297
|
+
const params = new URLSearchParams();
|
|
298
|
+
if (options?.intent) params.set("intent", options.intent);
|
|
299
|
+
if (options?.package) params.set("package", options.package);
|
|
300
|
+
if (options?.tree) params.set("tree", "true");
|
|
301
|
+
const qs = params.toString() ? `?${params.toString()}` : "";
|
|
296
302
|
return fetchRemoteJson(
|
|
297
303
|
serverUrl,
|
|
298
|
-
|
|
304
|
+
`/api/v1/info${qs}`,
|
|
299
305
|
token,
|
|
300
306
|
{ errorPrefix: "Failed to fetch remote info" }
|
|
301
307
|
);
|
|
302
308
|
}
|
|
309
|
+
|
|
310
|
+
export async function fetchRemoteDoctor(
|
|
311
|
+
serverUrl: string,
|
|
312
|
+
token?: string,
|
|
313
|
+
targetPackage?: string
|
|
314
|
+
): Promise<any> {
|
|
315
|
+
const query = targetPackage ? `?package=${encodeURIComponent(targetPackage)}` : "";
|
|
316
|
+
return fetchRemoteJson(
|
|
317
|
+
serverUrl,
|
|
318
|
+
`/api/v1/doctor${query}`,
|
|
319
|
+
token,
|
|
320
|
+
{ errorPrefix: "Failed to fetch remote doctor report" }
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export async function fetchRemotePlaybooks(
|
|
325
|
+
serverUrl: string,
|
|
326
|
+
token?: string,
|
|
327
|
+
options?: { intent?: string; package?: string }
|
|
328
|
+
): Promise<Array<{ id: string; description: string; actions: string[]; packageId: string; filePath: string }>> {
|
|
329
|
+
const params = new URLSearchParams();
|
|
330
|
+
if (options?.intent) params.set("intent", options.intent);
|
|
331
|
+
if (options?.package) params.set("package", options.package);
|
|
332
|
+
const qs = params.toString() ? `?${params.toString()}` : "";
|
|
333
|
+
return fetchRemoteJson(
|
|
334
|
+
serverUrl,
|
|
335
|
+
`/api/v1/playbooks${qs}`,
|
|
336
|
+
token,
|
|
337
|
+
{ errorPrefix: "Failed to fetch remote playbooks" }
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export async function fetchRemotePlaybookShow(
|
|
342
|
+
serverUrl: string,
|
|
343
|
+
playbookId: string,
|
|
344
|
+
token?: string
|
|
345
|
+
): Promise<any> {
|
|
346
|
+
return fetchRemoteJson(
|
|
347
|
+
serverUrl,
|
|
348
|
+
`/api/v1/playbooks/${encodeURIComponent(playbookId)}`,
|
|
349
|
+
token,
|
|
350
|
+
{ errorPrefix: `Failed to fetch remote playbook '${playbookId}'` }
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export async function fetchRemoteRuns(
|
|
355
|
+
serverUrl: string,
|
|
356
|
+
token?: string,
|
|
357
|
+
options?: { status?: string; actionId?: string; packageId?: string; intent?: string; limit?: number }
|
|
358
|
+
): Promise<{ ok: boolean; total: number; items: RunRecord[] }> {
|
|
359
|
+
const params = new URLSearchParams();
|
|
360
|
+
if (options?.status) params.set("status", options.status);
|
|
361
|
+
if (options?.actionId) params.set("actionId", options.actionId);
|
|
362
|
+
if (options?.packageId) params.set("packageId", options.packageId);
|
|
363
|
+
if (options?.intent) params.set("intent", options.intent);
|
|
364
|
+
if (options?.limit) params.set("limit", String(options.limit));
|
|
365
|
+
const qs = params.toString() ? `?${params.toString()}` : "";
|
|
366
|
+
return fetchRemoteJson(
|
|
367
|
+
serverUrl,
|
|
368
|
+
`/api/v1/runs${qs}`,
|
|
369
|
+
token,
|
|
370
|
+
{ errorPrefix: "Failed to fetch remote runs" }
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export async function clearRemoteRuns(
|
|
375
|
+
serverUrl: string,
|
|
376
|
+
token?: string,
|
|
377
|
+
options?: { packageId?: string; actionId?: string; status?: string }
|
|
378
|
+
): Promise<{ ok: boolean; clearedCount: number }> {
|
|
379
|
+
return fetchRemoteJson(
|
|
380
|
+
serverUrl,
|
|
381
|
+
"/api/v1/runs/clear",
|
|
382
|
+
token,
|
|
383
|
+
{
|
|
384
|
+
method: "POST",
|
|
385
|
+
body: options || {},
|
|
386
|
+
errorPrefix: "Failed to clear remote runs",
|
|
387
|
+
}
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export async function fetchRemoteStateList(
|
|
392
|
+
serverUrl: string,
|
|
393
|
+
token?: string,
|
|
394
|
+
options?: { package?: string; namespace?: string; prefix?: string }
|
|
395
|
+
): Promise<{ ok: boolean; packageId: string; keys: string[] }> {
|
|
396
|
+
const params = new URLSearchParams();
|
|
397
|
+
if (options?.package) params.set("package", options.package);
|
|
398
|
+
if (options?.namespace !== undefined) params.set("namespace", options.namespace);
|
|
399
|
+
if (options?.prefix) params.set("prefix", options.prefix);
|
|
400
|
+
const qs = params.toString() ? `?${params.toString()}` : "";
|
|
401
|
+
return fetchRemoteJson(
|
|
402
|
+
serverUrl,
|
|
403
|
+
`/api/v1/state${qs}`,
|
|
404
|
+
token,
|
|
405
|
+
{ errorPrefix: "Failed to list remote state keys" }
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
export async function getRemoteStateKey(
|
|
410
|
+
serverUrl: string,
|
|
411
|
+
key: string,
|
|
412
|
+
token?: string,
|
|
413
|
+
options?: { package?: string; namespace?: string }
|
|
414
|
+
): Promise<any> {
|
|
415
|
+
const params = new URLSearchParams();
|
|
416
|
+
if (options?.package) params.set("package", options.package);
|
|
417
|
+
if (options?.namespace !== undefined) params.set("namespace", options.namespace);
|
|
418
|
+
const qs = params.toString() ? `?${params.toString()}` : "";
|
|
419
|
+
return fetchRemoteJson(
|
|
420
|
+
serverUrl,
|
|
421
|
+
`/api/v1/state/${encodeURIComponent(key)}${qs}`,
|
|
422
|
+
token,
|
|
423
|
+
{ errorPrefix: `Failed to fetch remote state key '${key}'` }
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
export async function setRemoteStateKey(
|
|
428
|
+
serverUrl: string,
|
|
429
|
+
key: string,
|
|
430
|
+
value: unknown,
|
|
431
|
+
token?: string,
|
|
432
|
+
options?: { package?: string; namespace?: string; ttl?: number }
|
|
433
|
+
): Promise<any> {
|
|
434
|
+
return fetchRemoteJson(
|
|
435
|
+
serverUrl,
|
|
436
|
+
`/api/v1/state/${encodeURIComponent(key)}`,
|
|
437
|
+
token,
|
|
438
|
+
{
|
|
439
|
+
method: "PUT",
|
|
440
|
+
body: {
|
|
441
|
+
value,
|
|
442
|
+
package: options?.package,
|
|
443
|
+
namespace: options?.namespace,
|
|
444
|
+
ttl: options?.ttl,
|
|
445
|
+
},
|
|
446
|
+
errorPrefix: `Failed to set remote state key '${key}'`,
|
|
447
|
+
}
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
export async function deleteRemoteStateKey(
|
|
452
|
+
serverUrl: string,
|
|
453
|
+
key: string,
|
|
454
|
+
token?: string,
|
|
455
|
+
options?: { package?: string; namespace?: string }
|
|
456
|
+
): Promise<any> {
|
|
457
|
+
const params = new URLSearchParams();
|
|
458
|
+
if (options?.package) params.set("package", options.package);
|
|
459
|
+
if (options?.namespace !== undefined) params.set("namespace", options.namespace);
|
|
460
|
+
const qs = params.toString() ? `?${params.toString()}` : "";
|
|
461
|
+
return fetchRemoteJson(
|
|
462
|
+
serverUrl,
|
|
463
|
+
`/api/v1/state/${encodeURIComponent(key)}${qs}`,
|
|
464
|
+
token,
|
|
465
|
+
{
|
|
466
|
+
method: "DELETE",
|
|
467
|
+
errorPrefix: `Failed to delete remote state key '${key}'`,
|
|
468
|
+
}
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
export async function clearRemoteState(
|
|
473
|
+
serverUrl: string,
|
|
474
|
+
token?: string,
|
|
475
|
+
options?: { package?: string; namespace?: string; prefix?: string; all?: boolean }
|
|
476
|
+
): Promise<{ ok: boolean; packageId: string; clearedCount: number }> {
|
|
477
|
+
return fetchRemoteJson(
|
|
478
|
+
serverUrl,
|
|
479
|
+
"/api/v1/state/clear",
|
|
480
|
+
token,
|
|
481
|
+
{
|
|
482
|
+
method: "POST",
|
|
483
|
+
body: options || {},
|
|
484
|
+
errorPrefix: "Failed to clear remote state",
|
|
485
|
+
}
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
export async function fetchRemoteConfig(
|
|
490
|
+
serverUrl: string,
|
|
491
|
+
token?: string,
|
|
492
|
+
packageId?: string
|
|
493
|
+
): Promise<any> {
|
|
494
|
+
const query = packageId ? `?package=${encodeURIComponent(packageId)}` : "";
|
|
495
|
+
return fetchRemoteJson(
|
|
496
|
+
serverUrl,
|
|
497
|
+
`/api/v1/config${query}`,
|
|
498
|
+
token,
|
|
499
|
+
{ errorPrefix: "Failed to fetch remote config" }
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
export async function setRemoteConfig(
|
|
504
|
+
serverUrl: string,
|
|
505
|
+
key: string,
|
|
506
|
+
value: unknown,
|
|
507
|
+
token?: string,
|
|
508
|
+
packageId?: string
|
|
509
|
+
): Promise<any> {
|
|
510
|
+
return fetchRemoteJson(
|
|
511
|
+
serverUrl,
|
|
512
|
+
"/api/v1/config",
|
|
513
|
+
token,
|
|
514
|
+
{
|
|
515
|
+
method: "PUT",
|
|
516
|
+
body: { key, value, package: packageId },
|
|
517
|
+
errorPrefix: `Failed to set remote config '${key}'`,
|
|
518
|
+
}
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
export async function deleteRemoteConfig(
|
|
523
|
+
serverUrl: string,
|
|
524
|
+
key: string,
|
|
525
|
+
token?: string,
|
|
526
|
+
packageId?: string
|
|
527
|
+
): Promise<any> {
|
|
528
|
+
const query = packageId ? `?package=${encodeURIComponent(packageId)}` : "";
|
|
529
|
+
return fetchRemoteJson(
|
|
530
|
+
serverUrl,
|
|
531
|
+
`/api/v1/config/${encodeURIComponent(key)}${query}`,
|
|
532
|
+
token,
|
|
533
|
+
{
|
|
534
|
+
method: "DELETE",
|
|
535
|
+
errorPrefix: `Failed to delete remote config '${key}'`,
|
|
536
|
+
}
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
export async function fetchRemoteConfigEnv(
|
|
541
|
+
serverUrl: string,
|
|
542
|
+
token?: string,
|
|
543
|
+
packageId?: string
|
|
544
|
+
): Promise<any> {
|
|
545
|
+
const query = packageId ? `?package=${encodeURIComponent(packageId)}` : "";
|
|
546
|
+
return fetchRemoteJson(
|
|
547
|
+
serverUrl,
|
|
548
|
+
`/api/v1/config/env${query}`,
|
|
549
|
+
token,
|
|
550
|
+
{ errorPrefix: "Failed to fetch remote config env checks" }
|
|
551
|
+
);
|
|
552
|
+
}
|
package/src/project/index.ts
CHANGED
package/src/project/init.ts
CHANGED
|
@@ -16,13 +16,14 @@ export interface InitOptions {
|
|
|
16
16
|
/**
|
|
17
17
|
* 在目标目录初始化一个完整的 ActionDock 2.0 Action Package 脚手架。
|
|
18
18
|
* 生成内容包括:
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
19
|
+
* - actiondock.json(项目元数据与配置声明)
|
|
20
|
+
* - actiondock.manifest.json(声明式元数据清单事实源)
|
|
21
|
+
* - package.json(Node.js 标准脚本与依赖声明)
|
|
22
|
+
* - tsconfig.json(现代 NodeNext 模块规范)
|
|
23
|
+
* - .gitignore(排除持久化 db、node_modules、dist)
|
|
24
|
+
* - actions/greet.ts(标准示例 Action,演示 config、state、log 使用)
|
|
25
|
+
* - playbooks/greet-user.md(标准 SOP Playbook 演示)
|
|
26
|
+
* - tests/greet.test.ts(基于 node:test 与 @actiondock/testing 的测试用例)
|
|
26
27
|
*
|
|
27
28
|
* @param targetDir 目标项目目录
|
|
28
29
|
* @param options 初始化选项
|
|
@@ -58,20 +59,61 @@ export function initProject(targetDir: string, options: InitOptions = {}): void
|
|
|
58
59
|
JSON.stringify(actiondockJson, null, 2) + "\n"
|
|
59
60
|
);
|
|
60
61
|
|
|
61
|
-
// 2.
|
|
62
|
+
// 2. actiondock.manifest.json
|
|
63
|
+
const manifestJson = {
|
|
64
|
+
schemaVersion: 1,
|
|
65
|
+
actions: {
|
|
66
|
+
"sample.greet": {
|
|
67
|
+
entry: "actions/greet.ts",
|
|
68
|
+
description: "Greeting action demonstrating basic input, config, and state usage",
|
|
69
|
+
inputSchema: {
|
|
70
|
+
type: "object",
|
|
71
|
+
properties: {
|
|
72
|
+
name: {
|
|
73
|
+
type: "string",
|
|
74
|
+
description: "Name of the person to greet",
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
required: ["name"],
|
|
78
|
+
},
|
|
79
|
+
outputSchema: {
|
|
80
|
+
type: "object",
|
|
81
|
+
properties: {
|
|
82
|
+
message: { type: "string" },
|
|
83
|
+
timesGreeted: { type: "number" },
|
|
84
|
+
},
|
|
85
|
+
required: ["message", "timesGreeted"],
|
|
86
|
+
},
|
|
87
|
+
uses: [],
|
|
88
|
+
tags: ["sample"],
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
assets: [],
|
|
92
|
+
};
|
|
93
|
+
writeFileSync(
|
|
94
|
+
join(root, "actiondock.manifest.json"),
|
|
95
|
+
JSON.stringify(manifestJson, null, 2) + "\n"
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
// 3. package.json
|
|
62
99
|
const packageJson = {
|
|
63
100
|
name: id,
|
|
64
101
|
version: "0.1.0",
|
|
65
102
|
description,
|
|
66
103
|
type: "module",
|
|
67
104
|
scripts: {
|
|
68
|
-
test: "
|
|
105
|
+
test: "node --import tsx --test tests/*.test.ts",
|
|
106
|
+
},
|
|
107
|
+
engines: {
|
|
108
|
+
node: ">=22.12.0",
|
|
69
109
|
},
|
|
70
110
|
dependencies: {
|
|
71
|
-
"@actiondock/sdk": "^2.0.
|
|
111
|
+
"@actiondock/sdk": "^2.0.3",
|
|
72
112
|
},
|
|
73
113
|
devDependencies: {
|
|
74
|
-
"@
|
|
114
|
+
"@actiondock/testing": "^2.0.3",
|
|
115
|
+
"@types/node": "^22.12.0",
|
|
116
|
+
"tsx": "^4.19.0",
|
|
75
117
|
"typescript": "^5.7.0",
|
|
76
118
|
},
|
|
77
119
|
};
|
|
@@ -80,15 +122,15 @@ export function initProject(targetDir: string, options: InitOptions = {}): void
|
|
|
80
122
|
JSON.stringify(packageJson, null, 2) + "\n"
|
|
81
123
|
);
|
|
82
124
|
|
|
83
|
-
//
|
|
125
|
+
// 4. tsconfig.json
|
|
84
126
|
const tsconfigJson = {
|
|
85
127
|
compilerOptions: {
|
|
86
|
-
target: "
|
|
87
|
-
module: "
|
|
88
|
-
moduleResolution: "
|
|
128
|
+
target: "ES2022",
|
|
129
|
+
module: "NodeNext",
|
|
130
|
+
moduleResolution: "NodeNext",
|
|
89
131
|
strict: true,
|
|
90
132
|
skipLibCheck: true,
|
|
91
|
-
types: ["
|
|
133
|
+
types: ["node"],
|
|
92
134
|
},
|
|
93
135
|
};
|
|
94
136
|
writeFileSync(
|
|
@@ -96,16 +138,17 @@ export function initProject(targetDir: string, options: InitOptions = {}): void
|
|
|
96
138
|
JSON.stringify(tsconfigJson, null, 2) + "\n"
|
|
97
139
|
);
|
|
98
140
|
|
|
99
|
-
//
|
|
141
|
+
// 5. .gitignore
|
|
100
142
|
const gitignore = `.actiondock/
|
|
101
143
|
node_modules/
|
|
102
144
|
dist/
|
|
103
|
-
|
|
104
|
-
*.
|
|
145
|
+
build/
|
|
146
|
+
*.log
|
|
147
|
+
.env
|
|
105
148
|
`;
|
|
106
149
|
writeFileSync(join(root, ".gitignore"), gitignore);
|
|
107
150
|
|
|
108
|
-
//
|
|
151
|
+
// 6. actions/
|
|
109
152
|
const actionsDir = join(root, "actions");
|
|
110
153
|
mkdirSync(actionsDir, { recursive: true });
|
|
111
154
|
|
|
@@ -113,12 +156,15 @@ bun.lock
|
|
|
113
156
|
|
|
114
157
|
export default defineAction({
|
|
115
158
|
id: "sample.greet",
|
|
116
|
-
description: "
|
|
159
|
+
description: "Greeting action demonstrating basic input, config, and state usage",
|
|
117
160
|
|
|
118
161
|
inputSchema: {
|
|
119
162
|
type: "object",
|
|
120
163
|
properties: {
|
|
121
|
-
name: {
|
|
164
|
+
name: {
|
|
165
|
+
type: "string",
|
|
166
|
+
description: "Name of the person to greet",
|
|
167
|
+
},
|
|
122
168
|
},
|
|
123
169
|
required: ["name"],
|
|
124
170
|
},
|
|
@@ -127,9 +173,9 @@ export default defineAction({
|
|
|
127
173
|
type: "object",
|
|
128
174
|
properties: {
|
|
129
175
|
message: { type: "string" },
|
|
130
|
-
|
|
176
|
+
timesGreeted: { type: "number" },
|
|
131
177
|
},
|
|
132
|
-
required: ["message", "
|
|
178
|
+
required: ["message", "timesGreeted"],
|
|
133
179
|
},
|
|
134
180
|
|
|
135
181
|
async run(input: { name: string }, ctx) {
|
|
@@ -141,14 +187,14 @@ export default defineAction({
|
|
|
141
187
|
|
|
142
188
|
return {
|
|
143
189
|
message: \`\${greeting}, \${input.name}!\`,
|
|
144
|
-
|
|
190
|
+
timesGreeted: count,
|
|
145
191
|
};
|
|
146
192
|
},
|
|
147
193
|
});
|
|
148
194
|
`;
|
|
149
195
|
writeFileSync(join(actionsDir, "greet.ts"), sampleAction);
|
|
150
196
|
|
|
151
|
-
//
|
|
197
|
+
// 7. playbooks/
|
|
152
198
|
const playbooksDir = join(root, "playbooks");
|
|
153
199
|
mkdirSync(playbooksDir, { recursive: true });
|
|
154
200
|
|
|
@@ -166,12 +212,13 @@ actions:
|
|
|
166
212
|
`;
|
|
167
213
|
writeFileSync(join(playbooksDir, "greet-user.md"), samplePlaybook);
|
|
168
214
|
|
|
169
|
-
//
|
|
215
|
+
// 8. tests/
|
|
170
216
|
const testsDir = join(root, "tests");
|
|
171
217
|
mkdirSync(testsDir, { recursive: true });
|
|
172
218
|
|
|
173
|
-
const sampleTest = `import { describe,
|
|
174
|
-
import
|
|
219
|
+
const sampleTest = `import { describe, it } from "node:test";
|
|
220
|
+
import assert from "node:assert/strict";
|
|
221
|
+
import { createTestRuntime } from "@actiondock/testing";
|
|
175
222
|
import greetAction from "../actions/greet";
|
|
176
223
|
|
|
177
224
|
describe("greet action", () => {
|
|
@@ -181,12 +228,12 @@ describe("greet action", () => {
|
|
|
181
228
|
});
|
|
182
229
|
|
|
183
230
|
const res1 = await runtime.run(greetAction, { name: "Alice" });
|
|
184
|
-
|
|
185
|
-
|
|
231
|
+
assert.equal(res1.message, "Hi, Alice!");
|
|
232
|
+
assert.equal(await runtime.state.get("greet_count"), 1);
|
|
186
233
|
|
|
187
234
|
const res2 = await runtime.run(greetAction, { name: "Bob" });
|
|
188
|
-
|
|
189
|
-
|
|
235
|
+
assert.equal(res2.message, "Hi, Bob!");
|
|
236
|
+
assert.equal(await runtime.state.get("greet_count"), 2);
|
|
190
237
|
});
|
|
191
238
|
});
|
|
192
239
|
`;
|
package/src/project/loader.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
1
2
|
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
3
|
import { basename, dirname, join, resolve } from "node:path";
|
|
3
4
|
import YAML from "yaml";
|
|
@@ -68,29 +69,28 @@ export function loadProjectConfig(projectRoot: string): ProjectConfig {
|
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
/**
|
|
71
|
-
* 探测宿主系统中可用的包管理工具(优先级:
|
|
72
|
+
* 探测宿主系统中可用的包管理工具(优先级:pnpm > npm > yarn > bun)。
|
|
72
73
|
*/
|
|
73
74
|
function getInstallCommand(): string[] {
|
|
74
75
|
const candidates: [string, string][] = [
|
|
75
|
-
["bun", "install"],
|
|
76
76
|
["pnpm", "install"],
|
|
77
|
-
["yarn", "install"],
|
|
78
77
|
["npm", "install"],
|
|
78
|
+
["yarn", "install"],
|
|
79
|
+
["bun", "install"],
|
|
79
80
|
];
|
|
80
81
|
for (const [pm, action] of candidates) {
|
|
81
82
|
try {
|
|
82
|
-
const check =
|
|
83
|
-
|
|
84
|
-
stderr: "pipe",
|
|
83
|
+
const check = spawnSync(pm, ["--version"], {
|
|
84
|
+
stdio: "pipe",
|
|
85
85
|
});
|
|
86
|
-
if (check.
|
|
86
|
+
if (check.status === 0) {
|
|
87
87
|
return [pm, action];
|
|
88
88
|
}
|
|
89
89
|
} catch {
|
|
90
90
|
// 继续探测下一个候选包管理器
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
|
-
return ["
|
|
93
|
+
return ["npm", "install"];
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
/**
|
|
@@ -131,13 +131,12 @@ export function ensureProjectDependencies(projectRoot: string, force = false): b
|
|
|
131
131
|
`[actiondock] Installing dependencies using ${installCmd[0]} for '${pkg.name || basename(projectRoot)}'...\n`
|
|
132
132
|
);
|
|
133
133
|
|
|
134
|
-
const proc =
|
|
134
|
+
const proc = spawnSync(installCmd[0], installCmd.slice(1), {
|
|
135
135
|
cwd: projectRoot,
|
|
136
|
-
|
|
137
|
-
stderr: "pipe",
|
|
136
|
+
stdio: "pipe",
|
|
138
137
|
});
|
|
139
138
|
|
|
140
|
-
if (proc.
|
|
139
|
+
if (proc.status !== 0) {
|
|
141
140
|
const errText = proc.stderr?.toString() || `Unknown error during ${installCmd[0]} install`;
|
|
142
141
|
process.stderr.write(`[actiondock] Warning: Dependency installation failed: ${errText}\n`);
|
|
143
142
|
return false;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import type { ActionDockManifest, ActionManifestEntry } from "./types";
|
|
4
|
+
|
|
5
|
+
export const MANIFEST_FILE_NAME = "actiondock.manifest.json";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 读取并解析项目的声明式清单文件。
|
|
9
|
+
* 若文件不存在则返回 null。
|
|
10
|
+
*/
|
|
11
|
+
export function loadManifest(projectRoot: string): ActionDockManifest | null {
|
|
12
|
+
const filePath = join(projectRoot, MANIFEST_FILE_NAME);
|
|
13
|
+
if (!existsSync(filePath)) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
18
|
+
const parsed = JSON.parse(raw) as ActionDockManifest;
|
|
19
|
+
if (!parsed || typeof parsed !== "object" || parsed.schemaVersion !== 1) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
return parsed;
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 保存声明式清单文件至项目根目录。
|
|
30
|
+
*/
|
|
31
|
+
export function saveManifest(projectRoot: string, manifest: ActionDockManifest): void {
|
|
32
|
+
const filePath = join(projectRoot, MANIFEST_FILE_NAME);
|
|
33
|
+
writeFileSync(filePath, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 校验清单数据结构的合法性。
|
|
38
|
+
*/
|
|
39
|
+
export function validateManifest(manifest: unknown): { valid: boolean; errors?: string[] } {
|
|
40
|
+
if (!manifest || typeof manifest !== "object") {
|
|
41
|
+
return { valid: false, errors: ["Manifest must be an object"] };
|
|
42
|
+
}
|
|
43
|
+
const m = manifest as ActionDockManifest;
|
|
44
|
+
const errors: string[] = [];
|
|
45
|
+
|
|
46
|
+
if (m.schemaVersion !== 1) {
|
|
47
|
+
errors.push("Manifest 'schemaVersion' must be 1");
|
|
48
|
+
}
|
|
49
|
+
if (!m.actions || typeof m.actions !== "object") {
|
|
50
|
+
errors.push("Manifest 'actions' must be an object");
|
|
51
|
+
} else {
|
|
52
|
+
for (const [actionId, item] of Object.entries(m.actions)) {
|
|
53
|
+
if (!item || typeof item !== "object") {
|
|
54
|
+
errors.push(`Action entry '${actionId}' must be an object`);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (!item.entry || typeof item.entry !== "string") {
|
|
58
|
+
errors.push(`Action '${actionId}' must specify string 'entry'`);
|
|
59
|
+
}
|
|
60
|
+
if (item.uses && !Array.isArray(item.uses)) {
|
|
61
|
+
errors.push(`Action '${actionId}' property 'uses' must be an array`);
|
|
62
|
+
}
|
|
63
|
+
if (item.tags && !Array.isArray(item.tags)) {
|
|
64
|
+
errors.push(`Action '${actionId}' property 'tags' must be an array`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
valid: errors.length === 0,
|
|
71
|
+
errors: errors.length > 0 ? errors : undefined,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 为单个 Action 构建清单项。
|
|
77
|
+
*/
|
|
78
|
+
export function createManifestEntry(options: {
|
|
79
|
+
entry: string;
|
|
80
|
+
description?: string;
|
|
81
|
+
inputSchema?: Record<string, unknown> | boolean;
|
|
82
|
+
outputSchema?: Record<string, unknown> | boolean;
|
|
83
|
+
uses?: string[];
|
|
84
|
+
tags?: string[];
|
|
85
|
+
annotations?: Record<string, unknown>;
|
|
86
|
+
}): ActionManifestEntry {
|
|
87
|
+
return {
|
|
88
|
+
entry: options.entry,
|
|
89
|
+
description: options.description,
|
|
90
|
+
inputSchema: options.inputSchema,
|
|
91
|
+
outputSchema: options.outputSchema,
|
|
92
|
+
uses: options.uses || [],
|
|
93
|
+
tags: options.tags || [],
|
|
94
|
+
annotations: options.annotations,
|
|
95
|
+
};
|
|
96
|
+
}
|
package/src/project/types.ts
CHANGED
|
@@ -60,3 +60,33 @@ export interface PlaybookDefinition extends PlaybookFrontmatter {
|
|
|
60
60
|
/** Playbook 源文件的绝对物理路径 */
|
|
61
61
|
filePath: string;
|
|
62
62
|
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 单个 Action 在清单中的声明项。
|
|
66
|
+
*/
|
|
67
|
+
export interface ActionManifestEntry {
|
|
68
|
+
/** Action 入口文件相对路径(如 "actions/greet.ts") */
|
|
69
|
+
entry: string;
|
|
70
|
+
/** Action 功能描述 */
|
|
71
|
+
description?: string;
|
|
72
|
+
/** 输入参数模式规范 */
|
|
73
|
+
inputSchema?: Record<string, unknown> | boolean;
|
|
74
|
+
/** 输出结果模式规范 */
|
|
75
|
+
outputSchema?: Record<string, unknown> | boolean;
|
|
76
|
+
/** 静态依赖的 Action 列表 */
|
|
77
|
+
uses?: string[];
|
|
78
|
+
/** 标签列表 */
|
|
79
|
+
tags?: string[];
|
|
80
|
+
/** 协议注解元数据 */
|
|
81
|
+
annotations?: Record<string, unknown>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* ActionDock 声明式清单(actiondock.manifest.json)规范。
|
|
86
|
+
* 作为元数据的单一事实源,实现无副作用的模块发现与构建规划。
|
|
87
|
+
*/
|
|
88
|
+
export interface ActionDockManifest {
|
|
89
|
+
schemaVersion: number;
|
|
90
|
+
actions: Record<string, ActionManifestEntry>;
|
|
91
|
+
assets?: string[];
|
|
92
|
+
}
|