openworkgraph 0.2.12
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 +64 -0
- package/dist/api-image-agent.d.ts +32 -0
- package/dist/api-image-agent.js +132 -0
- package/dist/api-image-output.d.ts +8 -0
- package/dist/api-image-output.js +41 -0
- package/dist/api.d.ts +35 -0
- package/dist/api.js +153 -0
- package/dist/auth.d.ts +58 -0
- package/dist/auth.js +273 -0
- package/dist/backend/adapter.d.ts +35 -0
- package/dist/backend/adapter.js +439 -0
- package/dist/backend/capabilities.d.ts +23 -0
- package/dist/backend/capabilities.js +75 -0
- package/dist/backend/diagnostics.d.ts +9 -0
- package/dist/backend/diagnostics.js +80 -0
- package/dist/backend/index.d.ts +6 -0
- package/dist/backend/index.js +7 -0
- package/dist/backend/models.d.ts +15 -0
- package/dist/backend/models.js +43 -0
- package/dist/backend/paths.d.ts +6 -0
- package/dist/backend/paths.js +69 -0
- package/dist/backend/stdio.d.ts +66 -0
- package/dist/backend/stdio.js +172 -0
- package/dist/backend/types.d.ts +90 -0
- package/dist/backend/types.js +11 -0
- package/dist/backup-api.d.ts +10 -0
- package/dist/backup-api.js +40 -0
- package/dist/blob-store.d.ts +40 -0
- package/dist/blob-store.js +218 -0
- package/dist/cli-help.d.ts +1 -0
- package/dist/cli-help.js +63 -0
- package/dist/cli-output.d.ts +4 -0
- package/dist/cli-output.js +130 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +284 -0
- package/dist/client-pairing.d.ts +8 -0
- package/dist/client-pairing.js +30 -0
- package/dist/directories.d.ts +14 -0
- package/dist/directories.js +34 -0
- package/dist/errors.d.ts +12 -0
- package/dist/errors.js +7 -0
- package/dist/events.d.ts +22 -0
- package/dist/events.js +155 -0
- package/dist/execution-chains.d.ts +24 -0
- package/dist/execution-chains.js +231 -0
- package/dist/execution-settings.d.ts +10 -0
- package/dist/execution-settings.js +38 -0
- package/dist/graph-document-history.d.ts +43 -0
- package/dist/graph-document-history.js +275 -0
- package/dist/graph-files.d.ts +14 -0
- package/dist/graph-files.js +59 -0
- package/dist/graph-transfer.d.ts +53 -0
- package/dist/graph-transfer.js +501 -0
- package/dist/graphs.d.ts +68 -0
- package/dist/graphs.js +771 -0
- package/dist/history.d.ts +20 -0
- package/dist/history.js +61 -0
- package/dist/http.d.ts +2 -0
- package/dist/http.js +42 -0
- package/dist/image-credentials.d.ts +11 -0
- package/dist/image-credentials.js +130 -0
- package/dist/image-providers.d.ts +51 -0
- package/dist/image-providers.js +271 -0
- package/dist/input-materialization.d.ts +17 -0
- package/dist/input-materialization.js +95 -0
- package/dist/inputs.d.ts +74 -0
- package/dist/inputs.js +301 -0
- package/dist/local-control.d.ts +13 -0
- package/dist/local-control.js +37 -0
- package/dist/network.d.ts +11 -0
- package/dist/network.js +48 -0
- package/dist/openai-image-driver.d.ts +42 -0
- package/dist/openai-image-driver.js +368 -0
- package/dist/operations/backups.d.ts +40 -0
- package/dist/operations/backups.js +391 -0
- package/dist/operations/capacity.d.ts +7 -0
- package/dist/operations/capacity.js +35 -0
- package/dist/operations/cli-context.d.ts +7 -0
- package/dist/operations/cli-context.js +58 -0
- package/dist/operations/daemon.d.ts +36 -0
- package/dist/operations/daemon.js +159 -0
- package/dist/operations/files.d.ts +12 -0
- package/dist/operations/files.js +84 -0
- package/dist/operations/lifecycle.d.ts +28 -0
- package/dist/operations/lifecycle.js +155 -0
- package/dist/operations/local-admin.d.ts +6 -0
- package/dist/operations/local-admin.js +38 -0
- package/dist/operations/logs.d.ts +16 -0
- package/dist/operations/logs.js +37 -0
- package/dist/operations/plugins.d.ts +10 -0
- package/dist/operations/plugins.js +11 -0
- package/dist/operations/snapshot-worker.d.ts +1 -0
- package/dist/operations/snapshot-worker.js +18 -0
- package/dist/operations/snapshot.d.ts +1 -0
- package/dist/operations/snapshot.js +14 -0
- package/dist/operations/update.d.ts +26 -0
- package/dist/operations/update.js +216 -0
- package/dist/persistence/asset-business-schema.d.ts +2 -0
- package/dist/persistence/asset-business-schema.js +36 -0
- package/dist/persistence/database.d.ts +7 -0
- package/dist/persistence/database.js +91 -0
- package/dist/persistence/plugin-schema.d.ts +9 -0
- package/dist/persistence/plugin-schema.js +38 -0
- package/dist/persistence/repositories.d.ts +20 -0
- package/dist/persistence/repositories.js +106 -0
- package/dist/persistence/resource-schema.d.ts +2 -0
- package/dist/persistence/resource-schema.js +44 -0
- package/dist/persistence/schema.d.ts +6 -0
- package/dist/persistence/schema.js +236 -0
- package/dist/persistence/workflow-schema.d.ts +2 -0
- package/dist/persistence/workflow-schema.js +19 -0
- package/dist/plugin-api.d.ts +10 -0
- package/dist/plugin-api.js +40 -0
- package/dist/plugin-management.d.ts +36 -0
- package/dist/plugin-management.js +112 -0
- package/dist/plugins.d.ts +55 -0
- package/dist/plugins.js +239 -0
- package/dist/project-discovery.d.ts +3 -0
- package/dist/project-discovery.js +57 -0
- package/dist/project-files-api.d.ts +16 -0
- package/dist/project-files-api.js +243 -0
- package/dist/project-files.d.ts +97 -0
- package/dist/project-files.js +557 -0
- package/dist/project-lock.d.ts +4 -0
- package/dist/project-lock.js +35 -0
- package/dist/project-probe-worker.d.ts +1 -0
- package/dist/project-probe-worker.js +17 -0
- package/dist/project-probe.d.ts +22 -0
- package/dist/project-probe.js +67 -0
- package/dist/projects.d.ts +29 -0
- package/dist/projects.js +219 -0
- package/dist/publication.d.ts +31 -0
- package/dist/publication.js +642 -0
- package/dist/resource-api.d.ts +15 -0
- package/dist/resource-api.js +256 -0
- package/dist/resources-orphans.d.ts +41 -0
- package/dist/resources-orphans.js +235 -0
- package/dist/resources.d.ts +246 -0
- package/dist/resources.js +714 -0
- package/dist/run-api.d.ts +10 -0
- package/dist/run-api.js +308 -0
- package/dist/run-context.d.ts +54 -0
- package/dist/run-context.js +137 -0
- package/dist/runs.d.ts +82 -0
- package/dist/runs.js +506 -0
- package/dist/runtime-backend.d.ts +24 -0
- package/dist/runtime-backend.js +214 -0
- package/dist/runtime.d.ts +97 -0
- package/dist/runtime.js +553 -0
- package/dist/scheduler.d.ts +85 -0
- package/dist/scheduler.js +344 -0
- package/dist/service.d.ts +29 -0
- package/dist/service.js +515 -0
- package/dist/skills-api.d.ts +18 -0
- package/dist/skills-api.js +120 -0
- package/dist/storage-api.d.ts +21 -0
- package/dist/storage-api.js +92 -0
- package/dist/transfer-api.d.ts +10 -0
- package/dist/transfer-api.js +60 -0
- package/dist/vendor/protocol/contracts.typecheck.d.ts +1 -0
- package/dist/vendor/protocol/contracts.typecheck.js +35 -0
- package/dist/vendor/protocol/execution-chain.d.ts +6 -0
- package/dist/vendor/protocol/execution-chain.js +26 -0
- package/dist/vendor/protocol/graph-binary.d.ts +15 -0
- package/dist/vendor/protocol/graph-binary.js +121 -0
- package/dist/vendor/protocol/graph-title.d.ts +2 -0
- package/dist/vendor/protocol/graph-title.js +23 -0
- package/dist/vendor/protocol/index.d.ts +617 -0
- package/dist/vendor/protocol/index.js +20 -0
- package/dist/vendor/protocol/pairing.d.ts +29 -0
- package/dist/vendor/protocol/pairing.js +71 -0
- package/dist/vendor/protocol/preview.d.ts +2 -0
- package/dist/vendor/protocol/preview.js +9 -0
- package/dist/vendor/protocol/resource-mime.d.ts +2 -0
- package/dist/vendor/protocol/resource-mime.js +35 -0
- package/dist/video-thumbnail.d.ts +2 -0
- package/dist/video-thumbnail.js +34 -0
- package/package.json +9 -0
package/dist/service.js
ADDED
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import { once } from "node:events";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { PROTOCOL_VERSION, SERVICE_VERSION, isProtocolCompatible, } from "./vendor/protocol/index.js";
|
|
5
|
+
import { initializeDirectories } from "./directories.js";
|
|
6
|
+
import { openDatabase } from "./persistence/database.js";
|
|
7
|
+
import { Repositories } from "./persistence/repositories.js";
|
|
8
|
+
import { Auth, normalizeOrigin } from "./auth.js";
|
|
9
|
+
import { ServiceError } from "./errors.js";
|
|
10
|
+
import { readJson } from "./http.js";
|
|
11
|
+
import { acceptsHost, listenerInfo } from "./network.js";
|
|
12
|
+
import { Projects } from "./projects.js";
|
|
13
|
+
import { ProjectProbe } from "./project-probe.js";
|
|
14
|
+
import { BusinessApi } from "./api.js";
|
|
15
|
+
import { Events } from "./events.js";
|
|
16
|
+
import { WorkflowRuntime } from "./runtime.js";
|
|
17
|
+
import { compareSemVer, latestAgentServiceVersion } from './operations/update.js';
|
|
18
|
+
const HTTP_STATUS = {
|
|
19
|
+
INVALID_REQUEST: 400,
|
|
20
|
+
UNAUTHENTICATED: 401,
|
|
21
|
+
SESSION_EXPIRED: 401,
|
|
22
|
+
SESSION_REVOKED: 401,
|
|
23
|
+
ORIGIN_DENIED: 403,
|
|
24
|
+
HOST_DENIED: 403,
|
|
25
|
+
SERVICE_MISMATCH: 409,
|
|
26
|
+
PROTOCOL_INCOMPATIBLE: 426,
|
|
27
|
+
NOT_FOUND: 404,
|
|
28
|
+
PAIRING_CODE_INVALID: 400,
|
|
29
|
+
PAIRING_CODE_EXPIRED: 410,
|
|
30
|
+
PAIRING_CODE_USED: 409,
|
|
31
|
+
PAYLOAD_TOO_LARGE: 413,
|
|
32
|
+
UNSUPPORTED_MEDIA_TYPE: 415,
|
|
33
|
+
};
|
|
34
|
+
const CORS_HEADERS = [
|
|
35
|
+
"authorization",
|
|
36
|
+
"content-type",
|
|
37
|
+
"x-workgraph-protocol",
|
|
38
|
+
"x-workgraph-service-id",
|
|
39
|
+
"last-event-id",
|
|
40
|
+
"range",
|
|
41
|
+
];
|
|
42
|
+
export function createService(dataDir, options = {}) {
|
|
43
|
+
const directories = initializeDirectories(dataDir);
|
|
44
|
+
const db = openDatabase(directories.database);
|
|
45
|
+
let serviceId;
|
|
46
|
+
try {
|
|
47
|
+
serviceId = new Repositories(db).identity();
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
db.close();
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
const auth = new Auth(db, serviceId, options.now);
|
|
54
|
+
const projects = new Projects(db, serviceId);
|
|
55
|
+
const projectProbe = options.projectProbe ?? new ProjectProbe();
|
|
56
|
+
const runtime = new WorkflowRuntime(db, serviceId, directories, options.runtime);
|
|
57
|
+
const api = new BusinessApi(db, serviceId, auth, runtime);
|
|
58
|
+
const events = new Events(db, serviceId, auth);
|
|
59
|
+
const instanceId = randomUUID();
|
|
60
|
+
const npmInstallation = options.npmInstallation ?? (async () => false);
|
|
61
|
+
let updateQueued = false;
|
|
62
|
+
const unavailable = {
|
|
63
|
+
status: "unavailable",
|
|
64
|
+
reason: "Not implemented yet",
|
|
65
|
+
verifiedAt: null,
|
|
66
|
+
};
|
|
67
|
+
const info = {
|
|
68
|
+
serviceId,
|
|
69
|
+
version: SERVICE_VERSION,
|
|
70
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
71
|
+
capabilities: {
|
|
72
|
+
pairing: {
|
|
73
|
+
status: "available",
|
|
74
|
+
reason: "Local pairing code and persistent revocable sessions",
|
|
75
|
+
verifiedAt: "2026-09-14T00:00:00.000Z",
|
|
76
|
+
},
|
|
77
|
+
projects: unavailable,
|
|
78
|
+
graphs: unavailable,
|
|
79
|
+
assets: unavailable,
|
|
80
|
+
execution: unavailable,
|
|
81
|
+
imageGeneration: {
|
|
82
|
+
status: "unknown",
|
|
83
|
+
reason: "Real image generation has not been verified",
|
|
84
|
+
verifiedAt: null,
|
|
85
|
+
},
|
|
86
|
+
events: unavailable,
|
|
87
|
+
backups: unavailable,
|
|
88
|
+
projectFiles: {
|
|
89
|
+
status: "available",
|
|
90
|
+
reason: "Bounded project file browsing, search, status and content access",
|
|
91
|
+
verifiedAt: "2026-09-17T00:00:00.000Z",
|
|
92
|
+
},
|
|
93
|
+
projectFileReferences: {
|
|
94
|
+
status: "available",
|
|
95
|
+
reason: "Live project file references, detach-on-edit and execution path inputs",
|
|
96
|
+
verifiedAt: "2026-09-17T00:00:00.000Z",
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
info.capabilities.projects = {
|
|
101
|
+
status: "available",
|
|
102
|
+
reason: "Project registration, state filters and path repair; run submission integration is pending",
|
|
103
|
+
verifiedAt: "2026-09-14T00:00:00.000Z",
|
|
104
|
+
};
|
|
105
|
+
info.capabilities.graphs = {
|
|
106
|
+
status: "available",
|
|
107
|
+
reason: "Persistent graph commands, revisions and input locks",
|
|
108
|
+
verifiedAt: "2026-09-14T00:00:00.000Z",
|
|
109
|
+
};
|
|
110
|
+
info.capabilities.events = {
|
|
111
|
+
status: "available",
|
|
112
|
+
reason: "Authenticated persisted SSE and scoped replay cursors",
|
|
113
|
+
verifiedAt: "2026-09-14T00:00:00.000Z",
|
|
114
|
+
};
|
|
115
|
+
info.capabilities.assets = runtime.capabilities.assets;
|
|
116
|
+
info.capabilities.backups = {
|
|
117
|
+
status: "available",
|
|
118
|
+
reason: "Manual consistent backups and authenticated download; restore is local-only",
|
|
119
|
+
verifiedAt: null,
|
|
120
|
+
};
|
|
121
|
+
const server = createServer((request, response) => {
|
|
122
|
+
response.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
123
|
+
response.setHeader("Cache-Control", "no-store");
|
|
124
|
+
response.setHeader("X-Content-Type-Options", "nosniff");
|
|
125
|
+
response.setHeader("Referrer-Policy", "no-referrer");
|
|
126
|
+
response.setHeader("Vary", "Origin");
|
|
127
|
+
response.setHeader("Access-Control-Expose-Headers", "Content-Range, Accept-Ranges, Content-Disposition");
|
|
128
|
+
const requestId = randomUUID();
|
|
129
|
+
const send = (body) => response.end(JSON.stringify(body));
|
|
130
|
+
void (async () => {
|
|
131
|
+
const address = server.address();
|
|
132
|
+
if (!address ||
|
|
133
|
+
typeof address === "string" ||
|
|
134
|
+
!acceptsHost(request.headers.host, address))
|
|
135
|
+
throw new ServiceError("HOST_DENIED", "Host 请求头无效。");
|
|
136
|
+
const rawPath = request.url ?? "";
|
|
137
|
+
const fileRoute = /^[/]v1[/]projects[/][a-zA-Z0-9_-]+[/]files(?:[/](?:search|content|link-content|media|thumbnail|stat))?(?:[?]|$)/.test(rawPath);
|
|
138
|
+
const skillsRoute = /^[/]v1[/]projects[/][a-zA-Z0-9_-]+[/]skills[/]search(?:[?]|$)/.test(rawPath);
|
|
139
|
+
const mediaRoute = /^[/]v1[/]projects[/][a-zA-Z0-9_-]+[/](?:assets[/][a-zA-Z0-9_-]+[/]versions[/][1-9][0-9]*|graphs[/][a-zA-Z0-9_-]+[/]resources[/][a-zA-Z0-9_-]+[/]versions[/][1-9][0-9]*)[/](?:thumbnail|preview|content)(?:[?]|$)/.test(rawPath);
|
|
140
|
+
const queryRoute = fileRoute || skillsRoute || mediaRoute;
|
|
141
|
+
const path = queryRoute ? rawPath.split('?')[0] : rawPath;
|
|
142
|
+
if (!path.startsWith("/") ||
|
|
143
|
+
path.startsWith("//") ||
|
|
144
|
+
(!queryRoute && path.includes("?")) ||
|
|
145
|
+
path.includes("#") ||
|
|
146
|
+
path.includes("%") ||
|
|
147
|
+
path.includes("\\"))
|
|
148
|
+
throw new ServiceError("INVALID_REQUEST", "请使用标准 API 路径;凭据只能放在 Authorization 请求头,不能放入 URL。");
|
|
149
|
+
if (queryRoute && (rawPath.includes('#') || rawPath.length > 8192 || rawPath.split('?').length > 2))
|
|
150
|
+
throw new ServiceError('INVALID_REQUEST', '项目文件查询参数无效。');
|
|
151
|
+
const originHeader = request.headers.origin;
|
|
152
|
+
const pairingRequest = ['/v1/pair/request', '/v1/pair/status', '/v1/pair/complete'].includes(path) && (request.method === 'POST' || request.method === 'OPTIONS');
|
|
153
|
+
if (originHeader !== undefined) {
|
|
154
|
+
if (pairingRequest) {
|
|
155
|
+
if (normalizeOrigin(originHeader) !== originHeader)
|
|
156
|
+
throw new ServiceError('ORIGIN_DENIED', '网页来源无效。');
|
|
157
|
+
}
|
|
158
|
+
else
|
|
159
|
+
auth.requireOrigin(originHeader);
|
|
160
|
+
response.setHeader("Access-Control-Allow-Origin", originHeader);
|
|
161
|
+
}
|
|
162
|
+
if (request.method === "OPTIONS") {
|
|
163
|
+
if (!pairingRequest)
|
|
164
|
+
auth.requireOrigin(originHeader);
|
|
165
|
+
else if (!originHeader || request.headers['access-control-request-method'] !== 'POST')
|
|
166
|
+
throw new ServiceError('ORIGIN_DENIED', '配对申请仅允许 POST。');
|
|
167
|
+
const method = request.headers["access-control-request-method"];
|
|
168
|
+
const headers = String(request.headers["access-control-request-headers"] ?? "")
|
|
169
|
+
.toLowerCase()
|
|
170
|
+
.split(",")
|
|
171
|
+
.map((value) => value.trim())
|
|
172
|
+
.filter(Boolean);
|
|
173
|
+
if (typeof method !== "string" ||
|
|
174
|
+
!["GET", "POST", "DELETE"].includes(method) ||
|
|
175
|
+
headers.some((header) => !CORS_HEADERS.includes(header)))
|
|
176
|
+
throw new ServiceError("ORIGIN_DENIED", "不允许的跨来源方法或请求头。");
|
|
177
|
+
response.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE");
|
|
178
|
+
response.setHeader("Access-Control-Allow-Headers", CORS_HEADERS.join(", "));
|
|
179
|
+
response.setHeader("Access-Control-Max-Age", "300");
|
|
180
|
+
response.writeHead(204);
|
|
181
|
+
response.end();
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const protocol = request.headers["x-workgraph-protocol"];
|
|
185
|
+
if (protocol !== undefined &&
|
|
186
|
+
(typeof protocol !== "string" || !isProtocolCompatible(protocol)))
|
|
187
|
+
throw new ServiceError("PROTOCOL_INCOMPATIBLE", "请使用协议 1.x 的客户端。");
|
|
188
|
+
const expectedService = request.headers["x-workgraph-service-id"];
|
|
189
|
+
if (expectedService !== undefined && expectedService !== serviceId)
|
|
190
|
+
throw new ServiceError("SERVICE_MISMATCH", "地址对应其他服务,请核对 serviceId;不会自动接管该服务的数据。");
|
|
191
|
+
if (request.method === "GET" && path === "/health") {
|
|
192
|
+
send({
|
|
193
|
+
status: "ok",
|
|
194
|
+
version: SERVICE_VERSION,
|
|
195
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
196
|
+
});
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (request.method === "GET" && path === "/v1/info") {
|
|
200
|
+
const runtimeInfo = runtime.serviceInfo;
|
|
201
|
+
send({
|
|
202
|
+
...info,
|
|
203
|
+
installation: await npmInstallation() ? 'npm-global' : 'other',
|
|
204
|
+
imageRoutes: runtimeInfo.imageRoutes,
|
|
205
|
+
capabilities: {
|
|
206
|
+
...info.capabilities,
|
|
207
|
+
assets: runtime.capabilities.assets,
|
|
208
|
+
execution: runtime.executionCapability,
|
|
209
|
+
imageGeneration: runtime.imageGenerationCapability,
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (request.method === 'POST' && path === '/v1/pair/request') {
|
|
215
|
+
if (!originHeader || !protocol)
|
|
216
|
+
throw new ServiceError('INVALID_REQUEST', '配对申请缺少来源或协议版本。');
|
|
217
|
+
const input = await readJson(request);
|
|
218
|
+
if (Object.keys(input).some(key => key !== 'clientCode') || typeof input.clientCode !== 'string')
|
|
219
|
+
throw new ServiceError('INVALID_REQUEST', '配对申请必须包含客户端公钥。');
|
|
220
|
+
send(auth.requestClientPairing(input.clientCode, originHeader));
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (request.method === 'POST' && path === '/v1/pair/status') {
|
|
224
|
+
if (!originHeader || !protocol)
|
|
225
|
+
throw new ServiceError('INVALID_REQUEST', '配对状态请求缺少来源或协议版本。');
|
|
226
|
+
const input = await readJson(request);
|
|
227
|
+
if (Object.keys(input).some(key => !['code', 'browserName', 'expectedServiceId', 'clientCode', 'proof'].includes(key)))
|
|
228
|
+
throw new ServiceError('INVALID_REQUEST', '配对状态请求包含未知字段。');
|
|
229
|
+
send(auth.claimClientPairing(input, originHeader));
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (request.method === 'POST' && path === '/v1/pair/complete') {
|
|
233
|
+
if (!originHeader || !protocol)
|
|
234
|
+
throw new ServiceError('INVALID_REQUEST', '配对完成请求缺少来源或协议版本。');
|
|
235
|
+
const input = await readJson(request);
|
|
236
|
+
if (Object.keys(input).some(key => !['code', 'expectedServiceId'].includes(key)) || typeof input.code !== 'string' || typeof input.expectedServiceId !== 'string')
|
|
237
|
+
throw new ServiceError('INVALID_REQUEST', '配对完成请求无效。');
|
|
238
|
+
const bearer = request.headers.authorization;
|
|
239
|
+
if (!bearer?.startsWith('Bearer '))
|
|
240
|
+
throw new ServiceError('UNAUTHENTICATED', '配对完成请求缺少通讯凭据。');
|
|
241
|
+
send(auth.completeClientPairing(input.code, input.expectedServiceId, bearer.slice(7), originHeader));
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const origin = auth.requireOrigin(originHeader);
|
|
245
|
+
if (!protocol)
|
|
246
|
+
throw new ServiceError("PROTOCOL_INCOMPATIBLE", "业务请求必须携带 X-Workgraph-Protocol: 1.0。");
|
|
247
|
+
if (expectedService !== serviceId)
|
|
248
|
+
throw new ServiceError("SERVICE_MISMATCH", "业务请求必须携带 X-Workgraph-Service-Id。");
|
|
249
|
+
if (request.method === "POST" && path === "/v1/pair") {
|
|
250
|
+
const input = await readJson(request);
|
|
251
|
+
if (Object.keys(input).some((key) => !["code", "browserName", "expectedServiceId", "clientCode", "proof"].includes(key)))
|
|
252
|
+
throw new ServiceError("INVALID_REQUEST", "配对请求包含未知字段。");
|
|
253
|
+
send(auth.pair(input, origin));
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
const authorization = request.headers.authorization;
|
|
257
|
+
if (!authorization?.startsWith("Bearer "))
|
|
258
|
+
throw new ServiceError("UNAUTHENTICATED", "请提供 Authorization: Bearer 会话凭据。");
|
|
259
|
+
const token = authorization.slice(7);
|
|
260
|
+
if (request.method === 'POST' && path === '/v1/runtime/update') {
|
|
261
|
+
auth.withSession(token, origin, () => undefined);
|
|
262
|
+
if (!await npmInstallation() || !options.updateRuntime)
|
|
263
|
+
throw new ServiceError('INVALID_REQUEST', 'This Runtime was not installed globally with npm.');
|
|
264
|
+
if (updateQueued)
|
|
265
|
+
throw new ServiceError('INVALID_REQUEST', 'Runtime update is already starting.');
|
|
266
|
+
updateQueued = true;
|
|
267
|
+
let latestVersion;
|
|
268
|
+
try {
|
|
269
|
+
latestVersion = await (options.latestNpmVersion ?? latestAgentServiceVersion)();
|
|
270
|
+
if (compareSemVer(latestVersion, SERVICE_VERSION) <= 0)
|
|
271
|
+
throw new ServiceError('INVALID_REQUEST', 'No newer npm Runtime version is available.');
|
|
272
|
+
auth.withSession(token, origin, () => undefined);
|
|
273
|
+
await options.updateRuntime(() => { updateQueued = false; });
|
|
274
|
+
response.statusCode = 202;
|
|
275
|
+
send({ accepted: true, latestVersion });
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
updateQueued = false;
|
|
280
|
+
throw error;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (request.method === "GET" && path === "/v1/events/cursor") {
|
|
284
|
+
send(events.currentCursor(token, origin));
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (request.method === "GET" && path === "/v1/events") {
|
|
288
|
+
events.stream(request, response, token, origin);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
const business = await api.handle(request, fileRoute || skillsRoute ? rawPath : path, token, origin);
|
|
292
|
+
if (business.handled) {
|
|
293
|
+
const body = auth.withSession(token, origin, (current) => business.binary ?? (business.binaryParts ? undefined : events.decorate(business.body, current.id)));
|
|
294
|
+
if (business.status)
|
|
295
|
+
response.statusCode = business.status;
|
|
296
|
+
for (const [key, value] of Object.entries(business.headers ?? {}))
|
|
297
|
+
response.setHeader(key, value);
|
|
298
|
+
if (business.binaryParts) {
|
|
299
|
+
const disconnected = new AbortController();
|
|
300
|
+
const abort = () => disconnected.abort();
|
|
301
|
+
response.once('close', abort);
|
|
302
|
+
try {
|
|
303
|
+
for (const part of business.binaryParts) {
|
|
304
|
+
for (let offset = 0; offset < part.length; offset += 1024 * 1024) {
|
|
305
|
+
if (!response.write(part.subarray(offset, offset + 1024 * 1024)))
|
|
306
|
+
await once(response, 'drain', { signal: disconnected.signal });
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
response.end();
|
|
310
|
+
}
|
|
311
|
+
finally {
|
|
312
|
+
response.off('close', abort);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
else if (business.binary)
|
|
316
|
+
response.end(body);
|
|
317
|
+
else
|
|
318
|
+
send(body);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
// Authenticate before reading request data; recheck after the asynchronous body read.
|
|
322
|
+
let projectInput;
|
|
323
|
+
if (request.method === "POST" &&
|
|
324
|
+
(path === "/v1/projects" ||
|
|
325
|
+
/^[/]v1[/]projects[/][0-9a-f-]{36}[/](state|path)$/.test(path))) {
|
|
326
|
+
auth.withSession(token, origin, () => undefined);
|
|
327
|
+
projectInput = await readJson(request);
|
|
328
|
+
}
|
|
329
|
+
let projectView = projects;
|
|
330
|
+
let candidates;
|
|
331
|
+
let projectListSnapshot;
|
|
332
|
+
const listState = path === "/v1/projects/inactive"
|
|
333
|
+
? "inactive"
|
|
334
|
+
: path === "/v1/projects/all"
|
|
335
|
+
? "all"
|
|
336
|
+
: "active";
|
|
337
|
+
const listRows = () => db
|
|
338
|
+
.prepare("SELECT id,canonical_path,state FROM projects WHERE ?='all' OR state=? ORDER BY id")
|
|
339
|
+
.all(listState, listState);
|
|
340
|
+
const unprobed = () => {
|
|
341
|
+
throw new Error("Directory availability has not been probed");
|
|
342
|
+
};
|
|
343
|
+
if (request.method === "GET" && path === "/v1/project-candidates") {
|
|
344
|
+
auth.withSession(token, origin, () => undefined);
|
|
345
|
+
candidates = await projectProbe.discover(options.codexHome);
|
|
346
|
+
}
|
|
347
|
+
else if (projectInput ||
|
|
348
|
+
(request.method === "GET" &&
|
|
349
|
+
[
|
|
350
|
+
"/v1/projects",
|
|
351
|
+
"/v1/projects/inactive",
|
|
352
|
+
"/v1/projects/all",
|
|
353
|
+
].includes(path))) {
|
|
354
|
+
if (projectInput && path.endsWith("/state")) {
|
|
355
|
+
// A stalled old mount must never prevent deactivation/reactivation.
|
|
356
|
+
projectView = new Projects(db, serviceId, unprobed);
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
const paths = auth.withSession(token, origin, () => {
|
|
360
|
+
if (projectInput)
|
|
361
|
+
return typeof projectInput["path"] === "string"
|
|
362
|
+
? [projectInput["path"]]
|
|
363
|
+
: [];
|
|
364
|
+
const rows = listRows();
|
|
365
|
+
projectListSnapshot = JSON.stringify(rows);
|
|
366
|
+
return rows.map((row) => String(row["canonical_path"]));
|
|
367
|
+
});
|
|
368
|
+
try {
|
|
369
|
+
projectView = new Projects(db, serviceId, await projectProbe.inspect(paths));
|
|
370
|
+
}
|
|
371
|
+
catch (error) {
|
|
372
|
+
if (projectInput)
|
|
373
|
+
throw error;
|
|
374
|
+
// Metadata remains readable when mounted storage does not respond.
|
|
375
|
+
projectView = new Projects(db, serviceId, unprobed);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
// All nonpublic routes, including future media/event paths, pass this gate.
|
|
380
|
+
const result = auth.withSession(token, origin, (current) => {
|
|
381
|
+
if (projectListSnapshot !== undefined &&
|
|
382
|
+
projectListSnapshot !== JSON.stringify(listRows()))
|
|
383
|
+
throw new ServiceError("CONFLICT", "项目列表在探测期间变化,请刷新后重试。");
|
|
384
|
+
if (request.method === "GET" && path === "/v1/project-candidates")
|
|
385
|
+
return candidates;
|
|
386
|
+
if (request.method === "GET" && path === "/v1/projects")
|
|
387
|
+
return projectView.list("active");
|
|
388
|
+
if (request.method === "GET" && path === "/v1/projects/inactive")
|
|
389
|
+
return projectView.list("inactive");
|
|
390
|
+
if (request.method === "GET" && path === "/v1/projects/all")
|
|
391
|
+
return projectView.list("all");
|
|
392
|
+
const projectMatch = /^[/]v1[/]projects[/]([0-9a-f-]{36})[/](state|path)$/.exec(path);
|
|
393
|
+
if (projectInput) {
|
|
394
|
+
const field = projectMatch?.[2] === "state" ? "state" : "path";
|
|
395
|
+
if (Object.keys(projectInput).length !== 1 ||
|
|
396
|
+
typeof projectInput[field] !== "string")
|
|
397
|
+
throw new ServiceError("INVALID_REQUEST", "项目请求须只包含指定的 path 或 state 字段。");
|
|
398
|
+
if (path === "/v1/projects")
|
|
399
|
+
return projectView.register(projectInput[field]);
|
|
400
|
+
if (projectMatch?.[2] === "path")
|
|
401
|
+
return projectView.repairPath(projectMatch[1], projectInput[field]);
|
|
402
|
+
if (projectMatch?.[2] === "state") {
|
|
403
|
+
const state = projectInput["state"];
|
|
404
|
+
if (state !== "active" && state !== "inactive")
|
|
405
|
+
throw new ServiceError("INVALID_REQUEST", "项目状态必须是 active 或 inactive。");
|
|
406
|
+
return projectView.setState(projectMatch[1], state);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
if (request.method === "GET" && path === "/v1/session")
|
|
410
|
+
return current;
|
|
411
|
+
if (request.method === "GET" && path === "/v1/sessions")
|
|
412
|
+
return auth.list(current.id);
|
|
413
|
+
const match = /^[/]v1[/]sessions[/]([0-9a-f-]{36})$/.exec(path);
|
|
414
|
+
if (request.method === "DELETE" && match) {
|
|
415
|
+
auth.revoke(match[1]);
|
|
416
|
+
return null;
|
|
417
|
+
}
|
|
418
|
+
throw new ServiceError("NOT_FOUND", "接口尚未实现。");
|
|
419
|
+
});
|
|
420
|
+
if (result === null) {
|
|
421
|
+
response.writeHead(204);
|
|
422
|
+
response.end();
|
|
423
|
+
}
|
|
424
|
+
else
|
|
425
|
+
send(result);
|
|
426
|
+
})().catch((error) => {
|
|
427
|
+
if (response.destroyed || response.writableEnded)
|
|
428
|
+
return;
|
|
429
|
+
const code = error instanceof ServiceError ? error.code : "INTERNAL_ERROR";
|
|
430
|
+
const body = {
|
|
431
|
+
error: {
|
|
432
|
+
code,
|
|
433
|
+
message: error instanceof ServiceError
|
|
434
|
+
? error.message
|
|
435
|
+
: "服务内部错误,请检查本机服务状态。",
|
|
436
|
+
requestId,
|
|
437
|
+
retryable: error instanceof ServiceError && error.retryable,
|
|
438
|
+
...(error instanceof ServiceError && error.details !== undefined ? { details: error.details } : {}),
|
|
439
|
+
},
|
|
440
|
+
};
|
|
441
|
+
const projectStatus = {
|
|
442
|
+
CONFLICT: 409,
|
|
443
|
+
ACTIVE_RUN: 409,
|
|
444
|
+
PROJECT_INACTIVE: 409,
|
|
445
|
+
PROJECT_UNAVAILABLE: 409,
|
|
446
|
+
REVISION_CONFLICT: 409,
|
|
447
|
+
NODE_LOCKED: 409,
|
|
448
|
+
INVALID_EDGE: 400,
|
|
449
|
+
INPUT_BLOCKED: 409,
|
|
450
|
+
INPUT_BUDGET_EXCEEDED: 413,
|
|
451
|
+
IDEMPOTENCY_CONFLICT: 409,
|
|
452
|
+
PLUGIN_UNAVAILABLE: 409,
|
|
453
|
+
MODEL_UNAVAILABLE: 409,
|
|
454
|
+
ALREADY_HANDLED: 409,
|
|
455
|
+
CURSOR_EXPIRED: 409,
|
|
456
|
+
MAINTENANCE: 503,
|
|
457
|
+
NOT_IMPLEMENTED: 501,
|
|
458
|
+
};
|
|
459
|
+
response.writeHead(HTTP_STATUS[code] ?? projectStatus[code] ?? 500);
|
|
460
|
+
send(body);
|
|
461
|
+
request.resume();
|
|
462
|
+
});
|
|
463
|
+
});
|
|
464
|
+
server.requestTimeout = 15_000;
|
|
465
|
+
server.headersTimeout = 10_000;
|
|
466
|
+
server.on("listening", () => {
|
|
467
|
+
const address = server.address();
|
|
468
|
+
db.prepare("INSERT INTO settings(key,value) VALUES('listener',?) ON CONFLICT(key) DO UPDATE SET value=excluded.value").run(JSON.stringify(listenerInfo(address, serviceId, instanceId)));
|
|
469
|
+
});
|
|
470
|
+
let closing;
|
|
471
|
+
async function abortStartup() {
|
|
472
|
+
runtime.disposeUnstarted();
|
|
473
|
+
projectProbe.close();
|
|
474
|
+
events.close();
|
|
475
|
+
try {
|
|
476
|
+
if (server.listening)
|
|
477
|
+
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
|
|
478
|
+
}
|
|
479
|
+
finally {
|
|
480
|
+
db.close();
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
function close() {
|
|
484
|
+
closing ??= (async () => {
|
|
485
|
+
await runtime.drain();
|
|
486
|
+
runtime.close();
|
|
487
|
+
projectProbe.close();
|
|
488
|
+
events.close();
|
|
489
|
+
try {
|
|
490
|
+
if (server.listening)
|
|
491
|
+
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
|
|
492
|
+
db.prepare("DELETE FROM settings WHERE key='listener' AND json_extract(value,'$.instanceId')=?").run(instanceId);
|
|
493
|
+
}
|
|
494
|
+
finally {
|
|
495
|
+
db.close();
|
|
496
|
+
}
|
|
497
|
+
})();
|
|
498
|
+
return closing;
|
|
499
|
+
}
|
|
500
|
+
return {
|
|
501
|
+
server,
|
|
502
|
+
db,
|
|
503
|
+
directories,
|
|
504
|
+
info,
|
|
505
|
+
auth,
|
|
506
|
+
projects,
|
|
507
|
+
api,
|
|
508
|
+
events,
|
|
509
|
+
runtime,
|
|
510
|
+
instanceId,
|
|
511
|
+
close,
|
|
512
|
+
abortStartup,
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
//# sourceMappingURL=service.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { IncomingMessage } from 'node:http';
|
|
2
|
+
import type { DatabaseSync } from 'node:sqlite';
|
|
3
|
+
import { Auth } from './auth.js';
|
|
4
|
+
import type { WorkflowRuntime } from './runtime.js';
|
|
5
|
+
import type { ApiResult } from './api.js';
|
|
6
|
+
export interface SkillCandidate {
|
|
7
|
+
name: string;
|
|
8
|
+
description: string;
|
|
9
|
+
}
|
|
10
|
+
export declare class SkillsApi {
|
|
11
|
+
private readonly db;
|
|
12
|
+
private readonly auth;
|
|
13
|
+
private readonly runtime;
|
|
14
|
+
private readonly cache;
|
|
15
|
+
constructor(db: DatabaseSync, auth: Auth, runtime: WorkflowRuntime);
|
|
16
|
+
private load;
|
|
17
|
+
handle(request: IncomingMessage, rawPath: string, token: string, origin: string): Promise<ApiResult>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { basename, join, resolve } from 'node:path';
|
|
4
|
+
import { Auth } from './auth.js';
|
|
5
|
+
import { ServiceError } from './errors.js';
|
|
6
|
+
import { StdioRpc } from './backend/stdio.js';
|
|
7
|
+
import { initialize } from './backend/capabilities.js';
|
|
8
|
+
const validName = (name) => /^[\w.:-]+$/.test(name);
|
|
9
|
+
function collectCodexSkills(value) {
|
|
10
|
+
if (!value || typeof value !== 'object' || !Array.isArray(value.data))
|
|
11
|
+
throw Error('Invalid skill catalog');
|
|
12
|
+
const result = [];
|
|
13
|
+
for (const entry of value.data) {
|
|
14
|
+
if (!entry || typeof entry !== 'object' || !Array.isArray(entry.skills))
|
|
15
|
+
continue;
|
|
16
|
+
for (const raw of entry.skills) {
|
|
17
|
+
if (!raw || typeof raw !== 'object')
|
|
18
|
+
continue;
|
|
19
|
+
const skill = raw;
|
|
20
|
+
if (skill.enabled === false || typeof skill.name !== 'string' || !validName(skill.name))
|
|
21
|
+
continue;
|
|
22
|
+
result.push({ name: skill.name, description: typeof skill.description === 'string' ? skill.description : '' });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
async function fallbackSkills(cwd) {
|
|
28
|
+
const home = homedir();
|
|
29
|
+
const roots = [join(home, '.agents', 'skills'), join(cwd, '.agents', 'skills'), join(home, '.codex', 'skills'), ...(process.env.CODEX_HOME ? [join(process.env.CODEX_HOME, 'skills')] : []), join(cwd, '.codex', 'skills')];
|
|
30
|
+
const result = [];
|
|
31
|
+
for (const root of roots) {
|
|
32
|
+
let entries;
|
|
33
|
+
try {
|
|
34
|
+
entries = await fs.readdir(root, { withFileTypes: true });
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
for (const entry of entries.slice(0, 500)) {
|
|
40
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink())
|
|
41
|
+
continue;
|
|
42
|
+
const name = entry.name;
|
|
43
|
+
if (!validName(name))
|
|
44
|
+
continue;
|
|
45
|
+
try {
|
|
46
|
+
const file = await fs.realpath(join(root, name, 'SKILL.md'));
|
|
47
|
+
if (basename(file) !== 'SKILL.md')
|
|
48
|
+
continue;
|
|
49
|
+
if ((await fs.stat(file)).size > 128 * 1024)
|
|
50
|
+
continue;
|
|
51
|
+
const content = await fs.readFile(file, 'utf8');
|
|
52
|
+
const description = /^description:\s*["']?(.+?)["']?\s*$/m.exec(content)?.[1] ?? '';
|
|
53
|
+
result.push({ name, description });
|
|
54
|
+
}
|
|
55
|
+
catch { /* Ignore incomplete or unreadable skills. */ }
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
export class SkillsApi {
|
|
61
|
+
db;
|
|
62
|
+
auth;
|
|
63
|
+
runtime;
|
|
64
|
+
cache = new Map();
|
|
65
|
+
constructor(db, auth, runtime) {
|
|
66
|
+
this.db = db;
|
|
67
|
+
this.auth = auth;
|
|
68
|
+
this.runtime = runtime;
|
|
69
|
+
}
|
|
70
|
+
load(cwd) {
|
|
71
|
+
const cached = this.cache.get(cwd);
|
|
72
|
+
if (cached && cached.expires > Date.now())
|
|
73
|
+
return cached.value;
|
|
74
|
+
const value = (async () => {
|
|
75
|
+
const options = this.runtime.options.backendOptions ?? {};
|
|
76
|
+
let rpc;
|
|
77
|
+
try {
|
|
78
|
+
rpc = new StdioRpc({ cwd, ...(options.executable ? { executable: options.executable } : {}), ...(options.args ? { args: options.args } : {}), timeoutMs: options.timeoutMs ?? 5000 });
|
|
79
|
+
await initialize(rpc);
|
|
80
|
+
return collectCodexSkills(await rpc.request('skills/list', { cwds: [cwd], forceReload: false }));
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return fallbackSkills(cwd);
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
rpc?.close();
|
|
87
|
+
}
|
|
88
|
+
})();
|
|
89
|
+
this.cache.set(cwd, { expires: Date.now() + 30_000, value });
|
|
90
|
+
void value.catch(() => this.cache.delete(cwd));
|
|
91
|
+
return value;
|
|
92
|
+
}
|
|
93
|
+
async handle(request, rawPath, token, origin) {
|
|
94
|
+
const url = new URL(rawPath, 'http://localhost');
|
|
95
|
+
const match = /^\/v1\/projects\/([a-zA-Z0-9_-]+)\/skills\/search$/.exec(url.pathname);
|
|
96
|
+
if (!match)
|
|
97
|
+
return { handled: false };
|
|
98
|
+
this.auth.withSession(token, origin, () => undefined);
|
|
99
|
+
if (request.method !== 'GET' || [...url.searchParams.keys()].some(key => key !== 'query') || url.searchParams.getAll('query').length > 1)
|
|
100
|
+
throw new ServiceError('INVALID_REQUEST', '无效的技能查询。');
|
|
101
|
+
const row = this.db.prepare("SELECT canonical_path FROM projects WHERE id=? AND state='active'").get(match[1]);
|
|
102
|
+
if (!row || typeof row.canonical_path !== 'string')
|
|
103
|
+
throw new ServiceError('NOT_FOUND', '项目不存在或未激活。');
|
|
104
|
+
const cwd = resolve(row.canonical_path);
|
|
105
|
+
const query = (url.searchParams.get('query') ?? '').trim().toLocaleLowerCase();
|
|
106
|
+
if (query.length > 100)
|
|
107
|
+
throw new ServiceError('INVALID_REQUEST', '技能查询过长。');
|
|
108
|
+
const candidates = await this.load(cwd);
|
|
109
|
+
this.auth.withSession(token, origin, () => undefined);
|
|
110
|
+
const seen = new Set();
|
|
111
|
+
const items = candidates.filter(item => {
|
|
112
|
+
if (seen.has(item.name))
|
|
113
|
+
return false;
|
|
114
|
+
seen.add(item.name);
|
|
115
|
+
return !query || item.name.toLocaleLowerCase().includes(query) || item.description.toLocaleLowerCase().includes(query);
|
|
116
|
+
}).sort((a, b) => a.name.localeCompare(b.name)).slice(0, 100);
|
|
117
|
+
return { handled: true, body: { items } };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=skills-api.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { IncomingMessage } from 'node:http';
|
|
2
|
+
import type { DatabaseSync } from 'node:sqlite';
|
|
3
|
+
import type { ApiResult } from './api.js';
|
|
4
|
+
import { Auth } from './auth.js';
|
|
5
|
+
import type { DataDirectories } from './directories.js';
|
|
6
|
+
import { Resources } from './resources.js';
|
|
7
|
+
export interface StorageUsage {
|
|
8
|
+
totalBytes: number;
|
|
9
|
+
categories: Record<'database' | 'resources' | 'runs' | 'backups' | 'config' | 'staging' | 'other', number>;
|
|
10
|
+
measuredAt: string;
|
|
11
|
+
}
|
|
12
|
+
/** Count managed files below the Runtime data root; never follow symlinks. */
|
|
13
|
+
export declare function measureStorage(directories: DataDirectories): Promise<StorageUsage>;
|
|
14
|
+
export declare class StorageApi {
|
|
15
|
+
readonly db: DatabaseSync;
|
|
16
|
+
readonly directories: DataDirectories;
|
|
17
|
+
readonly resources: Resources;
|
|
18
|
+
readonly auth: Auth;
|
|
19
|
+
constructor(db: DatabaseSync, directories: DataDirectories, resources: Resources, auth: Auth);
|
|
20
|
+
handle(request: IncomingMessage, path: string, token: string, origin: string): Promise<ApiResult>;
|
|
21
|
+
}
|