@actiondock/sdk 2.0.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/README.md +121 -0
- package/package.json +41 -0
- package/src/action.ts +38 -0
- package/src/cli.ts +317 -0
- package/src/index.ts +31 -0
- package/src/test-runtime.ts +273 -0
- package/src/types.ts +204 -0
package/README.md
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# @actiondock/sdk
|
|
2
|
+
|
|
3
|
+
The lightweight, zero-dependency SDK for defining, orchestrating, and testing AI Agent Actions and Skills in ActionDock 2.0.
|
|
4
|
+
|
|
5
|
+
[](https://bun.sh/)
|
|
6
|
+
[](https://www.typescriptlang.org/)
|
|
7
|
+
[](https://opensource.org/licenses/Apache-2.0)
|
|
8
|
+
|
|
9
|
+
> **Runtime requirement**: [Bun](https://bun.sh/) >= 1.2.0 is required.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bun add @actiondock/sdk
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Quick Example
|
|
22
|
+
|
|
23
|
+
### 1. Define an Action (`actions/greet.ts`)
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { defineAction } from "@actiondock/sdk";
|
|
27
|
+
|
|
28
|
+
export default defineAction({
|
|
29
|
+
id: "sample.greet",
|
|
30
|
+
description: "Greet a user and track greeting count in persistent state",
|
|
31
|
+
|
|
32
|
+
inputSchema: {
|
|
33
|
+
type: "object",
|
|
34
|
+
properties: {
|
|
35
|
+
name: { type: "string", minLength: 1 },
|
|
36
|
+
},
|
|
37
|
+
required: ["name"],
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
async run(input: { name: string }, ctx) {
|
|
41
|
+
const greeting = ctx.config.get("GREETING_PREFIX", "Hello");
|
|
42
|
+
const count = ((await ctx.state.get<number>(`greet:${input.name}`)) || 0) + 1;
|
|
43
|
+
await ctx.state.set(`greet:${input.name}`, count);
|
|
44
|
+
ctx.log.info(`Greeted ${input.name} ${count} time(s)`);
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
message: `${greeting}, ${input.name}!`,
|
|
48
|
+
count,
|
|
49
|
+
};
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### 2. Test in Milliseconds (`tests/greet.test.ts`)
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import { describe, expect, it } from "bun:test";
|
|
58
|
+
import { createTestRuntime } from "@actiondock/sdk";
|
|
59
|
+
import greetAction from "../actions/greet";
|
|
60
|
+
|
|
61
|
+
describe("sample.greet Action", () => {
|
|
62
|
+
it("greets user and updates state counter", async () => {
|
|
63
|
+
const runtime = createTestRuntime({
|
|
64
|
+
config: { GREETING_PREFIX: "Welcome" },
|
|
65
|
+
state: { "greet:Alice": 2 },
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const result = await runtime.run(greetAction, { name: "Alice" });
|
|
69
|
+
expect(result.message).toBe("Welcome, Alice!");
|
|
70
|
+
expect(result.count).toBe(3);
|
|
71
|
+
expect(await runtime.state.get("greet:Alice")).toBe(3);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Core Capabilities
|
|
79
|
+
|
|
80
|
+
- **`ActionContext`**:
|
|
81
|
+
- `ctx.config`: 5-tier configuration resolution (`CLI > SQLite > Global > ENV > default > fallback`).
|
|
82
|
+
- `ctx.state`: Cross-action persistent state store with namespacing (`scope()`) and automatic TTL expiration.
|
|
83
|
+
- `ctx.actions`: Action-to-action invocation (`invoke()`) with recursion & cycle detection.
|
|
84
|
+
- `ctx.log`: Clean stderr-directed structured logging (keeping stdout clean for JSON envelopes).
|
|
85
|
+
- `ctx.signal`: Cooperative `AbortSignal` for graceful timeout and cancellation.
|
|
86
|
+
- **`execCli`**: Deadlock-safe, cross-platform synchronous CLI executor with Windows `.cmd` resolution, stdin streaming, timeout, and signal support.
|
|
87
|
+
- **`spawnDetached`**: Safe asynchronous launcher for daemon-spawning CLI tools (e.g., `agent-browser open`), preventing pipe EOF hangs via fire-and-forget stdio decoupling and polling probe.
|
|
88
|
+
- **`createTestRuntime`**: Fast, zero-dependency in-memory test harness for unit tests.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## API Summary
|
|
93
|
+
|
|
94
|
+
| Export | Type | Description |
|
|
95
|
+
|---|---|---|
|
|
96
|
+
| `defineAction(def)` | Function | Defines and defensively validates an Action |
|
|
97
|
+
| `createTestRuntime(opts)` | Function | Creates an in-memory test runner (`config`, `state`, `logger`, `run()`) |
|
|
98
|
+
| `execCli(cmd, args, opts)` | Function | Synchronous, deadlock-safe CLI execution utility |
|
|
99
|
+
| `spawnDetached(opts)` | Function | Safe asynchronous launcher & readiness prober for daemon-spawning CLI tools |
|
|
100
|
+
| `MemoryConfig` | Class | In-memory `Config` provider for unit tests |
|
|
101
|
+
| `MemoryStateStore` | Class | In-memory `StateStore` with TTL and namespace support |
|
|
102
|
+
| `MemoryLogger` | Class | In-memory `Logger` collecting log entries for test assertions |
|
|
103
|
+
| `ActionContext` | Interface | Runtime context provided to `action.run(input, ctx)` |
|
|
104
|
+
| `ActionDefinition` | Interface | Schema and execution contract for an Action |
|
|
105
|
+
| `ExecutionResult` | Type | Standard JSON envelope (`ok: true, data` or `ok: false, error`) |
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## 📖 Documentation
|
|
110
|
+
|
|
111
|
+
For detailed guides, design principles, and exhaustive API references, see the [ActionDock Documentation Center](https://github.com/team4u/actiondock#readme):
|
|
112
|
+
|
|
113
|
+
- [Action SDK API Reference](https://github.com/team4u/actiondock/blob/main/docs/reference/action-api.md)
|
|
114
|
+
- [ActionContext Concept](https://github.com/team4u/actiondock/blob/main/docs/concepts/action-context.md)
|
|
115
|
+
- [Testing & Verification Guide](https://github.com/team4u/actiondock/blob/main/docs/guides/testing.md)
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## License
|
|
120
|
+
|
|
121
|
+
[Apache-2.0](LICENSE) © team4u
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@actiondock/sdk",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "ActionDock SDK for defining and testing standalone AI Agent Actions",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.ts",
|
|
7
|
+
"module": "./src/index.ts",
|
|
8
|
+
"types": "./src/index.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./src/index.ts",
|
|
12
|
+
"types": "./src/index.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"src",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"engines": {
|
|
20
|
+
"bun": ">=1.2.0"
|
|
21
|
+
},
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public",
|
|
24
|
+
"registry": "https://registry.npmjs.org/"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"test": "bun test"
|
|
28
|
+
},
|
|
29
|
+
"keywords": ["actiondock", "agent", "actions", "skill", "ai", "mcp"],
|
|
30
|
+
"author": "team4u",
|
|
31
|
+
"license": "Apache-2.0",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/team4u/actiondock.git",
|
|
35
|
+
"directory": "packages/sdk"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://github.com/team4u/actiondock#readme",
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/team4u/actiondock/issues"
|
|
40
|
+
}
|
|
41
|
+
}
|
package/src/action.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ActionDefinition } from "./types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 辅助函数:声明并定义一个强类型的 Action 动作。
|
|
5
|
+
*
|
|
6
|
+
* 职责:
|
|
7
|
+
* 1. 提供 TypeScript 泛型推导支持(入参类型 `I` 与出参类型 `O`)。
|
|
8
|
+
* 2. 在声明期进行防御性基础结构校验,确保包含非空的字符串 `id` 和可执行的 `run` 函数。
|
|
9
|
+
*
|
|
10
|
+
* @param definition 包含 id, description, inputSchema, outputSchema, run 的 Action 定义对象
|
|
11
|
+
* @returns 经过校验的 ActionDefinition 原对象
|
|
12
|
+
* @throws {Error} 若 definition 不是对象、缺失 id 或缺失 run 函数
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* export default defineAction({
|
|
17
|
+
* id: "sample.greet",
|
|
18
|
+
* description: "向指定用户打招呼",
|
|
19
|
+
* async run(input: { name: string }, ctx) {
|
|
20
|
+
* return { message: `Hello, ${input.name}!` };
|
|
21
|
+
* }
|
|
22
|
+
* });
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export function defineAction<I = unknown, O = unknown>(
|
|
26
|
+
definition: ActionDefinition<I, O>
|
|
27
|
+
): ActionDefinition<I, O> {
|
|
28
|
+
if (!definition || typeof definition !== "object") {
|
|
29
|
+
throw new Error("Action definition must be an object");
|
|
30
|
+
}
|
|
31
|
+
if (!definition.id || typeof definition.id !== "string") {
|
|
32
|
+
throw new Error("Action definition must have a string 'id'");
|
|
33
|
+
}
|
|
34
|
+
if (typeof definition.run !== "function") {
|
|
35
|
+
throw new Error(`Action '${definition.id}' must have a 'run' function`);
|
|
36
|
+
}
|
|
37
|
+
return definition;
|
|
38
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI 执行选项配置。
|
|
3
|
+
*/
|
|
4
|
+
export interface ExecCliOptions {
|
|
5
|
+
/**
|
|
6
|
+
* 子进程工作目录,默认为当前工作目录 (process.cwd())。
|
|
7
|
+
*/
|
|
8
|
+
cwd?: string;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 自定义环境变量字典(合并到 process.env 之上)。
|
|
12
|
+
*/
|
|
13
|
+
env?: Record<string, string>;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 协作式取消信号(如 ActionContext 中的 ctx.signal)。
|
|
17
|
+
* 若信号在执行前或执行期间触发,将安全中断或拒绝执行。
|
|
18
|
+
*/
|
|
19
|
+
signal?: AbortSignal;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 单条命令超时时间(单位:毫秒)。
|
|
23
|
+
* 超时后将向子进程发送终止信号强制结束,并标记 timedOut 为 true。
|
|
24
|
+
*/
|
|
25
|
+
timeout?: number;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 写入子进程标准输入(stdin)的文本或原始二进制数据。
|
|
29
|
+
*/
|
|
30
|
+
input?: string | Uint8Array;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 输出文本的解码字符集(默认为 "utf-8",支持 "gbk" 等跨平台字符集)。
|
|
34
|
+
*/
|
|
35
|
+
encoding?: string;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 当命令执行失败(非零退出码或超时)时是否直接抛出 Error(默认为 false)。
|
|
39
|
+
* 设为 true 时,可省去手动 if (!res.ok) 校验。
|
|
40
|
+
*/
|
|
41
|
+
throwOnError?: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* CLI 执行结果结构体。
|
|
46
|
+
*/
|
|
47
|
+
export interface ExecCliResult {
|
|
48
|
+
/** 命令是否成功退出(即 exitCode === 0 且未发生超时或中断) */
|
|
49
|
+
ok: boolean;
|
|
50
|
+
/** 进程退出码(若未找到命令、超时或信号中断等异常时为 -1) */
|
|
51
|
+
exitCode: number;
|
|
52
|
+
/** 解码并去除首尾空白后的标准输出文本 */
|
|
53
|
+
stdout: string;
|
|
54
|
+
/** 解码并去除首尾空白后的标准错误文本 */
|
|
55
|
+
stderr: string;
|
|
56
|
+
/** 原始标准输出字节流(用于图片、音频、压缩包等二进制数据处理) */
|
|
57
|
+
raw: Uint8Array;
|
|
58
|
+
/** 是否因超时强制终止 */
|
|
59
|
+
timedOut?: boolean;
|
|
60
|
+
/** 命令执行总耗时(毫秒) */
|
|
61
|
+
durationMs: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 跨平台、防管道死锁的同步 CLI 命令调度器。
|
|
66
|
+
*
|
|
67
|
+
* 核心特性与设计原则:
|
|
68
|
+
* 1. **Windows .cmd 兼容**:自动通过 `Bun.which()` 解析 Windows 平台下的 `.cmd` / `.bat` / `.exe` 物理绝对路径;
|
|
69
|
+
* 2. **防管道死锁**:采用 `Bun.spawnSync` 同步排空(Drain)管道并关闭句柄,彻底避免无头浏览器/Node 子进程因句柄残留导致异步流挂起;
|
|
70
|
+
* 3. **超时与取消安全**:支持毫秒级 `timeout` 超时强杀与 `signal` (AbortSignal) 取消信号;
|
|
71
|
+
* 4. **标准输入与二进制支持**:支持 `input` 管道灌入与 `raw` 原始二进制字节流输出;
|
|
72
|
+
* 5. **耗时度量与编码支持**:自动统计 `durationMs`,支持自定义 `encoding`(如 Windows GBK/CP936 解码);
|
|
73
|
+
* 6. **灵活判定与快速抛错**:默认返回 `ok: false` 供业务层分支判定,亦可通过 `throwOnError: true` 自动抛错。
|
|
74
|
+
*
|
|
75
|
+
* @param command 可执行命令名称或路径(如 "git", "agent-browser", "docker", "jq")
|
|
76
|
+
* @param args 传递给命令的参数列表(默认为 [])
|
|
77
|
+
* @param options 运行选项(工作目录、环境变量、取消信号、超时、stdin、字符编码、抛错开关)
|
|
78
|
+
* @returns ExecCliResult 执行结果结构体
|
|
79
|
+
*/
|
|
80
|
+
export function execCli(
|
|
81
|
+
command: string,
|
|
82
|
+
args: string[] = [],
|
|
83
|
+
options: ExecCliOptions = {}
|
|
84
|
+
): ExecCliResult {
|
|
85
|
+
const startTime = performance.now();
|
|
86
|
+
|
|
87
|
+
// 1. 检查取消信号
|
|
88
|
+
if (options.signal?.aborted) {
|
|
89
|
+
const errRes: ExecCliResult = {
|
|
90
|
+
ok: false,
|
|
91
|
+
exitCode: -1,
|
|
92
|
+
stdout: "",
|
|
93
|
+
stderr: "Command aborted before execution by signal",
|
|
94
|
+
raw: new Uint8Array(0),
|
|
95
|
+
durationMs: 0,
|
|
96
|
+
};
|
|
97
|
+
if (options.throwOnError) {
|
|
98
|
+
throw new Error(errRes.stderr);
|
|
99
|
+
}
|
|
100
|
+
return errRes;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 2. 跨平台绝对路径解析(解决 Windows 下 npm 全局 .cmd shim 识别问题)
|
|
104
|
+
const hasPathSep = command.includes("/") || command.includes("\\");
|
|
105
|
+
const binPath = hasPathSep ? command : (Bun.which(command) || command);
|
|
106
|
+
|
|
107
|
+
if (!hasPathSep && !Bun.which(command)) {
|
|
108
|
+
const errRes: ExecCliResult = {
|
|
109
|
+
ok: false,
|
|
110
|
+
exitCode: -1,
|
|
111
|
+
stdout: "",
|
|
112
|
+
stderr: `Command '${command}' not found in PATH.`,
|
|
113
|
+
raw: new Uint8Array(0),
|
|
114
|
+
durationMs: Math.round(performance.now() - startTime),
|
|
115
|
+
};
|
|
116
|
+
if (options.throwOnError) {
|
|
117
|
+
throw new Error(errRes.stderr);
|
|
118
|
+
}
|
|
119
|
+
return errRes;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 3. 处理标准输入数据
|
|
123
|
+
let stdinOption: Uint8Array | "ignore" | undefined = "ignore";
|
|
124
|
+
if (options.input !== undefined) {
|
|
125
|
+
if (typeof options.input === "string") {
|
|
126
|
+
stdinOption = new TextEncoder().encode(options.input);
|
|
127
|
+
} else if (options.input instanceof Uint8Array) {
|
|
128
|
+
stdinOption = options.input;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
const proc = Bun.spawnSync([binPath, ...args], {
|
|
134
|
+
cwd: options.cwd || process.cwd(),
|
|
135
|
+
env: options.env ? { ...process.env, ...options.env } : process.env,
|
|
136
|
+
stdin: stdinOption,
|
|
137
|
+
stdout: "pipe",
|
|
138
|
+
stderr: "pipe",
|
|
139
|
+
timeout: options.timeout,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const durationMs = Math.round(performance.now() - startTime);
|
|
143
|
+
const timedOut = Boolean((proc as any).exitedDueToTimeout);
|
|
144
|
+
|
|
145
|
+
// 4. 自定义字符集解码
|
|
146
|
+
const decoder = new TextDecoder(options.encoding || "utf-8");
|
|
147
|
+
const rawStdout = proc.stdout ? new Uint8Array(proc.stdout) : new Uint8Array(0);
|
|
148
|
+
const rawStderr = proc.stderr ? new Uint8Array(proc.stderr) : new Uint8Array(0);
|
|
149
|
+
|
|
150
|
+
const stdout = rawStdout.length > 0 ? decoder.decode(rawStdout).trim() : "";
|
|
151
|
+
let stderr = rawStderr.length > 0 ? decoder.decode(rawStderr).trim() : "";
|
|
152
|
+
|
|
153
|
+
if (timedOut && !stderr) {
|
|
154
|
+
stderr = `Command '${command}' timed out after ${options.timeout}ms`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const exitCode = timedOut ? -1 : (proc.exitCode ?? (proc.success ? 0 : -1));
|
|
158
|
+
const ok = !timedOut && exitCode === 0;
|
|
159
|
+
|
|
160
|
+
const result: ExecCliResult = {
|
|
161
|
+
ok,
|
|
162
|
+
exitCode,
|
|
163
|
+
stdout,
|
|
164
|
+
stderr,
|
|
165
|
+
raw: rawStdout,
|
|
166
|
+
timedOut: timedOut || undefined,
|
|
167
|
+
durationMs,
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
if (options.throwOnError && !ok) {
|
|
171
|
+
throw new Error(stderr || `Command '${command}' failed with exit code ${exitCode}`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return result;
|
|
175
|
+
} catch (err: any) {
|
|
176
|
+
const durationMs = Math.round(performance.now() - startTime);
|
|
177
|
+
const errRes: ExecCliResult = {
|
|
178
|
+
ok: false,
|
|
179
|
+
exitCode: -1,
|
|
180
|
+
stdout: "",
|
|
181
|
+
stderr: err?.message || String(err),
|
|
182
|
+
raw: new Uint8Array(0),
|
|
183
|
+
durationMs,
|
|
184
|
+
};
|
|
185
|
+
if (options.throwOnError) {
|
|
186
|
+
throw err;
|
|
187
|
+
}
|
|
188
|
+
return errRes;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* 启动后台守护进程类 CLI 命令的选项配置。
|
|
194
|
+
*/
|
|
195
|
+
export interface SpawnDetachedOptions {
|
|
196
|
+
/**
|
|
197
|
+
* 可执行命令名称或路径(如 "agent-browser", "docker", "daemon")。
|
|
198
|
+
*/
|
|
199
|
+
command: string;
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* 传递给命令的参数列表(默认为 [])。
|
|
203
|
+
*/
|
|
204
|
+
args?: string[];
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* 就绪探测回调函数(返回 true 表示守护进程已就绪或命令副作用已生效)。
|
|
208
|
+
*/
|
|
209
|
+
probe: () => Promise<boolean> | boolean;
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* 轮询探测间隔时间(单位:毫秒,默认为 400ms)。
|
|
213
|
+
*/
|
|
214
|
+
intervalMs?: number;
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* 总超时时间(单位:毫秒,默认为 30000ms)。
|
|
218
|
+
*/
|
|
219
|
+
timeoutMs?: number;
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* 协作式取消信号(如 ActionContext 中的 ctx.signal)。
|
|
223
|
+
*/
|
|
224
|
+
signal?: AbortSignal;
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* 子进程工作目录,默认为当前工作目录 (process.cwd())。
|
|
228
|
+
*/
|
|
229
|
+
cwd?: string;
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* 自定义环境变量字典(合并到 process.env 之上)。
|
|
233
|
+
*/
|
|
234
|
+
env?: Record<string, string>;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* 安全执行“会拉起后台守护进程”的 CLI 命令(如 agent-browser open)。
|
|
239
|
+
*
|
|
240
|
+
* 核心三步工作流:
|
|
241
|
+
* 1. 异步 fire:stdio 全 ignore —— 确保后台 daemon 进程继承不到任何管道句柄,从根源杜绝管道 EOF 挂起;
|
|
242
|
+
* 2. 等 CLI 进程自身退出:错开冷启动窗口,避免探测命令并发拉起第二个 daemon 导致冲突;
|
|
243
|
+
* 3. 轮询探测就绪:执行轻量 probe 回调确认守护进程就绪或副作用生效。
|
|
244
|
+
*
|
|
245
|
+
* @param options 启动与探测配置项
|
|
246
|
+
* @returns Promise<boolean> 若在超时前 probe 返回 true 则返回 true;若超时仍未就绪则返回 false
|
|
247
|
+
*/
|
|
248
|
+
export async function spawnDetached(options: SpawnDetachedOptions): Promise<boolean> {
|
|
249
|
+
const { command, args = [], probe, signal, cwd, env } = options;
|
|
250
|
+
const intervalMs = options.intervalMs ?? 400;
|
|
251
|
+
const timeoutMs = options.timeoutMs ?? 30_000;
|
|
252
|
+
|
|
253
|
+
// 1. 检查取消信号
|
|
254
|
+
if (signal?.aborted) {
|
|
255
|
+
throw new Error("Command aborted before execution by signal");
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// 2. 跨平台绝对路径解析(解决 Windows 下 npm 全局 .cmd shim 识别问题)
|
|
259
|
+
const hasPathSep = command.includes("/") || command.includes("\\");
|
|
260
|
+
const binPath = hasPathSep ? command : (Bun.which(command) || command);
|
|
261
|
+
|
|
262
|
+
if (!hasPathSep && !Bun.which(command)) {
|
|
263
|
+
throw new Error(`Command '${command}' not found in PATH.`);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// 3. fire-and-forget:ignore 所有 stdio,unref 防止阻塞事件循环
|
|
267
|
+
const child = Bun.spawn([binPath, ...args], {
|
|
268
|
+
cwd: cwd || process.cwd(),
|
|
269
|
+
env: env ? { ...process.env, ...env } : process.env,
|
|
270
|
+
stdin: "ignore",
|
|
271
|
+
stdout: "ignore",
|
|
272
|
+
stderr: "ignore",
|
|
273
|
+
signal,
|
|
274
|
+
});
|
|
275
|
+
child.unref();
|
|
276
|
+
|
|
277
|
+
// 4. 等待 CLI 前端进程自身退出(错开冷启动窗口,避免 probe 并发拉起两个 daemon)
|
|
278
|
+
try {
|
|
279
|
+
await child.exited;
|
|
280
|
+
} catch (err) {
|
|
281
|
+
if (signal?.aborted) {
|
|
282
|
+
throw new Error("aborted");
|
|
283
|
+
}
|
|
284
|
+
throw err;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (signal?.aborted) {
|
|
288
|
+
throw new Error("aborted");
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// 5. 轮询探测就绪
|
|
292
|
+
const deadline = Date.now() + timeoutMs;
|
|
293
|
+
while (Date.now() < deadline) {
|
|
294
|
+
if (signal?.aborted) {
|
|
295
|
+
throw new Error("aborted");
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
try {
|
|
299
|
+
const ready = await probe();
|
|
300
|
+
if (ready) {
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
} catch (probeErr) {
|
|
304
|
+
if (signal?.aborted) {
|
|
305
|
+
throw new Error("aborted");
|
|
306
|
+
}
|
|
307
|
+
// probe 异常通常为守护进程未完全就绪时的暂时性错误,继续等待轮询
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const remaining = deadline - Date.now();
|
|
311
|
+
if (remaining <= 0) break;
|
|
312
|
+
const sleepTime = Math.min(intervalMs, remaining);
|
|
313
|
+
await new Promise((resolve) => setTimeout(resolve, sleepTime));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return false;
|
|
317
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export { defineAction } from "./action";
|
|
2
|
+
export {
|
|
3
|
+
execCli,
|
|
4
|
+
spawnDetached,
|
|
5
|
+
type ExecCliOptions,
|
|
6
|
+
type ExecCliResult,
|
|
7
|
+
type SpawnDetachedOptions,
|
|
8
|
+
} from "./cli";
|
|
9
|
+
export {
|
|
10
|
+
createTestRuntime,
|
|
11
|
+
MemoryConfig,
|
|
12
|
+
MemoryStateStore,
|
|
13
|
+
MemoryLogger,
|
|
14
|
+
type TestRuntime,
|
|
15
|
+
type TestRuntimeOptions,
|
|
16
|
+
} from "./test-runtime";
|
|
17
|
+
export type {
|
|
18
|
+
ActionContext,
|
|
19
|
+
ActionDefinition,
|
|
20
|
+
ActionInvoker,
|
|
21
|
+
Config,
|
|
22
|
+
ExecutionResult,
|
|
23
|
+
JsonSchema,
|
|
24
|
+
Logger,
|
|
25
|
+
RuntimeError,
|
|
26
|
+
RunRecord,
|
|
27
|
+
RunStatus,
|
|
28
|
+
StateStore,
|
|
29
|
+
} from "./types";
|
|
30
|
+
|
|
31
|
+
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ActionContext,
|
|
3
|
+
ActionDefinition,
|
|
4
|
+
ActionInvoker,
|
|
5
|
+
Config,
|
|
6
|
+
Logger,
|
|
7
|
+
StateStore,
|
|
8
|
+
} from "./types";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 创建内存测试运行时所需的初始化选项。
|
|
12
|
+
*/
|
|
13
|
+
export interface TestRuntimeOptions {
|
|
14
|
+
/** 初始注入的配置键值对映射 */
|
|
15
|
+
config?: Record<string, unknown>;
|
|
16
|
+
/** 初始注入的状态键值对映射 */
|
|
17
|
+
state?: Record<string, unknown>;
|
|
18
|
+
/** 自定义日志记录器(可选,默认创建 MemoryLogger) */
|
|
19
|
+
logger?: Logger;
|
|
20
|
+
/** 自定义取消信号(可选,默认使用未中断的 AbortSignal) */
|
|
21
|
+
signal?: AbortSignal;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 基于内存 Map 的只读/可写配置实现,专供单元测试使用。
|
|
26
|
+
*/
|
|
27
|
+
export class MemoryConfig implements Config {
|
|
28
|
+
private store: Map<string, unknown>;
|
|
29
|
+
|
|
30
|
+
constructor(initial: Record<string, unknown> = {}) {
|
|
31
|
+
this.store = new Map(Object.entries(initial));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
get<T = unknown>(key: string): T | undefined;
|
|
35
|
+
get<T = unknown>(key: string, defaultValue: T): T;
|
|
36
|
+
get<T = unknown>(key: string, defaultValue?: T): T | undefined {
|
|
37
|
+
if (this.store.has(key)) {
|
|
38
|
+
return this.store.get(key) as T;
|
|
39
|
+
}
|
|
40
|
+
return defaultValue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
has(key: string): boolean {
|
|
44
|
+
return this.store.has(key);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 在测试期间动态更新或插入配置值。
|
|
49
|
+
* @param key 配置键名
|
|
50
|
+
* @param value 配置值
|
|
51
|
+
*/
|
|
52
|
+
set(key: string, value: unknown): void {
|
|
53
|
+
this.store.set(key, value);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 内存状态条目结构体,包含数据值与可选的过期时间戳。
|
|
59
|
+
*/
|
|
60
|
+
export interface MemoryStateEntry {
|
|
61
|
+
value: unknown;
|
|
62
|
+
expiresAt?: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 基于内存 Map 的状态存储实现,支持命名空间隔离与 TTL 自动失效,专供单元测试使用。
|
|
67
|
+
*/
|
|
68
|
+
export class MemoryStateStore implements StateStore {
|
|
69
|
+
private store: Map<string, any>;
|
|
70
|
+
private namespace: string;
|
|
71
|
+
|
|
72
|
+
constructor(
|
|
73
|
+
store?: Map<string, any>,
|
|
74
|
+
namespace = ""
|
|
75
|
+
) {
|
|
76
|
+
this.store = store || new Map();
|
|
77
|
+
this.namespace = namespace;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private qualify(key: string): string {
|
|
81
|
+
return this.namespace ? `${this.namespace}:${key}` : key;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private extractEntry(raw: unknown): MemoryStateEntry {
|
|
85
|
+
if (
|
|
86
|
+
raw !== null &&
|
|
87
|
+
typeof raw === "object" &&
|
|
88
|
+
("__actiondock_entry__" in (raw as Record<string, unknown>) ||
|
|
89
|
+
"expiresAt" in (raw as Record<string, unknown>))
|
|
90
|
+
) {
|
|
91
|
+
return raw as MemoryStateEntry;
|
|
92
|
+
}
|
|
93
|
+
return { value: raw };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async get<T = unknown>(key: string): Promise<T | undefined> {
|
|
97
|
+
const qKey = this.qualify(key);
|
|
98
|
+
const raw = this.store.get(qKey);
|
|
99
|
+
if (raw === undefined) return undefined;
|
|
100
|
+
const entry = this.extractEntry(raw);
|
|
101
|
+
if (entry.expiresAt !== undefined && entry.expiresAt <= Date.now()) {
|
|
102
|
+
this.store.delete(qKey);
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
return (entry.value !== undefined ? structuredClone(entry.value) : undefined) as T;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async set<T = unknown>(
|
|
109
|
+
key: string,
|
|
110
|
+
value: T,
|
|
111
|
+
ttl?: number
|
|
112
|
+
): Promise<void> {
|
|
113
|
+
const qKey = this.qualify(key);
|
|
114
|
+
const expiresAt =
|
|
115
|
+
typeof ttl === "number" && ttl > 0 ? Date.now() + ttl * 1000 : undefined;
|
|
116
|
+
|
|
117
|
+
const entry: MemoryStateEntry = {
|
|
118
|
+
value: structuredClone(value),
|
|
119
|
+
expiresAt,
|
|
120
|
+
};
|
|
121
|
+
(entry as any).__actiondock_entry__ = true;
|
|
122
|
+
this.store.set(qKey, entry);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async delete(key: string): Promise<boolean> {
|
|
126
|
+
const qKey = this.qualify(key);
|
|
127
|
+
return this.store.delete(qKey);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async clear(prefix = ""): Promise<number> {
|
|
131
|
+
const keysToDelete = await this.keys(prefix);
|
|
132
|
+
let count = 0;
|
|
133
|
+
for (const k of keysToDelete) {
|
|
134
|
+
if (this.store.delete(this.qualify(k))) {
|
|
135
|
+
count++;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return count;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async keys(prefix = ""): Promise<string[]> {
|
|
142
|
+
const fullPrefix = this.qualify(prefix);
|
|
143
|
+
const now = Date.now();
|
|
144
|
+
const result: string[] = [];
|
|
145
|
+
for (const [k, raw] of this.store.entries()) {
|
|
146
|
+
if (k.startsWith(fullPrefix)) {
|
|
147
|
+
const entry = this.extractEntry(raw);
|
|
148
|
+
if (entry.expiresAt !== undefined && entry.expiresAt <= now) {
|
|
149
|
+
this.store.delete(k);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (this.namespace) {
|
|
153
|
+
result.push(k.slice(this.namespace.length + 1));
|
|
154
|
+
} else {
|
|
155
|
+
result.push(k);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return result;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
scope(namespace: string): StateStore {
|
|
163
|
+
const nextNs = this.namespace
|
|
164
|
+
? `${this.namespace}:${namespace}`
|
|
165
|
+
: namespace;
|
|
166
|
+
return new MemoryStateStore(this.store, nextNs);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* 内存日志记录器实现,将所有日志记录在数组中以便在测试断言中检索。
|
|
172
|
+
*/
|
|
173
|
+
export class MemoryLogger implements Logger {
|
|
174
|
+
public logs: Array<{ level: string; message: string; data?: unknown }> = [];
|
|
175
|
+
|
|
176
|
+
debug(message: string, data?: unknown): void {
|
|
177
|
+
this.logs.push({ level: "debug", message, data });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
info(message: string, data?: unknown): void {
|
|
181
|
+
this.logs.push({ level: "info", message, data });
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
warn(message: string, data?: unknown): void {
|
|
185
|
+
this.logs.push({ level: "warn", message, data });
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
error(message: string, data?: unknown): void {
|
|
189
|
+
this.logs.push({ level: "error", message, data });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* 测试运行时接口,提供对内存配置、状态和日志的直接访问及便捷的 Action 执行方法。
|
|
195
|
+
*/
|
|
196
|
+
export interface TestRuntime {
|
|
197
|
+
/** 内存配置实例 */
|
|
198
|
+
config: MemoryConfig;
|
|
199
|
+
/** 内存状态存储实例 */
|
|
200
|
+
state: MemoryStateStore;
|
|
201
|
+
/** 内存日志记录器 */
|
|
202
|
+
logger: MemoryLogger;
|
|
203
|
+
/**
|
|
204
|
+
* 执行指定的 Action 并返回最终输出结果
|
|
205
|
+
* @param action 目标 Action
|
|
206
|
+
* @param input 输入参数
|
|
207
|
+
*/
|
|
208
|
+
run<I, O>(action: ActionDefinition<I, O>, input: I): Promise<O>;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* 创建用于单元测试的轻量级内存测试运行时(TestRuntime)。
|
|
213
|
+
*
|
|
214
|
+
* 特点:
|
|
215
|
+
* 1. 零外部依赖:无需依赖 SQLite 或本地文件系统,即开即用。
|
|
216
|
+
* 2. 真实语义:完整支持状态持久化、TTL 过期、命名空间隔离、Action 相互调用与环路死锁检测。
|
|
217
|
+
*
|
|
218
|
+
* @param options 初始化选项(可选初始 config, state, logger, signal)
|
|
219
|
+
* @returns TestRuntime 实例
|
|
220
|
+
*
|
|
221
|
+
* @example
|
|
222
|
+
* ```ts
|
|
223
|
+
* const runtime = createTestRuntime({
|
|
224
|
+
* config: { API_KEY: "test_key" }
|
|
225
|
+
* });
|
|
226
|
+
* const result = await runtime.run(myAction, { foo: "bar" });
|
|
227
|
+
* expect(result.success).toBe(true);
|
|
228
|
+
* expect(await runtime.state.get("some_key")).toBe(1);
|
|
229
|
+
* ```
|
|
230
|
+
*/
|
|
231
|
+
export function createTestRuntime(options: TestRuntimeOptions = {}): TestRuntime {
|
|
232
|
+
const config = new MemoryConfig(options.config || {});
|
|
233
|
+
const memoryMap = new Map<string, unknown>(
|
|
234
|
+
Object.entries(options.state || {})
|
|
235
|
+
);
|
|
236
|
+
const state = new MemoryStateStore(memoryMap);
|
|
237
|
+
const logger = (options.logger as MemoryLogger) || new MemoryLogger();
|
|
238
|
+
const signal = options.signal ?? new AbortController().signal;
|
|
239
|
+
|
|
240
|
+
const callStack: string[] = [];
|
|
241
|
+
|
|
242
|
+
const invoker: ActionInvoker = {
|
|
243
|
+
async invoke<I, O>(action: ActionDefinition<I, O>, input: I): Promise<O> {
|
|
244
|
+
if (callStack.includes(action.id)) {
|
|
245
|
+
throw new Error(
|
|
246
|
+
`Cycle detected in action invocation: ${callStack.join(" -> ")} -> ${action.id}`
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
callStack.push(action.id);
|
|
250
|
+
try {
|
|
251
|
+
const ctx: ActionContext = {
|
|
252
|
+
config,
|
|
253
|
+
state,
|
|
254
|
+
actions: invoker,
|
|
255
|
+
log: logger,
|
|
256
|
+
signal,
|
|
257
|
+
};
|
|
258
|
+
return await action.run(input, ctx);
|
|
259
|
+
} finally {
|
|
260
|
+
callStack.pop();
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
config,
|
|
267
|
+
state,
|
|
268
|
+
logger,
|
|
269
|
+
async run<I, O>(action: ActionDefinition<I, O>, input: I): Promise<O> {
|
|
270
|
+
return invoker.invoke(action, input);
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 标准 JSON Schema 结构定义。
|
|
3
|
+
* 支持对象模式(Object Schema)或布尔模式(Boolean Schema)。
|
|
4
|
+
*/
|
|
5
|
+
export type JsonSchema = Record<string, unknown> | boolean;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* ActionDock 标准运行时错误对象。
|
|
9
|
+
*/
|
|
10
|
+
export interface RuntimeError {
|
|
11
|
+
/** 机器可读的唯一错误码,例如 ACTION_NOT_FOUND, INPUT_VALIDATION_FAILED, ACTION_TIMEOUT 等 */
|
|
12
|
+
code: string;
|
|
13
|
+
/** 人类可读的错误描述信息 */
|
|
14
|
+
message: string;
|
|
15
|
+
/** 结构化的附加错误详情(如 JSON Schema 校验失败的具体字段列表) */
|
|
16
|
+
details?: unknown;
|
|
17
|
+
/** 导致此错误的底层原始异常或原因 */
|
|
18
|
+
cause?: unknown;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 标准执行结果信封(JSON Envelope)。
|
|
23
|
+
* ActionDock 在 CLI、独立二进制、HTTP Runner 和 MCP 等所有场景中均输出该格式。
|
|
24
|
+
*/
|
|
25
|
+
export type ExecutionResult<T = unknown> =
|
|
26
|
+
| {
|
|
27
|
+
/** 执行是否成功 */
|
|
28
|
+
ok: true;
|
|
29
|
+
/** 本次执行的全局唯一运行 ID(UUIDv4) */
|
|
30
|
+
runId: string;
|
|
31
|
+
/** Action 执行返回的业务数据 */
|
|
32
|
+
data: T;
|
|
33
|
+
}
|
|
34
|
+
| {
|
|
35
|
+
/** 执行是否失败 */
|
|
36
|
+
ok: false;
|
|
37
|
+
/** 本次执行的全局唯一运行 ID(UUIDv4) */
|
|
38
|
+
runId: string;
|
|
39
|
+
/** 运行时错误详情 */
|
|
40
|
+
error: RuntimeError;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 配置提供器接口(只读查询)。
|
|
45
|
+
* 遵循 5 层优先级链:CLI 参数 > 本地 SQLite > 全局 SQLite > 环境变量 > 项目默认值 > 回退默认值。
|
|
46
|
+
*/
|
|
47
|
+
export interface Config {
|
|
48
|
+
/**
|
|
49
|
+
* 获取指定键的配置值,未设置时返回 undefined
|
|
50
|
+
* @param key 配置键名
|
|
51
|
+
*/
|
|
52
|
+
get<T = unknown>(key: string): T | undefined;
|
|
53
|
+
/**
|
|
54
|
+
* 获取指定键的配置值,未设置时返回提供的默认值
|
|
55
|
+
* @param key 配置键名
|
|
56
|
+
* @param defaultValue 默认回退值
|
|
57
|
+
*/
|
|
58
|
+
get<T = unknown>(key: string, defaultValue: T): T;
|
|
59
|
+
/**
|
|
60
|
+
* 检查指定键是否存在配置值(无论来源于哪一层优先级)
|
|
61
|
+
* @param key 配置键名
|
|
62
|
+
*/
|
|
63
|
+
has(key: string): boolean;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 共享状态持久化存储接口。
|
|
68
|
+
* 提供跨 Action 调用的数据共享与持久化存储能力,支持命名空间与基于秒的 TTL 自动过期机制。
|
|
69
|
+
*/
|
|
70
|
+
export interface StateStore {
|
|
71
|
+
/**
|
|
72
|
+
* 读取指定键的状态值。若键已过期则返回 undefined
|
|
73
|
+
* @param key 状态键名
|
|
74
|
+
*/
|
|
75
|
+
get<T = unknown>(key: string): Promise<T | undefined>;
|
|
76
|
+
/**
|
|
77
|
+
* 设置状态键值对,可选指定过期存活时间(TTL)
|
|
78
|
+
* @param key 状态键名
|
|
79
|
+
* @param value 要存储的数据值(会自动进行深拷贝/序列化)
|
|
80
|
+
* @param ttl 存活时间(单位:秒)。不传或 <= 0 表示永久有效
|
|
81
|
+
*/
|
|
82
|
+
set<T = unknown>(key: string, value: T, ttl?: number): Promise<void>;
|
|
83
|
+
/**
|
|
84
|
+
* 删除指定键的状态数据。
|
|
85
|
+
* @param key 状态键名
|
|
86
|
+
* @returns 是否实际删除了数据(true: 存在并删除,false: 不存在)
|
|
87
|
+
*/
|
|
88
|
+
delete(key: string): Promise<boolean>;
|
|
89
|
+
/**
|
|
90
|
+
* 清空当前命名空间(或指定前缀)下的所有状态数据
|
|
91
|
+
* @param prefix 可选的键名前缀过滤条件
|
|
92
|
+
* @returns 实际清除的条目数量
|
|
93
|
+
*/
|
|
94
|
+
clear(prefix?: string): Promise<number>;
|
|
95
|
+
/**
|
|
96
|
+
* 列出当前命名空间下所有匹配前缀的状态键名(已自动过滤已过期的键)
|
|
97
|
+
* @param prefix 键名前缀过滤条件
|
|
98
|
+
*/
|
|
99
|
+
keys(prefix?: string): Promise<string[]>;
|
|
100
|
+
/**
|
|
101
|
+
* 创建一个具有独立命名空间隔离的子 StateStore 实例
|
|
102
|
+
* @param namespace 命名空间标识
|
|
103
|
+
*/
|
|
104
|
+
scope(namespace: string): StateStore;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 结构化日志记录器接口。
|
|
109
|
+
* 所有日志输出均定向到 stderr,确保不污染 stdout 中的 JSON 信封。
|
|
110
|
+
*/
|
|
111
|
+
export interface Logger {
|
|
112
|
+
/** 记录调试级别日志 */
|
|
113
|
+
debug(message: string, data?: unknown): void;
|
|
114
|
+
/** 记录信息级别日志 */
|
|
115
|
+
info(message: string, data?: unknown): void;
|
|
116
|
+
/** 记录警告级别日志 */
|
|
117
|
+
warn(message: string, data?: unknown): void;
|
|
118
|
+
/** 记录错误级别日志 */
|
|
119
|
+
error(message: string, data?: unknown): void;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Action 间相互调用的执行器接口。
|
|
124
|
+
* 支持在 Action 内部安全调用同 Package 或其他 Action,内置调用栈防死循环环路检测。
|
|
125
|
+
*/
|
|
126
|
+
export interface ActionInvoker {
|
|
127
|
+
/**
|
|
128
|
+
* 调用指定的 Action 并传入参数,返回其执行结果
|
|
129
|
+
* @param action 目标 Action 定义对象
|
|
130
|
+
* @param input 传递给目标 Action 的输入参数
|
|
131
|
+
*/
|
|
132
|
+
invoke<I, O>(
|
|
133
|
+
action: ActionDefinition<I, O>,
|
|
134
|
+
input: I
|
|
135
|
+
): Promise<O>;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 传递给 Action `run` 处理函数的运行时上下文对象。
|
|
140
|
+
*/
|
|
141
|
+
export interface ActionContext {
|
|
142
|
+
/** 配置读取接口,自动按多层优先级解析配置 */
|
|
143
|
+
config: Config;
|
|
144
|
+
/** 状态持久化存储接口,提供跨 Action 调用的数据存取与 TTL */
|
|
145
|
+
state: StateStore;
|
|
146
|
+
/** Action 相互调用接口,支持模块化组合与复用 */
|
|
147
|
+
actions: ActionInvoker;
|
|
148
|
+
/** 结构化日志接口,输出定向至 stderr */
|
|
149
|
+
log: Logger;
|
|
150
|
+
/** 取消信号,用于感知客户端中断、超时或 SIGINT */
|
|
151
|
+
signal: AbortSignal;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Action 动作定义契约。
|
|
156
|
+
* 通过 `defineAction({...})` 声明。
|
|
157
|
+
*/
|
|
158
|
+
export interface ActionDefinition<I = unknown, O = unknown> {
|
|
159
|
+
/** Action 唯一标识符(例如: "github.get-pr" 或 "sample.greet") */
|
|
160
|
+
id: string;
|
|
161
|
+
/** Action 功能描述,用于 CLI 帮助文档、Agent 发现以及 MCP Tool 描述 */
|
|
162
|
+
description?: string;
|
|
163
|
+
/** 输入参数的 JSON Schema 校验规范 */
|
|
164
|
+
inputSchema?: JsonSchema;
|
|
165
|
+
/** 输出结果的 JSON Schema 校验规范 */
|
|
166
|
+
outputSchema?: JsonSchema;
|
|
167
|
+
/**
|
|
168
|
+
* Action 的核心业务执行函数
|
|
169
|
+
* @param input 符合 inputSchema 契约的输入数据
|
|
170
|
+
* @param ctx 运行时上下文对象(包含 config, state, actions, log, signal)
|
|
171
|
+
*/
|
|
172
|
+
run(input: I, ctx: ActionContext): Promise<O> | O;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* 运行记录状态枚举。
|
|
177
|
+
*/
|
|
178
|
+
export type RunStatus = "running" | "success" | "failed" | "cancelled";
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Action 执行运行历史记录。
|
|
182
|
+
*/
|
|
183
|
+
export interface RunRecord {
|
|
184
|
+
/** 全局唯一运行 ID (UUIDv4) */
|
|
185
|
+
id: string;
|
|
186
|
+
/** 所属 Action Package 的唯一 ID */
|
|
187
|
+
packageId: string;
|
|
188
|
+
/** 所执行的 Action ID */
|
|
189
|
+
actionId: string;
|
|
190
|
+
/** 父级调用的运行 ID(若由其他 Action 嵌套调用触发) */
|
|
191
|
+
parentRunId?: string;
|
|
192
|
+
/** 运行生命周期状态 */
|
|
193
|
+
status: RunStatus;
|
|
194
|
+
/** 输入参数快照 */
|
|
195
|
+
input: unknown;
|
|
196
|
+
/** 执行成功时的输出结果 */
|
|
197
|
+
output?: unknown;
|
|
198
|
+
/** 执行失败时的错误信息 */
|
|
199
|
+
error?: RuntimeError;
|
|
200
|
+
/** 开始执行时间(ISO 8601 格式) */
|
|
201
|
+
startedAt: string;
|
|
202
|
+
/** 结束执行时间(ISO 8601 格式,进行中时为 undefined) */
|
|
203
|
+
finishedAt?: string;
|
|
204
|
+
}
|