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/README.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# OpenWorkgraph
|
|
2
|
+
|
|
3
|
+
OpenWorkgraph 是基于无限画布思想的软件开发工作流产品,同时面向其他领域的工作组织与协作。
|
|
4
|
+
|
|
5
|
+
## 快速开始
|
|
6
|
+
|
|
7
|
+
打开 [OpenWorkgraph 官方编辑器](https://workgraph.giarld.com/),按照页面提供的引导完成配置即可开始使用。
|
|
8
|
+
|
|
9
|
+
## 本地构建与运行
|
|
10
|
+
|
|
11
|
+
### Agent Service
|
|
12
|
+
|
|
13
|
+
在仓库根目录构建协议包和 Agent Service:
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
npm run build
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
构建完成后,以前台方式运行 Agent Service:
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
node agent-service/dist/cli.js start
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Web Client
|
|
26
|
+
|
|
27
|
+
在仓库根目录构建 Web Client,并通过 Vite Preview 在 `http://127.0.0.1:4173` 运行生产构建:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
npm --prefix web-client run build
|
|
31
|
+
npm --prefix web-client run preview -- --host 127.0.0.1 --port 4173
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
打开 Web Client 后,填写运行时地址并点击“生成客户端码”。在运行时所在机器执行以下命令,输入浏览器显示的 8 位客户端码,核对来源和公钥指纹后确认;浏览器会轮询批准状态,完成私钥证明和通讯凭据交换后自动连接:
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
node agent-service/dist/cli.js pair --client-code '<客户端码>'
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
客户端码显示为连续 8 位数字,例如 `12345678`,中间没有空格,5 分钟有效。终端默认输出英文可读的批准结果;仅脚本需要解析时加 `--output-json`(非交互批准还需 `--yes`)。
|
|
41
|
+
|
|
42
|
+
一机只运行一个服务。`--data-dir` 仅用于 `start` 和 `serve`;启动成功后自动记住目录,配对、查看状态、停止、日志和备份命令都无需再填写。停服后运行 `start` 会沿用原目录,首次启动未指定时使用用户主目录下的 `.openworkgraph`。重复启动会明确拒绝,不提供服务选择或切换步骤。
|
|
43
|
+
|
|
44
|
+
无需手填 `--origin`,不强制 HTTPS,支持可信局域网普通 HTTP。HTTP 不加密,请勿暴露公网。
|
|
45
|
+
|
|
46
|
+
### 检查与测试
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
npm test
|
|
50
|
+
npm --prefix web-client run typecheck
|
|
51
|
+
npm --prefix web-client test
|
|
52
|
+
npm --prefix web-client run build
|
|
53
|
+
npm --prefix web-client run test:e2e
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
以上命令在仓库根目录执行;Web 构建输出为 `web-client/dist/`,浏览器测试使用已安装的 Chrome。
|
|
57
|
+
|
|
58
|
+
## 参考依据与致谢
|
|
59
|
+
|
|
60
|
+
Web UI 参考了 [basketikun/infinite-canvas](https://github.com/basketikun/infinite-canvas) 的视觉风格和画布交互,感谢原作者 basketikun。该项目采用 [MIT License](https://github.com/basketikun/infinite-canvas/blob/d213a74614e0e4bd8a26383d1e1e907249e9c61b/LICENSE);复用其代码或其他适用内容时,应保留原版权与许可声明。第三方素材若有独立授权,以其各自许可为准。
|
|
61
|
+
|
|
62
|
+
## 许可证
|
|
63
|
+
|
|
64
|
+
本项目采用 [MIT License](LICENSE)。第三方来源的版权与许可声明按原始归属保留。
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { InputSnapshot, Interaction, Json, Run } from "./vendor/protocol/index.js";
|
|
2
|
+
import type { SchedulerBackend, BackendContext, ReconcileResult } from './scheduler.js';
|
|
3
|
+
import { Runs } from './runs.js';
|
|
4
|
+
import { Resources } from './resources.js';
|
|
5
|
+
import { ImageProviders } from './image-providers.js';
|
|
6
|
+
import { ImageCredentials } from './image-credentials.js';
|
|
7
|
+
import { type DataDirectories } from './directories.js';
|
|
8
|
+
/** No project file/tool access: only the accepted Run's immutable canvas blobs,
|
|
9
|
+
* its empty output directory and the registered frozen provider API base URL. */
|
|
10
|
+
export declare class ApiImageAgent implements SchedulerBackend {
|
|
11
|
+
readonly runs: Runs;
|
|
12
|
+
readonly resources: Resources;
|
|
13
|
+
readonly providers: ImageProviders;
|
|
14
|
+
readonly credentials: ImageCredentials;
|
|
15
|
+
readonly dirs: DataDirectories;
|
|
16
|
+
private readonly pending;
|
|
17
|
+
constructor(runs: Runs, resources: Resources, providers: ImageProviders, credentials: ImageCredentials, dirs: DataDirectories);
|
|
18
|
+
start(run: Run, snapshot: InputSnapshot, context: BackendContext): Promise<void>;
|
|
19
|
+
cancel(run: Run, context: BackendContext): Promise<'confirmed' | 'unknown'>;
|
|
20
|
+
reconcile(run: Run, _context: BackendContext): Promise<ReconcileResult>;
|
|
21
|
+
}
|
|
22
|
+
export declare class RoutedImageBackend implements SchedulerBackend {
|
|
23
|
+
readonly codex: SchedulerBackend;
|
|
24
|
+
readonly api: ApiImageAgent;
|
|
25
|
+
readonly runs: Runs;
|
|
26
|
+
constructor(codex: SchedulerBackend, api: ApiImageAgent, runs: Runs);
|
|
27
|
+
private delegate;
|
|
28
|
+
start(run: Run, snapshot: InputSnapshot, context: BackendContext): Promise<void>;
|
|
29
|
+
cancel(run: Run, context: BackendContext): Promise<'confirmed' | 'unknown'>;
|
|
30
|
+
reconcile(run: Run, context: BackendContext): Promise<ReconcileResult>;
|
|
31
|
+
reply(run: Run, interaction: Interaction, answer: Json, context: BackendContext): Promise<void>;
|
|
32
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { Runs } from './runs.js';
|
|
2
|
+
import { Resources } from './resources.js';
|
|
3
|
+
import { ImageProviders } from './image-providers.js';
|
|
4
|
+
import { ImageCredentials } from './image-credentials.js';
|
|
5
|
+
import { ServiceError } from './errors.js';
|
|
6
|
+
import { planOpenAiImage, executeOpenAiImage } from './openai-image-driver.js';
|
|
7
|
+
import { stageApiImageOutput } from './api-image-output.js';
|
|
8
|
+
import { runDirectories } from './directories.js';
|
|
9
|
+
/** No project file/tool access: only the accepted Run's immutable canvas blobs,
|
|
10
|
+
* its empty output directory and the registered frozen provider API base URL. */
|
|
11
|
+
export class ApiImageAgent {
|
|
12
|
+
runs;
|
|
13
|
+
resources;
|
|
14
|
+
providers;
|
|
15
|
+
credentials;
|
|
16
|
+
dirs;
|
|
17
|
+
pending = new Map();
|
|
18
|
+
constructor(runs, resources, providers, credentials, dirs) {
|
|
19
|
+
this.runs = runs;
|
|
20
|
+
this.resources = resources;
|
|
21
|
+
this.providers = providers;
|
|
22
|
+
this.credentials = credentials;
|
|
23
|
+
this.dirs = dirs;
|
|
24
|
+
}
|
|
25
|
+
async start(run, snapshot, context) {
|
|
26
|
+
const route = snapshot.imageRoute;
|
|
27
|
+
if (!route || route.type !== 'api')
|
|
28
|
+
throw new ServiceError('INVALID_REQUEST', 'API 执行器只能运行冻结的 API 图片任务。');
|
|
29
|
+
const controller = new AbortController();
|
|
30
|
+
this.pending.set(run.id, controller);
|
|
31
|
+
let phase = 'preparing';
|
|
32
|
+
try {
|
|
33
|
+
if (this.runs.db.prepare('SELECT 1 FROM image_provider_revoked_credentials WHERE provider_id=? AND revision=?').get(route.providerId, route.credentialRevision))
|
|
34
|
+
throw new ServiceError('MODEL_UNAVAILABLE', '冻结凭据已撤销。');
|
|
35
|
+
const secret = await this.credentials.read(route.providerId, route.credentialRevision);
|
|
36
|
+
if (!secret)
|
|
37
|
+
throw new ServiceError('MODEL_UNAVAILABLE', '冻结凭据版本不在本机。');
|
|
38
|
+
const frozen = this.providers.frozen(route.providerId, route.configRevision, route.modelId);
|
|
39
|
+
const references = [];
|
|
40
|
+
for (const envelope of snapshot.resources) {
|
|
41
|
+
if (envelope.kind !== 'image' || !envelope.resource)
|
|
42
|
+
continue;
|
|
43
|
+
const metadata = this.resources.readCanvasVersion(run, envelope.resource.resourceId, envelope.resource.version);
|
|
44
|
+
if (metadata.sha256 !== envelope.resource.sha256 || metadata.bytes !== envelope.resource.bytes)
|
|
45
|
+
throw new ServiceError('INPUT_BLOCKED', '冻结参考图版本已变化。');
|
|
46
|
+
const content = await this.resources.readContent(run, 'canvas', envelope.resource.resourceId, envelope.resource.version);
|
|
47
|
+
if (!['image/png', 'image/jpeg', 'image/webp'].includes(content.mime))
|
|
48
|
+
throw new ServiceError('INPUT_BLOCKED', '参考图格式不支持。');
|
|
49
|
+
references.push({ bytes: content.bytes, mime: content.mime });
|
|
50
|
+
}
|
|
51
|
+
const plan = planOpenAiImage({ prompt: snapshot.prompt, references: snapshot.resources, images: references, route, model: frozen.model });
|
|
52
|
+
runDirectories(this.dirs, run.id);
|
|
53
|
+
if (controller.signal.aborted || this.runs.get(run.id).status === 'cancelling') {
|
|
54
|
+
context.emit({ type: 'stopped' });
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
// After this point an uncertain transport outcome can represent a paid request.
|
|
58
|
+
context.emit({ type: 'started' });
|
|
59
|
+
if (this.runs.db.prepare('SELECT 1 FROM image_provider_revoked_credentials WHERE provider_id=? AND revision=?').get(route.providerId, route.credentialRevision))
|
|
60
|
+
throw new ServiceError('MODEL_UNAVAILABLE', '冻结凭据已撤销。');
|
|
61
|
+
phase = 'provider_request';
|
|
62
|
+
const result = await executeOpenAiImage(plan, frozen.endpoint, secret, AbortSignal.any([controller.signal, AbortSignal.timeout(180000)]));
|
|
63
|
+
if (controller.signal.aborted || this.runs.get(run.id).status === 'cancelling') {
|
|
64
|
+
context.emit({ type: 'stopped' });
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
phase = 'output_staging';
|
|
68
|
+
await stageApiImageOutput(run, this.dirs, result.bytes, { providerId: route.providerId, modelId: route.modelId, inputDigest: snapshot.inputDigest });
|
|
69
|
+
if (result.requestId)
|
|
70
|
+
context.emit({ type: 'progress', payload: { requestId: result.requestId, source: 'image_provider' } });
|
|
71
|
+
context.emit({ type: 'completed', result: { providerId: route.providerId, modelId: route.modelId, ...(result.requestId ? { requestId: result.requestId } : {}) } });
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
if (controller.signal.aborted) {
|
|
75
|
+
context.emit({ type: 'stopped' });
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (error instanceof ServiceError && error.code === 'CONFLICT')
|
|
79
|
+
throw error; // unknown acceptance: reconcile, never re-send
|
|
80
|
+
if (!(error instanceof ServiceError)) {
|
|
81
|
+
const code = error && typeof error === 'object' && 'code' in error ? String(error.code).slice(0, 40) : undefined;
|
|
82
|
+
const name = error instanceof Error ? error.name : 'UnknownError';
|
|
83
|
+
const message = (error instanceof Error ? error.message : String(error)).replace(/Bearer\s+\S+/gi, 'Bearer <redacted>').replace(/https?:\/\/\S+/gi, '<redacted-url>').replace(/[A-Za-z0-9_-]{32,}/g, '<redacted>').slice(0, 500);
|
|
84
|
+
this.runs.record(run.id, 'api-image.error', { phase, name, ...(code ? { code } : {}), message });
|
|
85
|
+
}
|
|
86
|
+
const fallback = phase === 'output_staging' ? '服务商请求已成功返回,但保存图片输出失败。' : phase === 'provider_request' ? '服务商响应处理失败。' : '图片任务准备失败。';
|
|
87
|
+
context.emit({ type: 'failed', error: error instanceof ServiceError ? error.message : fallback });
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
this.pending.delete(run.id);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async cancel(run, context) {
|
|
94
|
+
const pending = this.pending.get(run.id);
|
|
95
|
+
if (pending) {
|
|
96
|
+
pending.abort();
|
|
97
|
+
return 'confirmed';
|
|
98
|
+
}
|
|
99
|
+
// Aborting local wait is not proof of provider cancellation; no new attempt follows.
|
|
100
|
+
if (this.runs.runtime(run.id).details.cancelRequested === true) {
|
|
101
|
+
context.emit({ type: 'stopped' });
|
|
102
|
+
return 'confirmed';
|
|
103
|
+
}
|
|
104
|
+
return 'unknown';
|
|
105
|
+
}
|
|
106
|
+
async reconcile(run, _context) {
|
|
107
|
+
if (this.runs.runtime(run.id).details.cancelRequested === true)
|
|
108
|
+
return 'interrupted';
|
|
109
|
+
return 'unknown'; // No provider query/idempotency contract: preserve uncertainty after restart.
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
export class RoutedImageBackend {
|
|
113
|
+
codex;
|
|
114
|
+
api;
|
|
115
|
+
runs;
|
|
116
|
+
constructor(codex, api, runs) {
|
|
117
|
+
this.codex = codex;
|
|
118
|
+
this.api = api;
|
|
119
|
+
this.runs = runs;
|
|
120
|
+
}
|
|
121
|
+
delegate(run) { return this.runs.snapshot(run.id).imageRoute?.type === 'api' ? this.api : this.codex; }
|
|
122
|
+
start(run, snapshot, context) { return (snapshot.imageRoute?.type === 'api' ? this.api : this.codex).start(run, snapshot, context); }
|
|
123
|
+
cancel(run, context) { return this.delegate(run).cancel(run, context); }
|
|
124
|
+
reconcile(run, context) { return this.delegate(run).reconcile?.(run, context) ?? Promise.resolve('unknown'); }
|
|
125
|
+
reply(run, interaction, answer, context) {
|
|
126
|
+
const delegate = this.delegate(run);
|
|
127
|
+
if (!delegate.reply)
|
|
128
|
+
throw new ServiceError('INVALID_REQUEST', 'API 图片任务不支持交互。');
|
|
129
|
+
return delegate.reply(run, interaction, answer, context);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=api-image-agent.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Run } from "./vendor/protocol/index.js";
|
|
2
|
+
import { type DataDirectories } from './directories.js';
|
|
3
|
+
/** Publishes nothing itself: the existing Publication re-reads and validates these files. */
|
|
4
|
+
export declare function stageApiImageOutput(run: Run, dirs: DataDirectories, image: Buffer, publicMetadata: {
|
|
5
|
+
providerId: string;
|
|
6
|
+
modelId: string;
|
|
7
|
+
inputDigest: string;
|
|
8
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { constants } from 'node:fs';
|
|
3
|
+
import { open, readdir } from 'node:fs/promises';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { runDirectories } from './directories.js';
|
|
6
|
+
import { ServiceError } from './errors.js';
|
|
7
|
+
import { validateImageOutput } from './publication.js';
|
|
8
|
+
const sha256 = (bytes) => createHash('sha256').update(bytes).digest('hex');
|
|
9
|
+
async function writeExclusive(path, bytes) {
|
|
10
|
+
const handle = await open(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
|
|
11
|
+
try {
|
|
12
|
+
await handle.writeFile(bytes);
|
|
13
|
+
await handle.sync();
|
|
14
|
+
}
|
|
15
|
+
finally {
|
|
16
|
+
await handle.close();
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** Publishes nothing itself: the existing Publication re-reads and validates these files. */
|
|
20
|
+
export async function stageApiImageOutput(run, dirs, image, publicMetadata) {
|
|
21
|
+
if (!/^[a-zA-Z0-9_-]{1,100}$/.test(publicMetadata.providerId) || !/^[a-zA-Z0-9_.-]{1,100}$/.test(publicMetadata.modelId) || ['.', '..'].includes(publicMetadata.modelId) || !/^[a-f0-9]{64}$/.test(publicMetadata.inputDigest) || publicMetadata.inputDigest !== run.inputDigest)
|
|
22
|
+
throw new ServiceError('INPUT_BLOCKED', '输出配置与冻结 Run 不一致。');
|
|
23
|
+
const mime = validateImageOutput(image);
|
|
24
|
+
const suffix = { 'image/png': 'png', 'image/jpeg': 'jpg', 'image/webp': 'webp' }[mime];
|
|
25
|
+
const root = runDirectories(dirs, run.id).output;
|
|
26
|
+
if ((await readdir(root)).length)
|
|
27
|
+
throw new ServiceError('CONFLICT', 'API Run 输出目录非空,不能覆盖或重发。');
|
|
28
|
+
const note = Buffer.from('# 图片生成结果\n\n服务商:' + publicMetadata.providerId + '\n\n模型:' + publicMetadata.modelId + '\n\n输入摘要:' + run.inputDigest + '\n', 'utf8');
|
|
29
|
+
const outputs = [
|
|
30
|
+
{ outputKey: 'image', path: 'image.' + suffix, mime, bytes: image.length, sha256: sha256(image) },
|
|
31
|
+
{ outputKey: 'description', path: 'description.md', mime: 'text/markdown', bytes: note.length, sha256: sha256(note) },
|
|
32
|
+
];
|
|
33
|
+
const manifest = Buffer.from(JSON.stringify({ outputs }), 'utf8');
|
|
34
|
+
if (manifest.length > 64 * 1024)
|
|
35
|
+
throw new ServiceError('PAYLOAD_TOO_LARGE', '输出清单超出限制。');
|
|
36
|
+
await writeExclusive(join(root, outputs[0].path), image);
|
|
37
|
+
await writeExclusive(join(root, outputs[1].path), note);
|
|
38
|
+
// A complete manifest is the last durable commit marker for Publication.
|
|
39
|
+
await writeExclusive(join(root, 'manifest.json'), manifest);
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=api-image-output.js.map
|
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { IncomingMessage } from 'node:http';
|
|
2
|
+
import type { DatabaseSync } from 'node:sqlite';
|
|
3
|
+
import { Auth } from './auth.js';
|
|
4
|
+
import { Graphs } from './graphs.js';
|
|
5
|
+
import { ResourceApi } from './resource-api.js';
|
|
6
|
+
import { WorkflowRuntime } from './runtime.js';
|
|
7
|
+
import { RunApi } from './run-api.js';
|
|
8
|
+
import { Backups } from './operations/backups.js';
|
|
9
|
+
import { BackupApi } from './backup-api.js';
|
|
10
|
+
export interface ApiResult {
|
|
11
|
+
handled: boolean;
|
|
12
|
+
body?: unknown;
|
|
13
|
+
status?: number;
|
|
14
|
+
binary?: Buffer;
|
|
15
|
+
binaryParts?: Uint8Array[];
|
|
16
|
+
headers?: Record<string, string>;
|
|
17
|
+
}
|
|
18
|
+
export declare class BusinessApi {
|
|
19
|
+
readonly db: DatabaseSync;
|
|
20
|
+
readonly serviceId: string;
|
|
21
|
+
readonly auth: Auth;
|
|
22
|
+
readonly runtime: WorkflowRuntime;
|
|
23
|
+
readonly graphs: Graphs;
|
|
24
|
+
readonly resources: ResourceApi;
|
|
25
|
+
readonly runApi: RunApi;
|
|
26
|
+
readonly backups: Backups;
|
|
27
|
+
readonly backupApi: BackupApi;
|
|
28
|
+
private transferApi?;
|
|
29
|
+
private pluginApi?;
|
|
30
|
+
private projectFilesApi?;
|
|
31
|
+
private skillsApi?;
|
|
32
|
+
private storageApi?;
|
|
33
|
+
constructor(db: DatabaseSync, serviceId: string, auth: Auth, runtime: WorkflowRuntime);
|
|
34
|
+
handle(request: IncomingMessage, path: string, token: string, origin: string): Promise<ApiResult>;
|
|
35
|
+
}
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { Auth } from './auth.js';
|
|
2
|
+
import { Graphs } from './graphs.js';
|
|
3
|
+
import { Events } from './events.js';
|
|
4
|
+
import { readJson } from './http.js';
|
|
5
|
+
import { ServiceError } from './errors.js';
|
|
6
|
+
import { ResourceApi } from './resource-api.js';
|
|
7
|
+
import { WorkflowRuntime } from './runtime.js';
|
|
8
|
+
import { RunApi } from './run-api.js';
|
|
9
|
+
import { Backups } from './operations/backups.js';
|
|
10
|
+
import { BackupApi } from './backup-api.js';
|
|
11
|
+
import { TransferApi } from './transfer-api.js';
|
|
12
|
+
import { GraphTransfer } from './graph-transfer.js';
|
|
13
|
+
import { PluginApi } from './plugin-api.js';
|
|
14
|
+
import { PluginManagement } from './plugin-management.js';
|
|
15
|
+
import { Repositories } from './persistence/repositories.js';
|
|
16
|
+
import { ProjectFilesApi } from './project-files-api.js';
|
|
17
|
+
import { SkillsApi } from './skills-api.js';
|
|
18
|
+
import { StorageApi } from './storage-api.js';
|
|
19
|
+
export class BusinessApi {
|
|
20
|
+
db;
|
|
21
|
+
serviceId;
|
|
22
|
+
auth;
|
|
23
|
+
runtime;
|
|
24
|
+
graphs;
|
|
25
|
+
resources;
|
|
26
|
+
runApi;
|
|
27
|
+
backups;
|
|
28
|
+
backupApi;
|
|
29
|
+
transferApi;
|
|
30
|
+
pluginApi;
|
|
31
|
+
projectFilesApi;
|
|
32
|
+
skillsApi;
|
|
33
|
+
storageApi;
|
|
34
|
+
constructor(db, serviceId, auth, runtime) {
|
|
35
|
+
this.db = db;
|
|
36
|
+
this.serviceId = serviceId;
|
|
37
|
+
this.auth = auth;
|
|
38
|
+
this.runtime = runtime;
|
|
39
|
+
this.graphs = runtime.graphs;
|
|
40
|
+
this.resources = new ResourceApi(runtime.resources, this.graphs, auth);
|
|
41
|
+
this.runApi = new RunApi(runtime, auth);
|
|
42
|
+
this.backups = new Backups(db, runtime.directories, { withBlobLease: work => runtime.resources.withBlobLease(work) });
|
|
43
|
+
this.backupApi = new BackupApi(this.backups, auth);
|
|
44
|
+
}
|
|
45
|
+
async handle(request, path, token, origin) {
|
|
46
|
+
this.storageApi ??= new StorageApi(this.db, this.runtime.directories, this.runtime.resources, this.auth);
|
|
47
|
+
const storage = await this.storageApi.handle(request, path, token, origin);
|
|
48
|
+
if (storage.handled)
|
|
49
|
+
return storage;
|
|
50
|
+
this.skillsApi ??= new SkillsApi(this.db, this.auth, this.runtime);
|
|
51
|
+
const skills = await this.skillsApi.handle(request, path, token, origin);
|
|
52
|
+
if (skills.handled)
|
|
53
|
+
return skills;
|
|
54
|
+
this.projectFilesApi ??= new ProjectFilesApi(this.db, this.auth, this.graphs, this.runtime.resources, this.runtime.directories.runs);
|
|
55
|
+
const files = await this.projectFilesApi.handle(request, path, token, origin);
|
|
56
|
+
if (files.handled)
|
|
57
|
+
return files;
|
|
58
|
+
const page = /^[/]v1[/]projects[/]([a-zA-Z0-9_-]+)[/]graphs[/]page$/.exec(path);
|
|
59
|
+
if (page && request.method === 'POST') {
|
|
60
|
+
this.auth.withSession(token, origin, () => undefined);
|
|
61
|
+
const body = await readJson(request, 16 * 1024);
|
|
62
|
+
return this.auth.withSession(token, origin, current => {
|
|
63
|
+
const result = this.graphs.page(page[1], body, current.id);
|
|
64
|
+
// service.ts decorates the outer eventCursor; nested snapshots must also
|
|
65
|
+
// carry session-bound cursors, never raw database sequence numbers.
|
|
66
|
+
const events = new Events(this.db, this.serviceId, this.auth);
|
|
67
|
+
return { handled: true, body: { ...result, items: events.decorate(result.items, current.id) } };
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
this.pluginApi ??= new PluginApi(new PluginManagement(this.graphs, this.runtime.plugins), this.auth);
|
|
71
|
+
const plugin = await this.pluginApi.handle(request, path, token, origin);
|
|
72
|
+
if (plugin.handled)
|
|
73
|
+
return plugin;
|
|
74
|
+
this.transferApi ??= new TransferApi(new GraphTransfer(this.graphs, this.runtime.resources), this.auth);
|
|
75
|
+
const transfer = await this.transferApi.handle(request, path, token, origin);
|
|
76
|
+
if (transfer.handled)
|
|
77
|
+
return transfer;
|
|
78
|
+
const backup = await this.backupApi.handle(request, path, token, origin);
|
|
79
|
+
if (backup.handled)
|
|
80
|
+
return backup;
|
|
81
|
+
const resource = await this.resources.handle(request, path, token, origin);
|
|
82
|
+
if (resource.handled)
|
|
83
|
+
return resource;
|
|
84
|
+
const run = await this.runApi.handle(request, path, token, origin);
|
|
85
|
+
if (run.handled)
|
|
86
|
+
return run;
|
|
87
|
+
const match = /^[/]v1[/]projects[/]([a-zA-Z0-9_-]+)[/]graphs(?:[/]([a-zA-Z0-9_-]+))?(?:[/](commands|history|all|archive|trash|restore|purge|permanent-delete))?$/.exec(path);
|
|
88
|
+
if (!match)
|
|
89
|
+
return { handled: false };
|
|
90
|
+
this.auth.withSession(token, origin, () => undefined);
|
|
91
|
+
const body = request.method === 'POST' ? await readJson(request, 16 * 1024 * 1024) : undefined;
|
|
92
|
+
const result = this.auth.withSession(token, origin, current => {
|
|
93
|
+
const projectId = match[1], graphId = match[2], action = match[3];
|
|
94
|
+
if (request.method === 'GET' && !graphId)
|
|
95
|
+
return { handled: true, body: this.graphs.list(projectId) };
|
|
96
|
+
if (request.method === 'GET' && graphId === 'all')
|
|
97
|
+
return { handled: true, body: this.graphs.list(projectId, true, true) };
|
|
98
|
+
if (request.method === 'GET' && graphId === 'trash' && !action)
|
|
99
|
+
return { handled: true, body: this.graphs.list(projectId, true, true, true) };
|
|
100
|
+
if (request.method === 'GET' && graphId && !action)
|
|
101
|
+
return { handled: true, body: this.graphs.snapshot({ serviceId: this.serviceId, projectId, graphId }) };
|
|
102
|
+
if (request.method === 'POST' && !graphId && body) {
|
|
103
|
+
if (Object.keys(body).some(key => !['title', 'idempotencyKey'].includes(key)) || typeof body['title'] !== 'string' || typeof body['idempotencyKey'] !== 'string')
|
|
104
|
+
throw new ServiceError('INVALID_REQUEST', '建图请求须包含 title 和 idempotencyKey。');
|
|
105
|
+
return { handled: true, body: this.graphs.create(projectId, body['title'], body['idempotencyKey'], current.id) };
|
|
106
|
+
}
|
|
107
|
+
if (request.method === 'POST' && graphId && action === 'permanent-delete' && body) {
|
|
108
|
+
if (Object.keys(body).some(key => !['idempotencyKey', 'expectedExecutionRevision', 'expectedLayoutRevision', 'confirmTitle'].includes(key)) || typeof body['idempotencyKey'] !== 'string')
|
|
109
|
+
throw new ServiceError('INVALID_REQUEST', '无效的彻底删除请求。');
|
|
110
|
+
return { handled: true, body: this.graphs.permanentDelete({ ...body, serviceId: this.serviceId, projectId, graphId }, this.runtime.resources, current.id) };
|
|
111
|
+
}
|
|
112
|
+
if (request.method === 'POST' && graphId && action && ['archive', 'trash', 'restore', 'purge'].includes(action) && body) {
|
|
113
|
+
const allowed = action === 'purge' ? ['idempotencyKey', 'expectedExecutionRevision', 'confirmationTitle'] : ['idempotencyKey', 'expectedExecutionRevision'];
|
|
114
|
+
if (Object.keys(body).some(key => !allowed.includes(key)) || typeof body['idempotencyKey'] !== 'string' || !Number.isSafeInteger(body['expectedExecutionRevision']) || Number(body['expectedExecutionRevision']) < 0 || (action === 'purge' && typeof body['confirmationTitle'] !== 'string'))
|
|
115
|
+
throw new ServiceError('INVALID_REQUEST', '生命周期请求需要幂等键、执行版本;彻底删除还需 confirmationTitle。');
|
|
116
|
+
const scope = { serviceId: this.serviceId, projectId, graphId };
|
|
117
|
+
const value = new Repositories(this.db).idempotent(current.id + ':graph.lifecycle:' + graphId, body['idempotencyKey'], { ...body, ...scope, action }, () => {
|
|
118
|
+
const snapshot = this.graphs.snapshot(scope);
|
|
119
|
+
const base = { ...scope, idempotencyKey: body['idempotencyKey'], expectedExecutionRevision: body['expectedExecutionRevision'], expectedLayoutRevision: snapshot.layoutRevision };
|
|
120
|
+
if (action === 'purge')
|
|
121
|
+
return this.graphs.permanentDelete({ ...base, confirmTitle: body['confirmationTitle'] }, this.runtime.resources, current.id);
|
|
122
|
+
const operation = action === 'archive' ? { type: 'graph.archive', archived: true } : action === 'trash' ? { type: 'graph.trash', trashed: true } : snapshot.trashed ? { type: 'graph.trash', trashed: false } : { type: 'graph.archive', archived: false };
|
|
123
|
+
return this.graphs.command({ ...base, operations: [operation] }, current.id);
|
|
124
|
+
});
|
|
125
|
+
return { handled: true, body: value };
|
|
126
|
+
}
|
|
127
|
+
if (request.method === 'POST' && graphId && action === 'history' && body) {
|
|
128
|
+
if (Object.keys(body).some(key => !['idempotencyKey', 'expectedExecutionRevision', 'expectedLayoutRevision', 'expectedCursor', 'direction'].includes(key)) || typeof body['idempotencyKey'] !== 'string')
|
|
129
|
+
throw new ServiceError('INVALID_REQUEST', '无效的历史恢复请求。');
|
|
130
|
+
const snapshot = this.graphs.history.travel({ ...body, serviceId: this.serviceId, projectId, graphId }, current.id);
|
|
131
|
+
this.runtime.resources.collectGarbage();
|
|
132
|
+
return { handled: true, body: snapshot };
|
|
133
|
+
}
|
|
134
|
+
if (request.method === 'POST' && graphId && action === 'commands' && body) {
|
|
135
|
+
if (Object.keys(body).some(key => !['idempotencyKey', 'expectedExecutionRevision', 'expectedLayoutRevision', 'operations'].includes(key)))
|
|
136
|
+
throw new ServiceError('INVALID_REQUEST', '图命令包含未知字段。');
|
|
137
|
+
const command = { ...body, serviceId: this.serviceId, projectId, graphId };
|
|
138
|
+
if (typeof command.idempotencyKey !== 'string' || !Number.isSafeInteger(command.expectedExecutionRevision) || !Number.isSafeInteger(command.expectedLayoutRevision))
|
|
139
|
+
throw new ServiceError('INVALID_REQUEST', '图命令需要幂等键和整数版本。');
|
|
140
|
+
const snapshot = this.graphs.command(command, current.id);
|
|
141
|
+
this.runtime.resources.collectGarbage();
|
|
142
|
+
return { handled: true, body: snapshot };
|
|
143
|
+
}
|
|
144
|
+
throw new ServiceError('NOT_FOUND', '图接口不存在。');
|
|
145
|
+
});
|
|
146
|
+
if (request.method === 'POST') {
|
|
147
|
+
await this.runtime.resources.drainFileDeletions();
|
|
148
|
+
await this.runtime.graphFiles.drain();
|
|
149
|
+
}
|
|
150
|
+
return result;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
//# sourceMappingURL=api.js.map
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
import type { ClientPairingStatus, PairRequest, PairResponse, Session } from "./vendor/protocol/index.js";
|
|
3
|
+
export declare const SESSION_IDLE_MS: number;
|
|
4
|
+
export declare const PAIRING_TTL_MS: number;
|
|
5
|
+
export declare function normalizeOrigin(value: string): string;
|
|
6
|
+
export declare class Auth {
|
|
7
|
+
readonly db: DatabaseSync;
|
|
8
|
+
readonly serviceId: string;
|
|
9
|
+
readonly now: () => number;
|
|
10
|
+
private pendingClients;
|
|
11
|
+
private shortAttempts;
|
|
12
|
+
private prunePairingRequests;
|
|
13
|
+
/** Public registration grants no trust. Only the local control channel can resolve/approve it. */
|
|
14
|
+
requestClientPairing(clientCode: string, origin: string): {
|
|
15
|
+
code: string;
|
|
16
|
+
expiresAt: string;
|
|
17
|
+
serviceId: string;
|
|
18
|
+
};
|
|
19
|
+
resolveClientPairing(code: string): string;
|
|
20
|
+
approveClientPairing(code: string, clientCode: string): {
|
|
21
|
+
status: string;
|
|
22
|
+
origin: string;
|
|
23
|
+
fingerprint: string;
|
|
24
|
+
expiresAt: string;
|
|
25
|
+
serviceId: string;
|
|
26
|
+
};
|
|
27
|
+
claimClientPairing(input: PairRequest, origin: string): ClientPairingStatus;
|
|
28
|
+
completeClientPairing(codeInput: string, expectedServiceId: string, token: string, origin: string): ClientPairingStatus;
|
|
29
|
+
constructor(db: DatabaseSync, serviceId: string, now?: () => number);
|
|
30
|
+
private hash;
|
|
31
|
+
/** Local-only authority. Never route issuance through the network API. */
|
|
32
|
+
issueCode(originInput: string): {
|
|
33
|
+
code: string;
|
|
34
|
+
origin: string;
|
|
35
|
+
expiresAt: string;
|
|
36
|
+
serviceId: string;
|
|
37
|
+
};
|
|
38
|
+
/** Local authorization only. A bound grant can never use the legacy exchange. */
|
|
39
|
+
issueClientCode(clientCode: string, short?: boolean): {
|
|
40
|
+
code: string;
|
|
41
|
+
origin: string;
|
|
42
|
+
fingerprint: string;
|
|
43
|
+
expiresAt: string;
|
|
44
|
+
serviceId: string;
|
|
45
|
+
};
|
|
46
|
+
requireOrigin(origin: string | undefined): string;
|
|
47
|
+
pair(input: PairRequest, origin: string): PairResponse;
|
|
48
|
+
private active;
|
|
49
|
+
/** Recheck immediately before a stream emits data; keepalives must not renew idle expiry. */
|
|
50
|
+
assertActive(token: string, origin: string): void;
|
|
51
|
+
/** Authorization, renewal and each short operation share a transaction, including CLI revocation races. */
|
|
52
|
+
withSession<T>(token: string, origin: string, operation: (session: Session) => T): T;
|
|
53
|
+
list(currentId?: string): Session[];
|
|
54
|
+
/** Call in the authenticated operation transaction, or through revokeLocal. */
|
|
55
|
+
revoke(id: string): void;
|
|
56
|
+
revokeLocal(id: string): void;
|
|
57
|
+
private toSession;
|
|
58
|
+
}
|