@actiondock/core 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.
Files changed (43) hide show
  1. package/README.md +50 -0
  2. package/package.json +51 -0
  3. package/src/build/builder.ts +205 -0
  4. package/src/build/index.ts +2 -0
  5. package/src/build/templates.ts +59 -0
  6. package/src/doctor/doctor.ts +332 -0
  7. package/src/doctor/index.ts +2 -0
  8. package/src/doctor/types.ts +25 -0
  9. package/src/export/index.ts +2 -0
  10. package/src/export/skill.ts +349 -0
  11. package/src/export/templates.ts +258 -0
  12. package/src/filter/index.ts +1 -0
  13. package/src/filter/intent.ts +154 -0
  14. package/src/index.ts +13 -0
  15. package/src/profile/client.ts +302 -0
  16. package/src/profile/index.ts +3 -0
  17. package/src/profile/manager.ts +341 -0
  18. package/src/profile/types.ts +71 -0
  19. package/src/project/index.ts +3 -0
  20. package/src/project/init.ts +194 -0
  21. package/src/project/loader.ts +382 -0
  22. package/src/project/types.ts +62 -0
  23. package/src/registry/index.ts +2 -0
  24. package/src/registry/registry.ts +703 -0
  25. package/src/registry/types.ts +127 -0
  26. package/src/runtime/context.ts +232 -0
  27. package/src/runtime/env.ts +172 -0
  28. package/src/runtime/execution-manager.ts +74 -0
  29. package/src/runtime/index.ts +5 -0
  30. package/src/runtime/runner.ts +368 -0
  31. package/src/runtime/standalone.ts +429 -0
  32. package/src/schema/validator.ts +61 -0
  33. package/src/server/body.ts +112 -0
  34. package/src/server/index.ts +6 -0
  35. package/src/server/runtime-registry.ts +80 -0
  36. package/src/server/security.ts +115 -0
  37. package/src/server/server.ts +572 -0
  38. package/src/server/types.ts +42 -0
  39. package/src/storage/index.ts +64 -0
  40. package/src/storage/mask.ts +34 -0
  41. package/src/storage/sqlite.ts +578 -0
  42. package/src/storage/types.ts +111 -0
  43. package/src/utils/index.ts +60 -0
