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.
Files changed (178) hide show
  1. package/README.md +64 -0
  2. package/dist/api-image-agent.d.ts +32 -0
  3. package/dist/api-image-agent.js +132 -0
  4. package/dist/api-image-output.d.ts +8 -0
  5. package/dist/api-image-output.js +41 -0
  6. package/dist/api.d.ts +35 -0
  7. package/dist/api.js +153 -0
  8. package/dist/auth.d.ts +58 -0
  9. package/dist/auth.js +273 -0
  10. package/dist/backend/adapter.d.ts +35 -0
  11. package/dist/backend/adapter.js +439 -0
  12. package/dist/backend/capabilities.d.ts +23 -0
  13. package/dist/backend/capabilities.js +75 -0
  14. package/dist/backend/diagnostics.d.ts +9 -0
  15. package/dist/backend/diagnostics.js +80 -0
  16. package/dist/backend/index.d.ts +6 -0
  17. package/dist/backend/index.js +7 -0
  18. package/dist/backend/models.d.ts +15 -0
  19. package/dist/backend/models.js +43 -0
  20. package/dist/backend/paths.d.ts +6 -0
  21. package/dist/backend/paths.js +69 -0
  22. package/dist/backend/stdio.d.ts +66 -0
  23. package/dist/backend/stdio.js +172 -0
  24. package/dist/backend/types.d.ts +90 -0
  25. package/dist/backend/types.js +11 -0
  26. package/dist/backup-api.d.ts +10 -0
  27. package/dist/backup-api.js +40 -0
  28. package/dist/blob-store.d.ts +40 -0
  29. package/dist/blob-store.js +218 -0
  30. package/dist/cli-help.d.ts +1 -0
  31. package/dist/cli-help.js +63 -0
  32. package/dist/cli-output.d.ts +4 -0
  33. package/dist/cli-output.js +130 -0
  34. package/dist/cli.d.ts +2 -0
  35. package/dist/cli.js +284 -0
  36. package/dist/client-pairing.d.ts +8 -0
  37. package/dist/client-pairing.js +30 -0
  38. package/dist/directories.d.ts +14 -0
  39. package/dist/directories.js +34 -0
  40. package/dist/errors.d.ts +12 -0
  41. package/dist/errors.js +7 -0
  42. package/dist/events.d.ts +22 -0
  43. package/dist/events.js +155 -0
  44. package/dist/execution-chains.d.ts +24 -0
  45. package/dist/execution-chains.js +231 -0
  46. package/dist/execution-settings.d.ts +10 -0
  47. package/dist/execution-settings.js +38 -0
  48. package/dist/graph-document-history.d.ts +43 -0
  49. package/dist/graph-document-history.js +275 -0
  50. package/dist/graph-files.d.ts +14 -0
  51. package/dist/graph-files.js +59 -0
  52. package/dist/graph-transfer.d.ts +53 -0
  53. package/dist/graph-transfer.js +501 -0
  54. package/dist/graphs.d.ts +68 -0
  55. package/dist/graphs.js +771 -0
  56. package/dist/history.d.ts +20 -0
  57. package/dist/history.js +61 -0
  58. package/dist/http.d.ts +2 -0
  59. package/dist/http.js +42 -0
  60. package/dist/image-credentials.d.ts +11 -0
  61. package/dist/image-credentials.js +130 -0
  62. package/dist/image-providers.d.ts +51 -0
  63. package/dist/image-providers.js +271 -0
  64. package/dist/input-materialization.d.ts +17 -0
  65. package/dist/input-materialization.js +95 -0
  66. package/dist/inputs.d.ts +74 -0
  67. package/dist/inputs.js +301 -0
  68. package/dist/local-control.d.ts +13 -0
  69. package/dist/local-control.js +37 -0
  70. package/dist/network.d.ts +11 -0
  71. package/dist/network.js +48 -0
  72. package/dist/openai-image-driver.d.ts +42 -0
  73. package/dist/openai-image-driver.js +368 -0
  74. package/dist/operations/backups.d.ts +40 -0
  75. package/dist/operations/backups.js +391 -0
  76. package/dist/operations/capacity.d.ts +7 -0
  77. package/dist/operations/capacity.js +35 -0
  78. package/dist/operations/cli-context.d.ts +7 -0
  79. package/dist/operations/cli-context.js +58 -0
  80. package/dist/operations/daemon.d.ts +36 -0
  81. package/dist/operations/daemon.js +159 -0
  82. package/dist/operations/files.d.ts +12 -0
  83. package/dist/operations/files.js +84 -0
  84. package/dist/operations/lifecycle.d.ts +28 -0
  85. package/dist/operations/lifecycle.js +155 -0
  86. package/dist/operations/local-admin.d.ts +6 -0
  87. package/dist/operations/local-admin.js +38 -0
  88. package/dist/operations/logs.d.ts +16 -0
  89. package/dist/operations/logs.js +37 -0
  90. package/dist/operations/plugins.d.ts +10 -0
  91. package/dist/operations/plugins.js +11 -0
  92. package/dist/operations/snapshot-worker.d.ts +1 -0
  93. package/dist/operations/snapshot-worker.js +18 -0
  94. package/dist/operations/snapshot.d.ts +1 -0
  95. package/dist/operations/snapshot.js +14 -0
  96. package/dist/operations/update.d.ts +26 -0
  97. package/dist/operations/update.js +216 -0
  98. package/dist/persistence/asset-business-schema.d.ts +2 -0
  99. package/dist/persistence/asset-business-schema.js +36 -0
  100. package/dist/persistence/database.d.ts +7 -0
  101. package/dist/persistence/database.js +91 -0
  102. package/dist/persistence/plugin-schema.d.ts +9 -0
  103. package/dist/persistence/plugin-schema.js +38 -0
  104. package/dist/persistence/repositories.d.ts +20 -0
  105. package/dist/persistence/repositories.js +106 -0
  106. package/dist/persistence/resource-schema.d.ts +2 -0
  107. package/dist/persistence/resource-schema.js +44 -0
  108. package/dist/persistence/schema.d.ts +6 -0
  109. package/dist/persistence/schema.js +236 -0
  110. package/dist/persistence/workflow-schema.d.ts +2 -0
  111. package/dist/persistence/workflow-schema.js +19 -0
  112. package/dist/plugin-api.d.ts +10 -0
  113. package/dist/plugin-api.js +40 -0
  114. package/dist/plugin-management.d.ts +36 -0
  115. package/dist/plugin-management.js +112 -0
  116. package/dist/plugins.d.ts +55 -0
  117. package/dist/plugins.js +239 -0
  118. package/dist/project-discovery.d.ts +3 -0
  119. package/dist/project-discovery.js +57 -0
  120. package/dist/project-files-api.d.ts +16 -0
  121. package/dist/project-files-api.js +243 -0
  122. package/dist/project-files.d.ts +97 -0
  123. package/dist/project-files.js +557 -0
  124. package/dist/project-lock.d.ts +4 -0
  125. package/dist/project-lock.js +35 -0
  126. package/dist/project-probe-worker.d.ts +1 -0
  127. package/dist/project-probe-worker.js +17 -0
  128. package/dist/project-probe.d.ts +22 -0
  129. package/dist/project-probe.js +67 -0
  130. package/dist/projects.d.ts +29 -0
  131. package/dist/projects.js +219 -0
  132. package/dist/publication.d.ts +31 -0
  133. package/dist/publication.js +642 -0
  134. package/dist/resource-api.d.ts +15 -0
  135. package/dist/resource-api.js +256 -0
  136. package/dist/resources-orphans.d.ts +41 -0
  137. package/dist/resources-orphans.js +235 -0
  138. package/dist/resources.d.ts +246 -0
  139. package/dist/resources.js +714 -0
  140. package/dist/run-api.d.ts +10 -0
  141. package/dist/run-api.js +308 -0
  142. package/dist/run-context.d.ts +54 -0
  143. package/dist/run-context.js +137 -0
  144. package/dist/runs.d.ts +82 -0
  145. package/dist/runs.js +506 -0
  146. package/dist/runtime-backend.d.ts +24 -0
  147. package/dist/runtime-backend.js +214 -0
  148. package/dist/runtime.d.ts +97 -0
  149. package/dist/runtime.js +553 -0
  150. package/dist/scheduler.d.ts +85 -0
  151. package/dist/scheduler.js +344 -0
  152. package/dist/service.d.ts +29 -0
  153. package/dist/service.js +515 -0
  154. package/dist/skills-api.d.ts +18 -0
  155. package/dist/skills-api.js +120 -0
  156. package/dist/storage-api.d.ts +21 -0
  157. package/dist/storage-api.js +92 -0
  158. package/dist/transfer-api.d.ts +10 -0
  159. package/dist/transfer-api.js +60 -0
  160. package/dist/vendor/protocol/contracts.typecheck.d.ts +1 -0
  161. package/dist/vendor/protocol/contracts.typecheck.js +35 -0
  162. package/dist/vendor/protocol/execution-chain.d.ts +6 -0
  163. package/dist/vendor/protocol/execution-chain.js +26 -0
  164. package/dist/vendor/protocol/graph-binary.d.ts +15 -0
  165. package/dist/vendor/protocol/graph-binary.js +121 -0
  166. package/dist/vendor/protocol/graph-title.d.ts +2 -0
  167. package/dist/vendor/protocol/graph-title.js +23 -0
  168. package/dist/vendor/protocol/index.d.ts +617 -0
  169. package/dist/vendor/protocol/index.js +20 -0
  170. package/dist/vendor/protocol/pairing.d.ts +29 -0
  171. package/dist/vendor/protocol/pairing.js +71 -0
  172. package/dist/vendor/protocol/preview.d.ts +2 -0
  173. package/dist/vendor/protocol/preview.js +9 -0
  174. package/dist/vendor/protocol/resource-mime.d.ts +2 -0
  175. package/dist/vendor/protocol/resource-mime.js +35 -0
  176. package/dist/video-thumbnail.d.ts +2 -0
  177. package/dist/video-thumbnail.js +34 -0
  178. package/package.json +9 -0
