@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
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { ActionContract, ActionDefinition, ActionRef, ResolvedActionRef } from "@actiondock/sdk";
|
|
2
|
+
import type { ProjectConfig } from "../project/types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 注册的位置条目类型。
|
|
6
|
+
*/
|
|
7
|
+
export type LocationType = "package" | "workspace";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 位置注册表中的单条链接记录。
|
|
11
|
+
*/
|
|
12
|
+
export interface LocationLink {
|
|
13
|
+
/** 位置类型:单包或工作区 */
|
|
14
|
+
type: LocationType;
|
|
15
|
+
/** 目录绝对物理路径 */
|
|
16
|
+
path: string;
|
|
17
|
+
/** 注册时间(ISO 8601) */
|
|
18
|
+
linkedAt: string;
|
|
19
|
+
/** 扫描最大深度(工作区模式有效,默认 3) */
|
|
20
|
+
depth?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 物理位置注册表文件(~/.actiondock/registry.json)格式。
|
|
25
|
+
*/
|
|
26
|
+
export interface LocationRegistryData {
|
|
27
|
+
schemaVersion: 1;
|
|
28
|
+
links: LocationLink[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 解析后的包实例快照。
|
|
33
|
+
*/
|
|
34
|
+
export interface CatalogPackageEntry {
|
|
35
|
+
/** 逻辑包标识 */
|
|
36
|
+
id: string;
|
|
37
|
+
/** 包物理实例唯一标识 */
|
|
38
|
+
packageInstanceId: string;
|
|
39
|
+
/** 项目根目录绝对路径 */
|
|
40
|
+
projectRoot: string;
|
|
41
|
+
/** 项目配置 */
|
|
42
|
+
config: ProjectConfig;
|
|
43
|
+
/** 是否来自工作区自动发现 */
|
|
44
|
+
isWorkspaceChild?: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 运行时目录与包快照。
|
|
49
|
+
*/
|
|
50
|
+
export interface CatalogSnapshot {
|
|
51
|
+
/** 快照代次唯一标识 */
|
|
52
|
+
generationId: string;
|
|
53
|
+
/** 生成快照时间 */
|
|
54
|
+
createdAt: string;
|
|
55
|
+
/** 已发现的包集合(按 packageId 索引) */
|
|
56
|
+
packages: Map<string, CatalogPackageEntry>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 索引中的 Action 描述符。
|
|
61
|
+
*/
|
|
62
|
+
export interface IndexedAction {
|
|
63
|
+
/** 所属逻辑包 ID */
|
|
64
|
+
packageId: string;
|
|
65
|
+
/** 动作 ID */
|
|
66
|
+
actionId: string;
|
|
67
|
+
/** 契约元数据 */
|
|
68
|
+
contract: ActionContract;
|
|
69
|
+
/** 实现入口相对路径 */
|
|
70
|
+
entry: string;
|
|
71
|
+
/** 包根目录绝对路径 */
|
|
72
|
+
projectRoot: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 模块加载器接口。
|
|
77
|
+
*/
|
|
78
|
+
export interface ModuleLoader {
|
|
79
|
+
load<T>(file: string, options: {
|
|
80
|
+
projectRoot: string;
|
|
81
|
+
tsconfigPath?: string;
|
|
82
|
+
}): Promise<T>;
|
|
83
|
+
}
|
package/src/doctor/doctor.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
2
|
+
import { delimiter, join } from "node:path";
|
|
3
3
|
import { findProjectRoot, loadActions, loadPlaybooks, loadProjectConfig } from "../project/loader";
|
|
4
4
|
import { getRegistryStatus } from "../registry/registry";
|
|
5
5
|
import { createGlobalStorage, createStorage } from "../storage";
|
|
@@ -18,6 +18,36 @@ function compareSemver(v1: string, v2: string): number {
|
|
|
18
18
|
return 0;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
function findExecutable(command: string): string | null {
|
|
22
|
+
if (typeof (globalThis as any).Bun !== "undefined" && typeof (globalThis as any).Bun.which === "function") {
|
|
23
|
+
try {
|
|
24
|
+
const bPath = (globalThis as any).Bun.which(command);
|
|
25
|
+
if (bPath) return bPath;
|
|
26
|
+
} catch {}
|
|
27
|
+
}
|
|
28
|
+
const hasPathSep = command.includes("/") || command.includes("\\");
|
|
29
|
+
if (hasPathSep) {
|
|
30
|
+
return existsSync(command) ? command : null;
|
|
31
|
+
}
|
|
32
|
+
const pathEnv = process.env.PATH || "";
|
|
33
|
+
const dirs = pathEnv.split(delimiter);
|
|
34
|
+
const isWindows = process.platform === "win32";
|
|
35
|
+
const pathext = isWindows
|
|
36
|
+
? (process.env.PATHEXT || ".COM;.EXE;.BAT;.CMD").split(";")
|
|
37
|
+
: [""];
|
|
38
|
+
|
|
39
|
+
for (const dir of dirs) {
|
|
40
|
+
if (!dir) continue;
|
|
41
|
+
for (const ext of pathext) {
|
|
42
|
+
const candidate = join(dir, isWindows && !command.includes(".") ? command + ext : command);
|
|
43
|
+
if (existsSync(candidate)) {
|
|
44
|
+
return candidate;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
21
51
|
export async function runDoctorChecks(options?: {
|
|
22
52
|
cwd?: string;
|
|
23
53
|
packageIdOrPath?: string;
|
|
@@ -26,46 +56,42 @@ export async function runDoctorChecks(options?: {
|
|
|
26
56
|
const cwd = options?.cwd || process.cwd();
|
|
27
57
|
const checks: DoctorCheckItem[] = [];
|
|
28
58
|
|
|
29
|
-
// 1. Check
|
|
30
|
-
const
|
|
59
|
+
// 1. Check Node.js Runtime
|
|
60
|
+
const nodeVersion = process.versions.node;
|
|
61
|
+
if (nodeVersion) {
|
|
62
|
+
const isGte22 = compareSemver(nodeVersion, "22.12.0") >= 0;
|
|
63
|
+
checks.push({
|
|
64
|
+
id: "runtime.node",
|
|
65
|
+
category: "runtime",
|
|
66
|
+
name: "Node.js Runtime",
|
|
67
|
+
status: isGte22 ? "ok" : "warn",
|
|
68
|
+
message: `v${nodeVersion} (${isGte22 ? ">= 22.12.0 supported" : ">= 22.12.0 recommended"})`,
|
|
69
|
+
fix: isGte22 ? undefined : "Upgrade Node.js to v22.12.0 or higher",
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 2. Check Bun Runtime (Optional compiler for standalone binaries)
|
|
74
|
+
const bunVersion = (typeof (globalThis as any).Bun !== "undefined" && (globalThis as any).Bun.version) || (process.versions as any).bun;
|
|
31
75
|
if (bunVersion) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
message: `v${bunVersion} (>= 1.2.0 required)`,
|
|
40
|
-
});
|
|
41
|
-
} else {
|
|
42
|
-
checks.push({
|
|
43
|
-
id: "runtime.bun",
|
|
44
|
-
category: "runtime",
|
|
45
|
-
name: "Bun Runtime",
|
|
46
|
-
status: "error",
|
|
47
|
-
message: `v${bunVersion} is too old (>= 1.2.0 required)`,
|
|
48
|
-
fix: "Run 'bun upgrade' to update Bun",
|
|
49
|
-
});
|
|
50
|
-
}
|
|
76
|
+
checks.push({
|
|
77
|
+
id: "runtime.bun",
|
|
78
|
+
category: "runtime",
|
|
79
|
+
name: "Bun Runtime",
|
|
80
|
+
status: "ok",
|
|
81
|
+
message: `v${bunVersion} (available for standalone binary compilation)`,
|
|
82
|
+
});
|
|
51
83
|
} else {
|
|
52
84
|
checks.push({
|
|
53
85
|
id: "runtime.bun",
|
|
54
86
|
category: "runtime",
|
|
55
87
|
name: "Bun Runtime",
|
|
56
|
-
status: "
|
|
57
|
-
message: "Bun
|
|
58
|
-
fix: "Install Bun via 'npm install -g bun'",
|
|
88
|
+
status: "ok",
|
|
89
|
+
message: "Bun compiler not detected (optional, required only for 'ad build' standalone binaries)",
|
|
59
90
|
});
|
|
60
91
|
}
|
|
61
92
|
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
try {
|
|
65
|
-
adPath = typeof Bun !== "undefined" && Bun.which ? Bun.which("ad") : null;
|
|
66
|
-
} catch {
|
|
67
|
-
// ignore
|
|
68
|
-
}
|
|
93
|
+
// 3. Check CLI in PATH
|
|
94
|
+
const adPath = findExecutable("ad");
|
|
69
95
|
|
|
70
96
|
if (adPath) {
|
|
71
97
|
checks.push({
|
|
@@ -82,7 +108,7 @@ export async function runDoctorChecks(options?: {
|
|
|
82
108
|
name: "CLI Executable",
|
|
83
109
|
status: "warn",
|
|
84
110
|
message: "'ad' command not found in PATH",
|
|
85
|
-
fix: "Run 'npm install -g @actiondock/cli' or in SDK workspace run 'cd packages/cli &&
|
|
111
|
+
fix: "Run 'npm install -g @actiondock/cli' or in SDK workspace run 'cd packages/cli && npm link'",
|
|
86
112
|
});
|
|
87
113
|
}
|
|
88
114
|
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import type {
|
|
3
|
+
ActionDefinition,
|
|
4
|
+
ActionRef,
|
|
5
|
+
ExecutionEvent,
|
|
6
|
+
ExecutionResult,
|
|
7
|
+
JsonValue,
|
|
8
|
+
ProgressReporter,
|
|
9
|
+
RunRecord,
|
|
10
|
+
RunStatus,
|
|
11
|
+
} from "@actiondock/sdk";
|
|
12
|
+
import type { ProjectConfig } from "../project/types";
|
|
13
|
+
import { type EventSink, getDefaultEventSink } from "../runtime/events";
|
|
14
|
+
import { ActionRunner, type ExecutionHandle } from "../runtime/runner";
|
|
15
|
+
import type { RuntimeStorage } from "../storage/types";
|
|
16
|
+
import type {
|
|
17
|
+
CancelResult,
|
|
18
|
+
ExecuteOptions,
|
|
19
|
+
ExecutionService,
|
|
20
|
+
ExecutionTicket,
|
|
21
|
+
} from "./types";
|
|
22
|
+
|
|
23
|
+
export interface ExecutionServiceOptions {
|
|
24
|
+
packageId: string;
|
|
25
|
+
storage: RuntimeStorage;
|
|
26
|
+
projectConfig?: ProjectConfig;
|
|
27
|
+
eventSink?: EventSink;
|
|
28
|
+
maxActiveRuns?: number;
|
|
29
|
+
ownerId?: string;
|
|
30
|
+
actionResolver?: (ref: ActionRef) => ActionDefinition | undefined | Promise<ActionDefinition | undefined>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface ActiveRun {
|
|
34
|
+
runId: string;
|
|
35
|
+
handle: ExecutionHandle;
|
|
36
|
+
controller: AbortController;
|
|
37
|
+
status: RunStatus;
|
|
38
|
+
startedAt: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 统一执行协调服务实现。
|
|
43
|
+
*/
|
|
44
|
+
export class DefaultExecutionService implements ExecutionService {
|
|
45
|
+
private packageId: string;
|
|
46
|
+
private storage: RuntimeStorage;
|
|
47
|
+
private projectConfig?: ProjectConfig;
|
|
48
|
+
private eventSink: EventSink;
|
|
49
|
+
private maxActiveRuns: number;
|
|
50
|
+
private ownerId: string;
|
|
51
|
+
private runner: ActionRunner;
|
|
52
|
+
private actionResolver?: (ref: ActionRef) => ActionDefinition | undefined | Promise<ActionDefinition | undefined>;
|
|
53
|
+
private activeRuns = new Map<string, ActiveRun>();
|
|
54
|
+
private isClosing = false;
|
|
55
|
+
|
|
56
|
+
constructor(options: ExecutionServiceOptions) {
|
|
57
|
+
this.packageId = options.packageId;
|
|
58
|
+
this.storage = options.storage;
|
|
59
|
+
this.projectConfig = options.projectConfig;
|
|
60
|
+
this.eventSink = options.eventSink || getDefaultEventSink();
|
|
61
|
+
this.maxActiveRuns = options.maxActiveRuns || 32;
|
|
62
|
+
this.ownerId = options.ownerId || `host-${randomUUID().slice(0, 8)}`;
|
|
63
|
+
this.actionResolver = options.actionResolver;
|
|
64
|
+
|
|
65
|
+
this.runner = new ActionRunner({
|
|
66
|
+
packageId: this.packageId,
|
|
67
|
+
storage: this.storage,
|
|
68
|
+
projectConfig: this.projectConfig,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
public registerAction(action: ActionDefinition): void {
|
|
73
|
+
this.runner.registerAction(action);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private async resolveTargetAction(ref: ActionRef): Promise<ActionDefinition | undefined> {
|
|
77
|
+
const fromRunner = this.runner.getAction(ref.actionId);
|
|
78
|
+
if (fromRunner) return fromRunner;
|
|
79
|
+
if (this.actionResolver) {
|
|
80
|
+
return this.actionResolver(ref);
|
|
81
|
+
}
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async execute(
|
|
86
|
+
ref: ActionRef,
|
|
87
|
+
input: JsonValue,
|
|
88
|
+
options: ExecuteOptions = {}
|
|
89
|
+
): Promise<ExecutionResult> {
|
|
90
|
+
const ticket = await this.start(ref, input, options);
|
|
91
|
+
const active = this.activeRuns.get(ticket.runId);
|
|
92
|
+
if (!active) {
|
|
93
|
+
const record = await this.get(ticket.runId);
|
|
94
|
+
if (record && record.status === "success") {
|
|
95
|
+
return { ok: true, runId: ticket.runId, data: record.output ?? null };
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
ok: false,
|
|
99
|
+
runId: ticket.runId,
|
|
100
|
+
error: record?.error || {
|
|
101
|
+
code: "RUN_TERMINATED_EARLY",
|
|
102
|
+
message: `Run ${ticket.runId} terminated without result`,
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
return active.handle.result;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async start(
|
|
110
|
+
ref: ActionRef,
|
|
111
|
+
input: JsonValue,
|
|
112
|
+
options: ExecuteOptions = {}
|
|
113
|
+
): Promise<ExecutionTicket> {
|
|
114
|
+
if (this.isClosing) {
|
|
115
|
+
throw new Error("ExecutionService is closing: new tasks rejected");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (this.activeRuns.size >= this.maxActiveRuns) {
|
|
119
|
+
throw new Error(
|
|
120
|
+
`Concurrency limit reached: ${this.activeRuns.size}/${this.maxActiveRuns} active runs`
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const action = await this.resolveTargetAction(ref);
|
|
125
|
+
if (!action) {
|
|
126
|
+
const runId = randomUUID();
|
|
127
|
+
const errEvt: ExecutionEvent = {
|
|
128
|
+
runId,
|
|
129
|
+
rootRunId: runId,
|
|
130
|
+
sequence: 0,
|
|
131
|
+
timestamp: new Date().toISOString(),
|
|
132
|
+
type: "finish",
|
|
133
|
+
result: {
|
|
134
|
+
ok: false,
|
|
135
|
+
runId,
|
|
136
|
+
error: {
|
|
137
|
+
code: "ACTION_NOT_FOUND",
|
|
138
|
+
message: `Action '${ref.actionId}' not found in package '${ref.packageId || this.packageId}'`,
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
this.eventSink.emit(errEvt);
|
|
143
|
+
return {
|
|
144
|
+
runId,
|
|
145
|
+
status: "failed",
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const controller = new AbortController();
|
|
150
|
+
if (options.signal) {
|
|
151
|
+
if (options.signal.aborted) {
|
|
152
|
+
controller.abort(options.signal.reason);
|
|
153
|
+
} else {
|
|
154
|
+
options.signal.addEventListener(
|
|
155
|
+
"abort",
|
|
156
|
+
() => controller.abort(options.signal?.reason),
|
|
157
|
+
{ once: true }
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
let sequence = 0;
|
|
163
|
+
type EventPayload =
|
|
164
|
+
| { type: "log"; level: "debug" | "info" | "warn" | "error"; message: string; data?: JsonValue }
|
|
165
|
+
| { type: "progress"; current?: number; total?: number; message?: string }
|
|
166
|
+
| { type: "status"; status: RunStatus }
|
|
167
|
+
| { type: "finish"; result: ExecutionResult };
|
|
168
|
+
|
|
169
|
+
const handle = this.runner.start(action, input, {
|
|
170
|
+
signal: controller.signal,
|
|
171
|
+
timeoutMs: options.timeoutMs,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const emitEvent = (payload: EventPayload) => {
|
|
175
|
+
const evt: ExecutionEvent = {
|
|
176
|
+
...payload,
|
|
177
|
+
runId: handle.runId,
|
|
178
|
+
rootRunId: handle.runId,
|
|
179
|
+
sequence: sequence++,
|
|
180
|
+
timestamp: new Date().toISOString(),
|
|
181
|
+
};
|
|
182
|
+
this.eventSink.emit(evt);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const progressReporter: ProgressReporter = {
|
|
186
|
+
report(current: number, total?: number, message?: string) {
|
|
187
|
+
emitEvent({
|
|
188
|
+
type: "progress",
|
|
189
|
+
current,
|
|
190
|
+
total,
|
|
191
|
+
message,
|
|
192
|
+
});
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const activeItem: ActiveRun = {
|
|
197
|
+
runId: handle.runId,
|
|
198
|
+
handle,
|
|
199
|
+
controller,
|
|
200
|
+
status: "running",
|
|
201
|
+
startedAt: new Date().toISOString(),
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
this.activeRuns.set(handle.runId, activeItem);
|
|
205
|
+
emitEvent({ type: "status", status: "running" });
|
|
206
|
+
|
|
207
|
+
handle.result
|
|
208
|
+
.then((result: ExecutionResult) => {
|
|
209
|
+
activeItem.status = result.ok ? "success" : "failed";
|
|
210
|
+
emitEvent({ type: "finish", result });
|
|
211
|
+
})
|
|
212
|
+
.catch((err: any) => {
|
|
213
|
+
activeItem.status = "failed";
|
|
214
|
+
emitEvent({
|
|
215
|
+
type: "finish",
|
|
216
|
+
result: {
|
|
217
|
+
ok: false,
|
|
218
|
+
runId: handle.runId,
|
|
219
|
+
error: {
|
|
220
|
+
code: "UNHANDLED_EXECUTION_ERROR",
|
|
221
|
+
message: err?.message || String(err),
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
})
|
|
226
|
+
.finally(() => {
|
|
227
|
+
this.activeRuns.delete(handle.runId);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
runId: handle.runId,
|
|
232
|
+
status: "running",
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async get(runId: string): Promise<RunRecord | undefined> {
|
|
237
|
+
const record = this.storage.getRun(runId);
|
|
238
|
+
return record || undefined;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async cancel(runId: string, reason?: string): Promise<CancelResult> {
|
|
242
|
+
const active = this.activeRuns.get(runId);
|
|
243
|
+
if (!active) {
|
|
244
|
+
const record = await this.get(runId);
|
|
245
|
+
if (record) {
|
|
246
|
+
return { outcome: "already_terminal", runId, status: record.status };
|
|
247
|
+
}
|
|
248
|
+
return { outcome: "not_found", runId };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
active.controller.abort(new Error(reason || "Execution cancelled"));
|
|
252
|
+
active.handle.cancel(reason);
|
|
253
|
+
return { outcome: "requested", runId };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
events(
|
|
257
|
+
runId: string,
|
|
258
|
+
options: { after?: number; signal?: AbortSignal } = {}
|
|
259
|
+
): AsyncIterable<ExecutionEvent> {
|
|
260
|
+
return this.eventSink.subscribe(runId, options);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async close(options: { graceMs?: number } = {}): Promise<void> {
|
|
264
|
+
this.isClosing = true;
|
|
265
|
+
const graceMs = options.graceMs ?? 5000;
|
|
266
|
+
|
|
267
|
+
for (const [_, active] of this.activeRuns) {
|
|
268
|
+
active.controller.abort(new Error("Service shutting down"));
|
|
269
|
+
active.handle.cancel("Service shutting down");
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (this.activeRuns.size > 0) {
|
|
273
|
+
const waitPromise = Promise.all(
|
|
274
|
+
Array.from(this.activeRuns.values()).map((a) => a.handle.result.catch(() => {}))
|
|
275
|
+
);
|
|
276
|
+
const timeoutPromise = new Promise((resolve) => setTimeout(resolve, graceMs));
|
|
277
|
+
await Promise.race([waitPromise, timeoutPromise]);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
this.activeRuns.clear();
|
|
281
|
+
}
|
|
282
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ActionRef,
|
|
3
|
+
ExecutionEvent,
|
|
4
|
+
ExecutionResult,
|
|
5
|
+
JsonValue,
|
|
6
|
+
RunRecord,
|
|
7
|
+
RunStatus,
|
|
8
|
+
} from "@actiondock/sdk";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 执行参数选项。
|
|
12
|
+
*/
|
|
13
|
+
export interface ExecuteOptions {
|
|
14
|
+
/** 外部取消信号 */
|
|
15
|
+
signal?: AbortSignal;
|
|
16
|
+
/** 超时时间(毫秒) */
|
|
17
|
+
timeoutMs?: number;
|
|
18
|
+
/** 配置临时覆盖字典 */
|
|
19
|
+
config?: Record<string, JsonValue>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 异步任务执行票据。
|
|
24
|
+
*/
|
|
25
|
+
export interface ExecutionTicket {
|
|
26
|
+
/** 运行标识 */
|
|
27
|
+
runId: string;
|
|
28
|
+
/** 当前状态 */
|
|
29
|
+
status: RunStatus;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 取消操作结果枚举。
|
|
34
|
+
*/
|
|
35
|
+
export type CancelResult =
|
|
36
|
+
| { outcome: "requested"; runId: string }
|
|
37
|
+
| { outcome: "already_terminal"; runId: string; status: RunStatus }
|
|
38
|
+
| { outcome: "not_found"; runId: string }
|
|
39
|
+
| { outcome: "not_owner"; runId: string };
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 统一执行协调服务接口。
|
|
43
|
+
*/
|
|
44
|
+
export interface ExecutionService {
|
|
45
|
+
/** 同步执行 Action 并等待终态结果 */
|
|
46
|
+
execute(
|
|
47
|
+
ref: ActionRef,
|
|
48
|
+
input: JsonValue,
|
|
49
|
+
options?: ExecuteOptions
|
|
50
|
+
): Promise<ExecutionResult>;
|
|
51
|
+
|
|
52
|
+
/** 异步启动 Action 并立即返回任务票据 */
|
|
53
|
+
start(
|
|
54
|
+
ref: ActionRef,
|
|
55
|
+
input: JsonValue,
|
|
56
|
+
options?: ExecuteOptions
|
|
57
|
+
): Promise<ExecutionTicket>;
|
|
58
|
+
|
|
59
|
+
/** 根据 ID 获取运行记录 */
|
|
60
|
+
get(runId: string): Promise<RunRecord | undefined>;
|
|
61
|
+
|
|
62
|
+
/** 取消指定的在运行任务 */
|
|
63
|
+
cancel(runId: string, reason?: string): Promise<CancelResult>;
|
|
64
|
+
|
|
65
|
+
/** 订阅执行事件流 */
|
|
66
|
+
events(
|
|
67
|
+
runId: string,
|
|
68
|
+
options?: { after?: number; signal?: AbortSignal }
|
|
69
|
+
): AsyncIterable<ExecutionEvent>;
|
|
70
|
+
|
|
71
|
+
/** 优雅关闭服务并等待活跃任务收尾 */
|
|
72
|
+
close(options?: { graceMs?: number }): Promise<void>;
|
|
73
|
+
}
|
package/src/export/skill.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
1
2
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
3
|
import { basename, dirname, join, resolve } from "node:path";
|
|
3
4
|
import { buildProject } from "../build/builder";
|
|
@@ -319,17 +320,17 @@ export async function exportSkill(
|
|
|
319
320
|
if (options.archive) {
|
|
320
321
|
const zipName = `${skillFolderName}.zip`;
|
|
321
322
|
archivePath = join(dirname(skillDir), zipName);
|
|
322
|
-
const zipProc =
|
|
323
|
-
|
|
323
|
+
const zipProc = spawnSync(
|
|
324
|
+
"zip",
|
|
325
|
+
["-r", archivePath, basename(skillDir)],
|
|
324
326
|
{
|
|
325
327
|
cwd: dirname(skillDir),
|
|
326
|
-
|
|
327
|
-
stderr: "pipe",
|
|
328
|
+
stdio: "pipe",
|
|
328
329
|
}
|
|
329
330
|
);
|
|
330
|
-
if (zipProc.
|
|
331
|
+
if (zipProc.status !== 0) {
|
|
331
332
|
console.warn(
|
|
332
|
-
`[WARN] Failed to create zip archive: ${zipProc.stderr
|
|
333
|
+
`[WARN] Failed to create zip archive: ${zipProc.stderr?.toString() || "zip command failed"}`
|
|
333
334
|
);
|
|
334
335
|
archivePath = undefined;
|
|
335
336
|
}
|
package/src/export/templates.ts
CHANGED