@@ -0,0 +1,154 @@
1
+ /**
2
+ * 从对象中提取待过滤字段的提取器函数类型。
3
+ */
4
+ export type Extractor<T> = (item: T) => unknown;
5
+
6
+ /**
7
+ * 安全地将模式字符串、数组或现有正则编译为不区分大小写的 RegExp 正则表达式。
8
+ * 若正则语法不合法,自动对特殊字符进行转义并降级为字面量匹配正则,保证零 Crash。
9
+ *
10
+ * @param intent 意图关键词、正则或数组
11
+ */
12
+ export function compileIntentRegex(
13
+ intent?: string | string[] | RegExp | null
14
+ ): RegExp | null {
15
+ if (!intent) return null;
16
+ if (intent instanceof RegExp) return intent;
17
+
18
+ let patternStr: string;
19
+ if (Array.isArray(intent)) {
20
+ const valid = intent.map((s) => s.trim()).filter(Boolean);
21
+ if (valid.length === 0) return null;
22
+ patternStr = valid.join("|");
23
+ } else {
24
+ patternStr = intent.trim();
25
+ if (!patternStr) return null;
26
+ }
27
+
28
+ try {
29
+ return new RegExp(patternStr, "i");
30
+ } catch {
31
+ const escaped = patternStr.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
32
+ return new RegExp(escaped, "i");
33
+ }
34
+ }
35
+
36
+ /**
37
+ * 递归检查某个值(包括嵌套的数组或对象结构)是否匹配给定的正则表达式。
38
+ *
39
+ * @param value 待检测的任意数据类型
40
+ * @param regex 正则表达式
41
+ */
42
+ export function matchIntent(value: unknown, regex: RegExp): boolean {
43
+ if (value === undefined || value === null) return false;
44
+
45
+ if (typeof value === "string") {
46
+ return regex.test(value);
47
+ }
48
+
49
+ if (typeof value === "number" || typeof value === "boolean") {
50
+ return regex.test(String(value));
51
+ }
52
+
53
+ if (Array.isArray(value)) {
54
+ for (const elem of value) {
55
+ if (matchIntent(elem, regex)) return true;
56
+ }
57
+ return false;
58
+ }
59
+
60
+ if (typeof value === "object") {
61
+ try {
62
+ return regex.test(JSON.stringify(value));
63
+ } catch {
64
+ return false;
65
+ }
66
+ }
67
+
68
+ return false;
69
+ }
70
+
71
+ export interface FilterResult<T> {
72
+ items: T[];
73
+ isFallback: boolean;
74
+ matchedCount: number;
75
+ }
76
+
77
+ /**
78
+ * Filters a collection of items based on an intent pattern across specified extractor functions.
79
+ * Returns detailed result including whether a fallback was triggered when 0 items matched.
80
+ */
81
+ export function filterWithFallbackInfo<T>(
82
+ items: T[],
83
+ intent?: string | string[] | RegExp | null,
84
+ extractors?: Extractor<T>[],
85
+ fallback = true
86
+ ): FilterResult<T> {
87
+ if (!intent) {
88
+ return {
89
+ items,
90
+ isFallback: false,
91
+ matchedCount: items.length,
92
+ };
93
+ }
94
+
95
+ const regex = compileIntentRegex(intent);
96
+ if (!regex) {
97
+ return {
98
+ items,
99
+ isFallback: false,
100
+ matchedCount: items.length,
101
+ };
102
+ }
103
+
104
+ const defaultExtractors: Extractor<T>[] = [
105
+ (item: any) =>
106
+ typeof item === "string"
107
+ ? item
108
+ : item?.id || item?.name || item?.key || String(item),
109
+ ];
110
+
111
+ const effectiveExtractors =
112
+ extractors && extractors.length > 0 ? extractors : defaultExtractors;
113
+
114
+ const matched = items.filter((item) => {
115
+ for (const extractor of effectiveExtractors) {
116
+ try {
117
+ const val = extractor(item);
118
+ if (matchIntent(val, regex)) {
119
+ return true;
120
+ }
121
+ } catch {
122
+ // Ignore extraction error
123
+ }
124
+ }
125
+ return false;
126
+ });
127
+
128
+ if (matched.length === 0 && fallback) {
129
+ return {
130
+ items,
131
+ isFallback: true,
132
+ matchedCount: 0,
133
+ };
134
+ }
135
+
136
+ return {
137
+ items: matched,
138
+ isFallback: false,
139
+ matchedCount: matched.length,
140
+ };
141
+ }
142
+
143
+ /**
144
+ * Filters a collection of items based on an intent pattern across specified extractor functions.
145
+ * When fallback is enabled (default), returns the full list if no items match the intent.
146
+ */
147
+ export function filterByIntent<T>(
148
+ items: T[],
149
+ intent?: string | string[] | RegExp | null,
150
+ extractors?: Extractor<T>[],
151
+ fallback = true
152
+ ): T[] {
153
+ return filterWithFallbackInfo(items, intent, extractors, fallback).items;
154
+ }
package/src/index.ts ADDED
@@ -0,0 +1,13 @@
1
+ export * from "./build";
2
+ export * from "./doctor";
3
+ export * from "./export";
4
+ export * from "./filter";
5
+ export * from "./profile";
6
+ export * from "./project";
7
+ export * from "./registry";
8
+ export * from "./runtime";
9
+ export * from "./schema/validator";
10
+ export * from "./server";
11
+ export * from "./storage";
12
+ export * from "./utils";
13
+
@@ -0,0 +1,302 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import type { ExecutionResult, RuntimeError, RunRecord } from "@actiondock/sdk";
3
+ import { normalizeServerUrl } from "./manager";
4
+ import type { RemoteHealthResult } from "./types";
5
+
6
+ function buildHeaders(token?: string): Record<string, string> {
7
+ const headers: Record<string, string> = {
8
+ Accept: "application/json",
9
+ };
10
+ if (token && token.trim()) {
11
+ headers.Authorization = `Bearer ${token.trim()}`;
12
+ }
13
+ return headers;
14
+ }
15
+
16
+ /**
17
+ * 调用远端 ActionDock 服务端执行 Action 时的选项参数。
18
+ */
19
+ export interface RemoteExecuteOptions {
20
+ /** 动态配置覆盖 */
21
+ configOverrides?: Record<string, unknown>;
22
+ /** 鉴权 Bearer Token */
23
+ token?: string;
24
+ /** 超时毫秒数 */
25
+ timeoutMs?: number;
26
+ /** 中断信号 */
27
+ signal?: AbortSignal;
28
+ /** 是否异步触发(202 Accepted 立即返回 runId) */
29
+ async?: boolean;
30
+ }
31
+
32
+ /**
33
+ * 远端 Action 执行结果信封对象。
34
+ */
35
+ export type RemoteExecutionResult<T = unknown> = ExecutionResult<T> & {
36
+ status?: string;
37
+ };
38
+
39
+ /**
40
+ * 探测指定远端 ActionDock 服务的健康状态与网络延迟。
41
+ *
42
+ * @param serverUrl 目标服务端地址
43
+ * @param token 鉴权 Token(可选)
44
+ * @param timeoutMs 探测超时时间(默认 5000ms)
45
+ */
46
+ export async function checkRemoteHealth(
47
+ serverUrl: string,
48
+ token?: string,
49
+ timeoutMs: number = 5000
50
+ ): Promise<RemoteHealthResult> {
51
+ const base = normalizeServerUrl(serverUrl);
52
+ const url = `${base}/api/v1/health`;
53
+ const startTime = Date.now();
54
+
55
+ try {
56
+ const controller = new AbortController();
57
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
58
+
59
+ const res = await fetch(url, {
60
+ method: "GET",
61
+ headers: buildHeaders(token),
62
+ signal: controller.signal,
63
+ });
64
+ clearTimeout(timer);
65
+
66
+ const latencyMs = Date.now() - startTime;
67
+
68
+ if (!res.ok) {
69
+ const text = await res.text().catch(() => "");
70
+ return {
71
+ ok: false,
72
+ latencyMs,
73
+ error: `Server responded with status ${res.status}: ${text || res.statusText}`,
74
+ };
75
+ }
76
+
77
+ const data = (await res.json().catch(() => ({}))) as any;
78
+ return {
79
+ ok: true,
80
+ status: data.status || "ok",
81
+ version: data.version,
82
+ uptime: data.uptime,
83
+ latencyMs,
84
+ };
85
+ } catch (err: any) {
86
+ const latencyMs = Date.now() - startTime;
87
+ return {
88
+ ok: false,
89
+ latencyMs,
90
+ error: err.name === "AbortError" ? "Connection timed out" : err.message,
91
+ };
92
+ }
93
+ }
94
+
95
+ export async function executeRemoteAction<T = unknown>(
96
+ serverUrl: string,
97
+ actionId: string,
98
+ input: unknown = {},
99
+ configOverridesOrOptions?: Record<string, unknown> | RemoteExecuteOptions,
100
+ tokenArg?: string
101
+ ): Promise<RemoteExecutionResult<T>> {
102
+ const base = normalizeServerUrl(serverUrl);
103
+ const url = `${base}/api/v1/actions/${encodeURIComponent(actionId)}/run`;
104
+
105
+ // Parse options / backwards compatibility
106
+ let configOverrides: Record<string, unknown> | undefined;
107
+ let token: string | undefined = tokenArg;
108
+ let timeoutMs: number | undefined;
109
+ let signal: AbortSignal | undefined;
110
+ let isAsync = false;
111
+
112
+ if (configOverridesOrOptions && typeof configOverridesOrOptions === "object") {
113
+ if (
114
+ "token" in configOverridesOrOptions ||
115
+ "timeoutMs" in configOverridesOrOptions ||
116
+ "signal" in configOverridesOrOptions ||
117
+ "async" in configOverridesOrOptions ||
118
+ "configOverrides" in configOverridesOrOptions
119
+ ) {
120
+ const opts = configOverridesOrOptions as RemoteExecuteOptions;
121
+ configOverrides = opts.configOverrides;
122
+ token = opts.token ?? tokenArg;
123
+ timeoutMs = opts.timeoutMs;
124
+ signal = opts.signal;
125
+ isAsync = Boolean(opts.async);
126
+ } else {
127
+ configOverrides = configOverridesOrOptions as Record<string, unknown>;
128
+ }
129
+ }
130
+
131
+ const executionPayload: Record<string, unknown> = {};
132
+ if (isAsync) {
133
+ executionPayload.mode = "async";
134
+ }
135
+ if (typeof timeoutMs === "number" && timeoutMs > 0) {
136
+ executionPayload.timeoutMs = timeoutMs;
137
+ }
138
+
139
+ try {
140
+ const headers = {
141
+ ...buildHeaders(token),
142
+ "Content-Type": "application/json",
143
+ };
144
+
145
+ const res = await fetch(url, {
146
+ method: "POST",
147
+ headers,
148
+ body: JSON.stringify({
149
+ input,
150
+ config: configOverrides,
151
+ execution: Object.keys(executionPayload).length > 0 ? executionPayload : undefined,
152
+ }),
153
+ signal,
154
+ });
155
+
156
+ const data = (await res.json().catch(() => null)) as any;
157
+
158
+ if (data && typeof data === "object" && typeof data.ok === "boolean") {
159
+ return data;
160
+ }
161
+
162
+ if (!res.ok) {
163
+ return {
164
+ ok: false,
165
+ runId: randomUUID(),
166
+ error: {
167
+ code: res.status === 401 ? "UNAUTHORIZED" : "REMOTE_EXECUTION_FAILED",
168
+ message: `Remote server HTTP ${res.status}: ${res.statusText}`,
169
+ details: data,
170
+ },
171
+ };
172
+ }
173
+
174
+ return {
175
+ ok: true,
176
+ runId: randomUUID(),
177
+ data,
178
+ };
179
+ } catch (err: any) {
180
+ if (err.name === "AbortError" || signal?.aborted) {
181
+ return {
182
+ ok: false,
183
+ runId: randomUUID(),
184
+ error: {
185
+ code: "ACTION_CANCELLED",
186
+ message: "Action execution was cancelled",
187
+ },
188
+ };
189
+ }
190
+ return {
191
+ ok: false,
192
+ runId: randomUUID(),
193
+ error: {
194
+ code: "NETWORK_ERROR",
195
+ message: `Failed to connect to remote ActionDock server at ${serverUrl}: ${err.message}`,
196
+ },
197
+ };
198
+ }
199
+ }
200
+
201
+ async function fetchRemoteJson<T = any>(
202
+ serverUrl: string,
203
+ path: string,
204
+ token?: string,
205
+ options: { method?: string; body?: unknown; errorPrefix?: string } = {}
206
+ ): Promise<T> {
207
+ const base = normalizeServerUrl(serverUrl);
208
+ const url = `${base}${path.startsWith("/") ? path : `/${path}`}`;
209
+ const method = options.method || "GET";
210
+ const headers: Record<string, string> = {
211
+ ...buildHeaders(token),
212
+ };
213
+ if (options.body !== undefined) {
214
+ headers["Content-Type"] = "application/json";
215
+ }
216
+
217
+ const res = await fetch(url, {
218
+ method,
219
+ headers,
220
+ body: options.body !== undefined ? JSON.stringify(options.body) : undefined,
221
+ });
222
+
223
+ const data = (await res.json().catch(() => ({}))) as any;
224
+
225
+ if (!res.ok || (options.method === "POST" && data && data.ok === false)) {
226
+ const errorPrefix = options.errorPrefix || "Remote request failed";
227
+ const msg = data?.error?.message || `${errorPrefix} (${res.status}): ${res.statusText}`;
228
+ throw new Error(msg);
229
+ }
230
+
231
+ return data as T;
232
+ }
233
+
234
+ export async function fetchRemoteRun(
235
+ serverUrl: string,
236
+ runId: string,
237
+ token?: string
238
+ ): Promise<RunRecord> {
239
+ return fetchRemoteJson<RunRecord>(
240
+ serverUrl,
241
+ `/api/v1/runs/${encodeURIComponent(runId)}`,
242
+ token,
243
+ { errorPrefix: `Failed to fetch remote run '${runId}'` }
244
+ );
245
+ }
246
+
247
+ export async function cancelRemoteRun(
248
+ serverUrl: string,
249
+ runId: string,
250
+ token?: string,
251
+ reason?: string
252
+ ): Promise<{ ok: boolean; runId: string; status: string }> {
253
+ return fetchRemoteJson(
254
+ serverUrl,
255
+ `/api/v1/runs/${encodeURIComponent(runId)}/cancel`,
256
+ token,
257
+ {
258
+ method: "POST",
259
+ body: { reason },
260
+ errorPrefix: `Failed to cancel remote run '${runId}'`,
261
+ }
262
+ );
263
+ }
264
+
265
+ export async function fetchRemoteActions(
266
+ serverUrl: string,
267
+ token?: string,
268
+ intent?: string
269
+ ): Promise<Array<{ id: string; description: string; packageId?: string }>> {
270
+ const query = intent ? `?intent=${encodeURIComponent(intent)}` : "";
271
+ return fetchRemoteJson(
272
+ serverUrl,
273
+ `/api/v1/actions${query}`,
274
+ token,
275
+ { errorPrefix: "Failed to fetch remote actions" }
276
+ );
277
+ }
278
+
279
+ export async function fetchRemoteActionShow(
280
+ serverUrl: string,
281
+ actionId: string,
282
+ token?: string
283
+ ): Promise<any> {
284
+ return fetchRemoteJson(
285
+ serverUrl,
286
+ `/api/v1/actions/${encodeURIComponent(actionId)}`,
287
+ token,
288
+ { errorPrefix: `Failed to fetch remote action '${actionId}'` }
289
+ );
290
+ }
291
+
292
+ export async function fetchRemoteInfo(
293
+ serverUrl: string,
294
+ token?: string
295
+ ): Promise<any> {
296
+ return fetchRemoteJson(
297
+ serverUrl,
298
+ "/api/v1/info",
299
+ token,
300
+ { errorPrefix: "Failed to fetch remote info" }
301
+ );
302
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./types";
2
+ export * from "./manager";
3
+ export * from "./client";