@@ -0,0 +1,10 @@
1
+ import type { IncomingMessage } from 'node:http';
2
+ import type { ApiResult } from './api.js';
3
+ import { Auth } from './auth.js';
4
+ import { WorkflowRuntime } from './runtime.js';
5
+ export declare class RunApi {
6
+ readonly runtime: WorkflowRuntime;
7
+ readonly auth: Auth;
8
+ constructor(runtime: WorkflowRuntime, auth: Auth);
9
+ handle(request: IncomingMessage, path: string, token: string, origin: string): Promise<ApiResult>;
10
+ }
@@ -0,0 +1,308 @@
1
+ import { Auth } from './auth.js';
2
+ import { WorkflowRuntime } from './runtime.js';
3
+ import { readJson } from './http.js';
4
+ import { ServiceError } from './errors.js';
5
+ import { Repositories, canonicalJson } from './persistence/repositories.js';
6
+ import { BackendError } from './backend/types.js';
7
+ function text(value) { if (typeof value !== 'string' || !value)
8
+ throw new ServiceError('INVALID_REQUEST', '需要非空字符串。'); return value; }
9
+ function fields(body, allowed) { if (Object.keys(body).some(key => !allowed.includes(key)))
10
+ throw new ServiceError('INVALID_REQUEST', '请求包含未知字段。'); }
11
+ function requireSecureCredentialConnection(request, origin) {
12
+ const web = new URL(origin), host = request.headers.host ?? '';
13
+ const remote = request.socket.remoteAddress ?? '';
14
+ const local = (value) => ['localhost', '127.0.0.1', '::1', '::ffff:127.0.0.1', '[::1]'].includes(value);
15
+ const hostName = host.startsWith('[') ? host.slice(0, host.indexOf(']') + 1) : host.split(':')[0];
16
+ const forwarded = request.headers['x-forwarded-proto'];
17
+ const forwardedProto = (Array.isArray(forwarded) ? forwarded[0] : forwarded)?.split(',')[0]?.trim().toLowerCase();
18
+ const secure = (request.socket.encrypted === true || forwardedProto === 'https') && web.protocol === 'https:';
19
+ if (!secure && !(web.protocol === 'http:' && local(web.hostname) && local(remote) && local(hostName)))
20
+ throw new ServiceError('HOST_DENIED', '密钥只能通过回环连接或 HTTPS 录入。');
21
+ }
22
+ function imageRoute(body) {
23
+ if (body.kind !== 'image_generation') {
24
+ if ('imageRoute' in body)
25
+ throw new ServiceError('INVALID_REQUEST', '只有图片任务可以指定生成方式。');
26
+ return undefined;
27
+ }
28
+ if (body.imageRoute === undefined)
29
+ return { type: 'codex' }; // Old clients always mean Codex.
30
+ const route = body.imageRoute;
31
+ if (!route || typeof route !== 'object' || Array.isArray(route))
32
+ throw new ServiceError('INVALID_REQUEST', '图片生成方式无效。');
33
+ const value = route;
34
+ const parseOptions = () => {
35
+ if (value.options === undefined)
36
+ return undefined;
37
+ if (!value.options || typeof value.options !== 'object' || Array.isArray(value.options))
38
+ throw new ServiceError('INVALID_REQUEST', '图片参数无效。');
39
+ const raw = value.options;
40
+ fields(raw, ['size', 'quality', 'aspectRatio', 'outputFormat']);
41
+ for (const key of ['size', 'aspectRatio'])
42
+ if (raw[key] !== undefined && (typeof raw[key] !== 'string' || !raw[key].trim() || raw[key].length > 60))
43
+ throw new ServiceError('INVALID_REQUEST', '图片参数无效。');
44
+ if (raw.quality !== undefined && !['auto', 'high', 'medium', 'low'].includes(String(raw.quality)))
45
+ throw new ServiceError('INVALID_REQUEST', '图片质量无效。');
46
+ if (raw.outputFormat !== undefined && !['png', 'jpeg', 'webp'].includes(String(raw.outputFormat)))
47
+ throw new ServiceError('INVALID_REQUEST', '图片格式无效。');
48
+ return raw;
49
+ };
50
+ if (value.type === 'codex') {
51
+ fields(value, ['type', 'options']);
52
+ const options = parseOptions();
53
+ return { type: 'codex', ...(options ? { options } : {}) };
54
+ }
55
+ if (value.type !== 'api')
56
+ throw new ServiceError('INVALID_REQUEST', '图片生成方式无效。');
57
+ fields(value, ['type', 'providerId', 'modelId', 'options']);
58
+ if (typeof value.providerId !== 'string' || !value.providerId.trim() || typeof value.modelId !== 'string' || !value.modelId.trim())
59
+ throw new ServiceError('INVALID_REQUEST', '需要有效的服务商和模型 ID。');
60
+ if ('modelOverride' in body || 'reasoningEffort' in body)
61
+ throw new ServiceError('INVALID_REQUEST', 'API 图片任务不能使用 Codex 模型或推理强度。');
62
+ const options = parseOptions();
63
+ return { type: 'api', providerId: value.providerId, modelId: value.modelId, ...(options ? { options } : {}) };
64
+ }
65
+ export class RunApi {
66
+ runtime;
67
+ auth;
68
+ constructor(runtime, auth) {
69
+ this.runtime = runtime;
70
+ this.auth = auth;
71
+ }
72
+ async handle(request, path, token, origin) {
73
+ const graph = /^[/]v1[/]projects[/]([a-zA-Z0-9_-]+)[/]graphs[/]([a-zA-Z0-9_-]+)[/](runs|input-preview)$/.exec(path);
74
+ const run = /^[/]v1[/]runs(?:[/]([a-zA-Z0-9_-]+))?(?:[/](cancel|cancel-chain|reply|history|snapshot|public-metadata|interactions|active-interaction|candidate|continue|input-changes|execution-plan|start))?$/.exec(path);
75
+ const notifications = path === '/v1/notifications';
76
+ const notificationRead = /^[/]v1[/]notifications[/]([a-zA-Z0-9_-]+)[/]read$/.exec(path);
77
+ const notificationReadAll = path === '/v1/notifications/read-all';
78
+ const history = /^[/]v1[/]history[/](preview|clear)$/.exec(path);
79
+ const providers = /^[/]v1[/]image-providers(?:[/]([a-zA-Z0-9_-]+)(?:[/](.+))?)?$/.exec(path);
80
+ if (!graph && !run && !history && !providers && !notifications && !notificationRead && !notificationReadAll && !['/v1/models', '/v1/capacity', '/v1/execution-settings'].includes(path))
81
+ return { handled: false };
82
+ const principal = this.auth.withSession(token, origin, s => s.id);
83
+ const body = request.method === 'POST' ? await readJson(request, 2 * 1024 * 1024) : {};
84
+ const read = (work) => this.auth.withSession(token, origin, work);
85
+ const ok = (value) => ({ handled: true, body: value });
86
+ const rt = this.runtime, repo = new Repositories(rt.db);
87
+ if (notifications && request.method === 'GET')
88
+ return ok(read(() => rt.runs.notifications()));
89
+ if (notificationRead && request.method === 'POST') {
90
+ fields(body, ['expectedRevision']);
91
+ return ok(read(() => rt.runs.readNotification(notificationRead[1], body.expectedRevision)));
92
+ }
93
+ if (notificationReadAll && request.method === 'POST') {
94
+ fields(body, []);
95
+ return ok(read(() => rt.runs.readAllNotifications()));
96
+ }
97
+ if (providers) {
98
+ if (request.method === 'GET' && !providers[1]) {
99
+ await rt.imageProviders.refreshCredentials();
100
+ return ok(read(() => rt.imageProviders.list()));
101
+ }
102
+ if (request.method === 'POST') {
103
+ if (providers[1]) {
104
+ if (providers[2] === 'delete') {
105
+ fields(body, ['expectedRevision']);
106
+ return ok(read(() => { rt.imageProviders.deleteProvider(providers[1], body.expectedRevision); return { deleted: true }; }));
107
+ }
108
+ if (providers[2] === 'models/discover') {
109
+ fields(body, ['expectedRevision']);
110
+ if (!Number.isSafeInteger(body.expectedRevision))
111
+ throw new ServiceError('INVALID_REQUEST', '需要有效的修订号。');
112
+ this.auth.assertActive(token, origin);
113
+ const result = await rt.discoverImageModels(providers[1], body.expectedRevision);
114
+ return ok(read(() => result));
115
+ }
116
+ if (providers[2] === 'models/select') {
117
+ fields(body, ['models', 'expectedRevision']);
118
+ if (!Array.isArray(body.models) || !Number.isSafeInteger(body.expectedRevision))
119
+ throw new ServiceError('INVALID_REQUEST', '模型选择无效。');
120
+ return ok(read(() => rt.imageProviders.syncDiscoveredModels(providers[1], body.models, body.expectedRevision)));
121
+ }
122
+ if (providers[2] === 'models/delete') {
123
+ fields(body, ['modelId', 'expectedRevision']);
124
+ if (typeof body.modelId !== 'string')
125
+ throw new ServiceError('INVALID_REQUEST', '图片模型 ID 无效。');
126
+ return ok(read(() => rt.imageProviders.deleteModel(providers[1], body.modelId, body.expectedRevision)));
127
+ }
128
+ const modelAction = /^models[/]([^/]+)[/]delete$/.exec(providers[2] ?? '');
129
+ let actionModel;
130
+ if (modelAction) {
131
+ try {
132
+ actionModel = decodeURIComponent(modelAction[1]);
133
+ }
134
+ catch {
135
+ throw new ServiceError('INVALID_REQUEST', '图片模型 ID 无效。');
136
+ }
137
+ }
138
+ if (modelAction) {
139
+ fields(body, ['expectedRevision']);
140
+ return ok(read(() => rt.imageProviders.deleteModel(providers[1], actionModel, body.expectedRevision)));
141
+ }
142
+ if (providers[2] === 'credential' || providers[2] === 'credential-revoke') {
143
+ fields(body, providers[2] === 'credential' ? ['secret', 'expectedRevision'] : ['expectedRevision']);
144
+ requireSecureCredentialConnection(request, origin);
145
+ if (!Number.isSafeInteger(body.expectedRevision))
146
+ throw new ServiceError('INVALID_REQUEST', '需要有效的修订号。');
147
+ this.auth.assertActive(token, origin);
148
+ const result = providers[2] === 'credential'
149
+ ? await rt.imageProviders.setCredential(providers[1], body.secret, body.expectedRevision)
150
+ : await rt.imageProviders.revokeCredential(providers[1], body.expectedRevision);
151
+ return ok(read(() => result));
152
+ }
153
+ fields(body, ['id', 'name', 'modes', 'formats', 'sizes', 'qualities', 'isDefault', 'expectedRevision']);
154
+ return ok(read(() => rt.imageProviders.setModel(providers[1], body, body.expectedRevision)));
155
+ }
156
+ fields(body, ['id', 'name', 'driver', 'endpoint', 'enabled', 'expectedRevision']);
157
+ return ok(read(() => rt.imageProviders.setProvider(body, body.expectedRevision)));
158
+ }
159
+ }
160
+ if (path === '/v1/execution-settings') {
161
+ if (request.method === 'GET')
162
+ return ok(read(() => rt.executionSettings.get()));
163
+ if (request.method === 'POST') {
164
+ fields(body, ['sandboxMode', 'expectedRevision']);
165
+ return ok(read(() => rt.executionSettings.set(body.sandboxMode, body.expectedRevision)));
166
+ }
167
+ }
168
+ if (path === '/v1/models') {
169
+ if (request.method === 'GET') {
170
+ await rt.refreshModels();
171
+ return ok(read(() => rt.models.get(rt.catalog)));
172
+ }
173
+ if (request.method === 'POST') {
174
+ fields(body, ['selection', 'expectedRevision']);
175
+ const selection = body.selection;
176
+ if (!selection || typeof selection !== 'object' || Array.isArray(selection) || typeof selection.model !== 'string' || !selection.model || !(selection.reasoningEffort === null || typeof selection.reasoningEffort === 'string') || Object.keys(selection).some(k => !['model', 'reasoningEffort'].includes(k)) || !Number.isSafeInteger(body.expectedRevision))
177
+ throw new ServiceError('INVALID_REQUEST', '模型配置无效。');
178
+ try {
179
+ return ok(read(() => rt.models.set(selection, body.expectedRevision, rt.catalog)));
180
+ }
181
+ catch (error) {
182
+ if (error instanceof BackendError)
183
+ throw new ServiceError(error.code === 'CONFLICT' ? 'REVISION_CONFLICT' : 'MODEL_UNAVAILABLE', error.message);
184
+ throw error;
185
+ }
186
+ }
187
+ }
188
+ if (path === '/v1/capacity' && request.method === 'GET')
189
+ return ok(read(() => rt.runs.capacity()));
190
+ if (graph) {
191
+ const scope = { serviceId: rt.serviceId, projectId: graph[1], graphId: graph[2] };
192
+ read(() => rt.graphs.scope(scope));
193
+ if (graph[3] === 'input-preview' && request.method === 'POST') {
194
+ fields(body, ['nodeId', 'excludeFiles']);
195
+ if (body.excludeFiles !== undefined && typeof body.excludeFiles !== 'boolean')
196
+ throw new ServiceError('INVALID_REQUEST', '无效输入预检选项');
197
+ return ok(read(() => rt.inputs.inputPreview(scope.graphId, text(body.nodeId), body.excludeFiles === true)));
198
+ }
199
+ if (graph[3] === 'runs' && request.method === 'GET')
200
+ return ok(read(() => rt.runs.list(scope.projectId).filter(value => value.graphId === scope.graphId)));
201
+ if (graph[3] === 'runs' && request.method === 'POST') {
202
+ fields(body, ['nodeId', 'kind', 'idempotencyKey', 'expectedExecutionRevision', 'modelOverride', 'imageRoute']);
203
+ const route = imageRoute(body);
204
+ const input = { ...body, ...scope, ...(route ? { imageRoute: route } : {}) };
205
+ text(input.nodeId);
206
+ text(input.idempotencyKey);
207
+ let prior;
208
+ try {
209
+ prior = read(() => repo.replay(principal + ':run.submit', input.idempotencyKey, input));
210
+ }
211
+ catch (error) {
212
+ // Old persisted request hashes predate imageRoute. Only retry the exact old
213
+ // body for a legacy image request; all other key collisions still fail.
214
+ if (!(error instanceof ServiceError && error.code === 'IDEMPOTENCY_CONFLICT' && body.kind === 'image_generation' && !('imageRoute' in body)))
215
+ throw error;
216
+ prior = read(() => repo.replay(principal + ':run.submit', input.idempotencyKey, { ...body, ...scope }));
217
+ }
218
+ if (prior !== undefined)
219
+ return ok(prior && typeof prior === 'object' && !Array.isArray(prior) && 'executionStart' in prior ? read(() => rt.runs.get(String(prior.id))) : prior);
220
+ const commit = await rt.prepareSubmission(input);
221
+ return ok(read(() => commit(principal)));
222
+ }
223
+ }
224
+ if (history && request.method === 'POST') {
225
+ fields(body, history[1] === 'clear' ? ['runIds', 'idempotencyKey'] : ['runIds']);
226
+ return ok(read(() => history[1] === 'preview' ? rt.history.preview(body.runIds) : rt.history.clear(body.runIds, principal + ':' + text(body.idempotencyKey))));
227
+ }
228
+ if (run) {
229
+ const id = run[1], action = run[2];
230
+ if (request.method === 'GET' && !id)
231
+ return ok(read(() => rt.runs.list()));
232
+ if (id) {
233
+ const current = read(() => rt.runs.get(id));
234
+ if (request.method === 'GET' && action === 'execution-plan')
235
+ return ok(read(() => rt.executionChains.forRun(id)));
236
+ if (request.method === 'POST' && action === 'start') {
237
+ fields(body, ['nodeIds', 'expectedExecutionRevision', 'idempotencyKey']);
238
+ const key = text(body.idempotencyKey);
239
+ if (!Array.isArray(body.nodeIds) || body.nodeIds.some(v => typeof v !== 'string') || !Number.isSafeInteger(body.expectedExecutionRevision))
240
+ throw new ServiceError('INVALID_REQUEST', '无效执行范围。');
241
+ const data = { nodeIds: body.nodeIds, expectedExecutionRevision: body.expectedExecutionRevision };
242
+ const prior = read(() => repo.replay(principal + ':run.start:' + id, key, data));
243
+ if (prior !== undefined)
244
+ return ok(read(() => rt.runs.get(id)));
245
+ const commit = await rt.prepareExecutionStart(id, body.nodeIds, body.expectedExecutionRevision);
246
+ return ok(read(() => commit(principal, key)));
247
+ }
248
+ if (request.method === 'GET' && !action)
249
+ return ok(current);
250
+ if (request.method === 'GET' && action === 'history')
251
+ return ok(read(() => rt.history.records(id)));
252
+ if (request.method === 'GET' && action === 'active-interaction')
253
+ return ok(read(() => rt.runs.activeInteraction(id)));
254
+ if (request.method === 'GET' && action === 'snapshot')
255
+ return ok(read(() => rt.runs.snapshot(id)));
256
+ if (request.method === 'GET' && action === 'public-metadata')
257
+ return ok(read(() => {
258
+ const snapshot = rt.runs.snapshot(id), details = rt.runs.runtime(id).details;
259
+ const result = details.result;
260
+ const publicId = result && typeof result === 'object' && !Array.isArray(result) ? result.requestId : null;
261
+ return { imageRoute: snapshot.imageRoute ?? { type: 'codex' }, inputMode: snapshot.inputMode ?? null, requestId: typeof publicId === 'string' && /^[a-zA-Z0-9_-]{1,100}$/.test(publicId) ? publicId : null };
262
+ }));
263
+ if (request.method === 'GET' && action === 'input-changes')
264
+ return ok(read(() => {
265
+ // Compare editable reference inputs with the submission snapshot; execution
266
+ // outputs are bound later and must not look like user edits on every poll.
267
+ const snapshot = JSON.parse(String(rt.db.prepare('SELECT payload FROM snapshots WHERE run_id=?').get(id).payload)), preview = rt.inputs.inputPreview(current.graphId, current.nodeId);
268
+ if (preview.issues.length)
269
+ return { state: 'unknown', issues: preview.issues };
270
+ const before = canonicalJson({ prompt: snapshot.prompt, resources: snapshot.resources, projectFiles: snapshot.projectFiles ?? [] });
271
+ const after = canonicalJson({ prompt: preview.prompt, resources: preview.resources, projectFiles: preview.projectFiles });
272
+ const dependencies = rt.runs.runtime(id).details.chainDependencies;
273
+ const bound = Array.isArray(dependencies) ? dependencies.map(runId => rt.runs.get(String(runId)).nodeId).sort() : null;
274
+ const live = bound ? rt.graphs.snapshot(current).edges.filter(e => e.kind === 'execution' && e.targetId === current.nodeId).map(e => e.sourceId).sort() : null;
275
+ return { state: before === after && JSON.stringify(bound) === JSON.stringify(live) ? 'unchanged' : 'changed', issues: [] };
276
+ }));
277
+ if (request.method === 'GET' && action === 'interactions')
278
+ return ok(read(() => rt.db.prepare('SELECT id FROM interactions WHERE run_id=? ORDER BY rowid').all(id).map(row => rt.runs.interaction(String(row.id)))));
279
+ if (request.method === 'GET' && action === 'candidate')
280
+ return ok(read(() => { const row = rt.db.prepare('SELECT base_version,content,state FROM generation_candidates WHERE run_id=?').get(id); return row ? { baseVersion: row.base_version, content: JSON.parse(String(row.content)), state: row.state } : null; }));
281
+ if (request.method === 'POST' && action === 'cancel') {
282
+ fields(body, ['idempotencyKey']);
283
+ return ok(read(() => rt.runs.cancel(id, text(body.idempotencyKey), principal)));
284
+ }
285
+ if (request.method === 'POST' && action === 'cancel-chain') {
286
+ fields(body, ['idempotencyKey']);
287
+ return ok(read(() => rt.executionChains.cancel(id, text(body.idempotencyKey), principal)));
288
+ }
289
+ if (request.method === 'POST' && action === 'continue') {
290
+ fields(body, ['idempotencyKey']);
291
+ const key = text(body.idempotencyKey);
292
+ const commit = await rt.prepareContinuation(id);
293
+ return ok(read(() => commit(principal, key)));
294
+ }
295
+ if (request.method === 'POST' && action === 'reply') {
296
+ fields(body, ['interactionId', 'epoch', 'expectedVersion', 'idempotencyKey', 'answer']);
297
+ return ok(read(() => rt.runs.reply({ ...body, runId: id }, principal)));
298
+ }
299
+ if (request.method === 'POST' && action === 'candidate') {
300
+ fields(body, ['idempotencyKey', 'decision', 'expectedContentVersion']);
301
+ return ok(read(() => repo.idempotent(principal + ':candidate:' + id, text(body.idempotencyKey), body, () => rt.publication.decide({ ...current, runId: id, decision: body.decision, expectedContentVersion: body.expectedContentVersion }))));
302
+ }
303
+ }
304
+ }
305
+ throw new ServiceError('NOT_FOUND', '运行接口不存在。');
306
+ }
307
+ }
308
+ //# sourceMappingURL=run-api.js.map
@@ -0,0 +1,54 @@
1
+ import type { Run } from "./vendor/protocol/index.js";
2
+ import { Runs } from './runs.js';
3
+ import { type DataDirectories } from './directories.js';
4
+ export interface RunContextRequest {
5
+ runId: string;
6
+ section: 'summary' | 'snapshot' | 'history' | 'outputs' | 'output';
7
+ outputKey?: string;
8
+ offset?: number;
9
+ }
10
+ export interface RunLineage {
11
+ version: 1;
12
+ projectId: string;
13
+ rootRunId: string;
14
+ nodes: RunDescription[];
15
+ edges: {
16
+ sourceRunId: string;
17
+ targetRunId: string;
18
+ }[];
19
+ }
20
+ interface RunDescription {
21
+ runId: string;
22
+ nodeId: string;
23
+ graphId: string;
24
+ status: Run['status'];
25
+ inputDigest: string;
26
+ historyState: Run['historyState'];
27
+ predecessorRunIds: string[];
28
+ lineageRecorded: boolean;
29
+ outputs: ReturnType<RunContexts['outputs']>;
30
+ nodeSnapshot: {
31
+ contentVersion: number | null;
32
+ title: string | null;
33
+ promptPreview: string;
34
+ promptTruncated: boolean;
35
+ executionRevision: number;
36
+ };
37
+ }
38
+ /** Run identities, not live graph edges, define historical ancestry. */
39
+ export declare class RunContexts {
40
+ readonly runs: Runs;
41
+ readonly dirs: DataDirectories;
42
+ private readonly copies;
43
+ constructor(runs: Runs, dirs: DataDirectories);
44
+ private scoped;
45
+ describe(run: Run): RunDescription;
46
+ lineage(runId: string): RunLineage;
47
+ outputs(run: Run): {
48
+ canvas: Record<string, import("node:sqlite").SQLOutputValue>[];
49
+ projectFiles: Record<string, import("node:sqlite").SQLOutputValue>[];
50
+ projectFileNotice: string;
51
+ };
52
+ read(callerRunId: string, request: RunContextRequest): Promise<string>;
53
+ }
54
+ export {};
@@ -0,0 +1,137 @@
1
+ import { Runs } from './runs.js';
2
+ import { ServiceError } from './errors.js';
3
+ import { materializeInputs } from './input-materialization.js';
4
+ import { runDirectories } from './directories.js';
5
+ /** Run identities, not live graph edges, define historical ancestry. */
6
+ export class RunContexts {
7
+ runs;
8
+ dirs;
9
+ copies = new Map();
10
+ constructor(runs, dirs) {
11
+ this.runs = runs;
12
+ this.dirs = dirs;
13
+ }
14
+ scoped(caller, id) {
15
+ if (typeof id !== 'string' || !id || id.length > 200)
16
+ throw new ServiceError('INVALID_REQUEST', 'A valid Run ID is required');
17
+ if (!this.runs.db.prepare('SELECT 1 FROM runs WHERE id=? AND project_id=?').get(id, caller.projectId))
18
+ throw new ServiceError('NOT_FOUND', 'Run is unavailable in the current project');
19
+ return this.runs.get(id);
20
+ }
21
+ describe(run) {
22
+ const details = this.runs.runtime(run.id).details;
23
+ const dependencies = details.chainDependencies;
24
+ if (dependencies !== undefined && (!Array.isArray(dependencies) || dependencies.some(id => typeof id !== 'string')))
25
+ throw new ServiceError('INPUT_BLOCKED', 'Invalid persisted Run lineage');
26
+ const predecessorRunIds = [...new Set((dependencies ?? []))];
27
+ const snapshot = this.runs.snapshot(run.id);
28
+ const version = typeof details.baseVersion === 'number' ? details.baseVersion : null;
29
+ const row = version === null ? undefined : this.runs.db.prepare('SELECT content FROM node_versions WHERE node_id=? AND version=?').get(run.nodeId, version);
30
+ const content = row ? JSON.parse(String(row.content)) : null;
31
+ return {
32
+ runId: run.id, nodeId: run.nodeId, graphId: run.graphId, status: run.status,
33
+ inputDigest: snapshot.inputDigest, historyState: run.historyState, predecessorRunIds, lineageRecorded: dependencies !== undefined, outputs: this.outputs(run),
34
+ nodeSnapshot: { contentVersion: version, title: typeof content?.title === 'string' ? content.title : null,
35
+ promptPreview: snapshot.prompt.slice(0, 2000), promptTruncated: snapshot.prompt.length > 2000, executionRevision: snapshot.executionRevision },
36
+ };
37
+ }
38
+ lineage(runId) {
39
+ const root = this.runs.get(runId);
40
+ const nodes = [], edges = [];
41
+ const visited = new Set(), active = new Set();
42
+ const stack = [{ id: runId, exit: false }];
43
+ while (stack.length) {
44
+ const entry = stack.pop();
45
+ if (entry.exit) {
46
+ active.delete(entry.id);
47
+ visited.add(entry.id);
48
+ continue;
49
+ }
50
+ if (active.has(entry.id))
51
+ throw new ServiceError('INPUT_BLOCKED', 'Persisted Run lineage contains a cycle');
52
+ if (visited.has(entry.id))
53
+ continue;
54
+ if (nodes.length >= 10000)
55
+ throw new ServiceError('INPUT_BUDGET_EXCEEDED', 'Run lineage exceeds 10000 runs');
56
+ const node = this.describe(this.scoped(root, entry.id));
57
+ nodes.push(node);
58
+ active.add(entry.id);
59
+ stack.push({ id: entry.id, exit: true });
60
+ for (const id of node.predecessorRunIds) {
61
+ edges.push({ sourceRunId: id, targetRunId: entry.id });
62
+ stack.push({ id, exit: false });
63
+ }
64
+ }
65
+ return { version: 1, projectId: root.projectId, rootRunId: runId, nodes, edges };
66
+ }
67
+ outputs(run) {
68
+ const canvas = this.runs.db.prepare(`SELECT o.output_key AS outputKey, o.resource_id AS resourceId,
69
+ o.resource_version AS version, v.mime, v.sha256, b.bytes
70
+ FROM canvas_outputs o JOIN canvas_resource_versions v ON v.resource_id=o.resource_id AND v.version=o.resource_version
71
+ JOIN blobs b ON b.sha256=v.sha256 WHERE o.run_id=? AND o.graph_id=? ORDER BY o.output_key`).all(run.id, run.graphId);
72
+ const projectFiles = this.runs.db.prepare(`SELECT output_key AS outputKey, relative_path AS relativePath, bytes, sha256
73
+ FROM project_file_outputs WHERE run_id=? AND project_id=? ORDER BY output_key`).all(run.id, run.projectId);
74
+ return { canvas, projectFiles, projectFileNotice: 'Project file paths refer to the live working tree, not frozen historical file contents. Verify their hashes before assuming they match this Run.' };
75
+ }
76
+ async read(callerRunId, request) {
77
+ if (!request || typeof request !== 'object' || Array.isArray(request) ||
78
+ Object.keys(request).some(key => !['runId', 'section', 'outputKey', 'offset'].includes(key)) ||
79
+ !['summary', 'snapshot', 'history', 'outputs', 'output'].includes(request.section) ||
80
+ !Number.isSafeInteger(request.offset ?? 0) || (request.offset ?? 0) < 0 ||
81
+ (request.section === 'output' ? typeof request.outputKey !== 'string' || !request.outputKey || request.outputKey.length > 1024 : request.outputKey !== undefined))
82
+ throw new ServiceError('INVALID_REQUEST', 'Invalid Run context request');
83
+ const caller = this.runs.get(callerRunId), target = this.scoped(caller, request.runId);
84
+ let value;
85
+ switch (request.section) {
86
+ case 'summary':
87
+ value = this.describe(target);
88
+ break;
89
+ case 'snapshot':
90
+ value = this.runs.snapshot(target.id);
91
+ break;
92
+ case 'outputs':
93
+ value = this.outputs(target);
94
+ break;
95
+ case 'history':
96
+ value = { historyState: target.historyState, records: target.historyState === 'cleared' ? [] :
97
+ this.runs.db.prepare('SELECT id,occurred_at AS occurredAt,kind,payload FROM run_process_records WHERE run_id=? ORDER BY id').all(target.id)
98
+ .map(row => ({ ...row, payload: JSON.parse(String(row.payload)) })) };
99
+ break;
100
+ case 'output': {
101
+ const output = this.outputs(target).canvas.find(item => item.outputKey === request.outputKey);
102
+ if (!output)
103
+ throw new ServiceError('NOT_FOUND', 'Published canvas output unavailable; no newer Run or live project file is substituted');
104
+ const resource = { resourceId: String(output.resourceId), version: Number(output.version), mime: String(output.mime),
105
+ sha256: String(output.sha256), bytes: Number(output.bytes), representationVersion: null };
106
+ const key = JSON.stringify([callerRunId, target.id, request.outputKey, resource.sha256]);
107
+ let copy = this.copies.get(key);
108
+ if (!copy) {
109
+ const snapshot = { inputDigest: '', executionRevision: 0, prompt: '', model: { model: '', reasoningEffort: null },
110
+ resources: [{ kind: 'file', text: null, sourceNodeIds: [target.nodeId], resource }] };
111
+ copy = materializeInputs(snapshot, { inputDirectory: runDirectories(this.dirs, callerRunId).input, blobDirectory: this.dirs.blobs,
112
+ resolveBlobPath: sha => {
113
+ const row = this.runs.db.prepare('SELECT path FROM resource_blob_files WHERE sha256=?').get(sha);
114
+ if (!row)
115
+ throw new ServiceError('NOT_FOUND', 'Published output bytes are unavailable');
116
+ return String(row.path);
117
+ } });
118
+ this.copies.set(key, copy);
119
+ copy.catch(() => this.copies.delete(key));
120
+ }
121
+ const file = (await copy).files[0];
122
+ value = { ...output, path: file.path, notice: 'Verified immutable output copied to the current Run input directory. Read this file using the available file tools.' };
123
+ break;
124
+ }
125
+ }
126
+ // Page serialized data inside a valid JSON envelope; never truncate the wire JSON.
127
+ const serialized = JSON.stringify(value), offset = request.offset ?? 0;
128
+ if (offset > serialized.length)
129
+ throw new ServiceError('INVALID_REQUEST', 'Offset exceeds section length');
130
+ let end = Math.min(offset + 4000, serialized.length);
131
+ if (end < serialized.length && /[\uD800-\uDBFF]/.test(serialized[end - 1]))
132
+ end--;
133
+ return JSON.stringify({ runId: target.id, section: request.section, encoding: 'json-text', offset,
134
+ nextOffset: end < serialized.length ? end : null, totalCharacters: serialized.length, text: serialized.slice(offset, end) });
135
+ }
136
+ }
137
+ //# sourceMappingURL=run-context.js.map
package/dist/runs.d.ts ADDED
@@ -0,0 +1,82 @@
1
+ import type { DatabaseSync } from 'node:sqlite';
2
+ import type { InputSnapshot, Interaction, InteractionReply, Json, Run, RunNotification, RunStatus, SubmitRun, SandboxMode } from "./vendor/protocol/index.js";
3
+ import { Repositories } from './persistence/repositories.js';
4
+ export interface RunContext {
5
+ runId: string;
6
+ projectId: string;
7
+ graphId: string;
8
+ nodeId: string;
9
+ kind: SubmitRun['kind'];
10
+ baseVersion: number;
11
+ canonicalPath: string;
12
+ publicationVersion: 1 | 2;
13
+ sandboxMode?: SandboxMode;
14
+ }
15
+ export interface RunsDependencies {
16
+ /** Synchronous: extraction/preflight caches must already be prepared. */
17
+ freeze: (request: SubmitRun, context: RunContext) => InputSnapshot;
18
+ validate?: (request: SubmitRun, context: RunContext) => void;
19
+ validateReply?: (interaction: Interaction, answer: Json) => void;
20
+ now?: () => number;
21
+ }
22
+ export interface RunToken {
23
+ epoch: string;
24
+ revision: number;
25
+ }
26
+ export interface RunRuntime extends RunToken {
27
+ details: {
28
+ [key: string]: Json;
29
+ };
30
+ }
31
+ /** All authoritative changes are short synchronous transactions (safe inside Auth). */
32
+ export declare class Runs {
33
+ readonly db: DatabaseSync;
34
+ readonly serviceId: string;
35
+ readonly dependencies: RunsDependencies;
36
+ readonly repo: Repositories;
37
+ readonly now: () => number;
38
+ constructor(db: DatabaseSync, serviceId: string, dependencies: RunsDependencies);
39
+ get(id: string): Run;
40
+ list(projectId?: string): Run[];
41
+ notifications(): RunNotification[];
42
+ readNotification(id: string, expectedRevision: number): {
43
+ read: boolean;
44
+ };
45
+ readAllNotifications(): {
46
+ read: number;
47
+ };
48
+ runtime(id: string): RunRuntime;
49
+ snapshot(id: string): InputSnapshot;
50
+ private event;
51
+ private refreshNotification;
52
+ record(id: string, kind: string, payload: Json): void;
53
+ submit(request: SubmitRun, principal?: string, holdExecution?: boolean): Run;
54
+ capacity(): {
55
+ capacity: number;
56
+ occupied: number;
57
+ available: number;
58
+ };
59
+ /** ExecutionChains enqueues only dependency-ready runs. Claim those in persisted
60
+ * order up to capacity, without serializing unrelated nodes by project or batch. */
61
+ claimNext(epoch: string, excludedProjectIds?: readonly string[], eligible?: (run: Run, snapshot: InputSnapshot) => boolean): Run | null;
62
+ /** Persist the uncertain spawn boundary BEFORE calling an external backend. */
63
+ markBackendLaunching(id: string, token: RunToken): RunToken;
64
+ /** Only a known lock-contention result BEFORE the spawn boundary can requeue.
65
+ * No generic preparing -> queued transition is allowed after uncertain launch. */
66
+ requeueUnstarted(id: string, token: RunToken): Run;
67
+ transition(id: string, token: RunToken, next: RunStatus, details?: {
68
+ [key: string]: Json;
69
+ }): Run;
70
+ cancel(id: string, key: string, principal?: string): Run;
71
+ interaction(id: string): Interaction;
72
+ /** Oldest independent action whose reply has not been acknowledged by its responder. */
73
+ activeInteraction(id: string): Interaction | null;
74
+ acknowledgeReply(id: string, epoch: string, interactionId: string): Run;
75
+ openInteraction(id: string, token: RunToken, kind: Interaction['kind'], payload: Json, interactionId?: string): Interaction;
76
+ private expireInteractions;
77
+ reply(request: InteractionReply, sessionId: string, principal?: string): Interaction;
78
+ /** Caller must first establish previous service ownership is stale (not just a PID check). */
79
+ recover(epoch: string): Run[];
80
+ /** A new service instance must not replay or retain ownership of old work. */
81
+ abandonOnRestart(epoch: string): Run[];
82
+ }