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,20 @@
1
+ import type { DatabaseSync } from 'node:sqlite';
2
+ import type { HistoryCleanupPreview, Json } from "./vendor/protocol/index.js";
3
+ import { Repositories } from './persistence/repositories.js';
4
+ export declare class History {
5
+ readonly db: DatabaseSync;
6
+ readonly now: () => number;
7
+ readonly repo: Repositories;
8
+ constructor(db: DatabaseSync, now?: () => number);
9
+ preview(runIds: string[]): HistoryCleanupPreview;
10
+ clear(runIds: string[], idempotencyKey: string): HistoryCleanupPreview;
11
+ records(id: string): {
12
+ historyState: 'retained' | 'cleared';
13
+ records: {
14
+ id: number;
15
+ occurredAt: string;
16
+ kind: string;
17
+ payload: Json;
18
+ }[];
19
+ };
20
+ }
@@ -0,0 +1,61 @@
1
+ import { isTerminalRunStatus } from "./vendor/protocol/index.js";
2
+ import { randomUUID } from 'node:crypto';
3
+ import { ServiceError } from './errors.js';
4
+ import { atomic } from './persistence/database.js';
5
+ import { Repositories } from './persistence/repositories.js';
6
+ export class History {
7
+ db;
8
+ now;
9
+ repo;
10
+ constructor(db, now = Date.now) {
11
+ this.db = db;
12
+ this.now = now;
13
+ this.repo = new Repositories(db);
14
+ }
15
+ preview(runIds) {
16
+ return atomic(this.db, () => {
17
+ if (!Array.isArray(runIds) || !runIds.length || runIds.length > 1000 || runIds.some(id => typeof id !== 'string' || !id))
18
+ throw new ServiceError('INVALID_REQUEST', 'Select 1..1000 run IDs');
19
+ const unique = [...new Set(runIds)];
20
+ let count = 0;
21
+ for (const id of unique) {
22
+ const run = this.db.prepare('SELECT status FROM runs WHERE id=?').get(id);
23
+ if (!run)
24
+ throw new ServiceError('NOT_FOUND', 'Run not found');
25
+ if (!isTerminalRunStatus(String(run.status)))
26
+ throw new ServiceError('ACTIVE_RUN', 'Cannot clear unfinished run history');
27
+ count += Number(this.db.prepare('SELECT COUNT(*) AS n FROM run_process_records WHERE run_id=?').get(id).n);
28
+ }
29
+ return { runIds: unique, processRecordCount: count, preserves: ['runs', 'deliveries', 'assets', 'graphLinks'] };
30
+ });
31
+ }
32
+ clear(runIds, idempotencyKey) {
33
+ return this.repo.idempotent('history.clear', idempotencyKey, { runIds }, () => {
34
+ const preview = this.preview(runIds);
35
+ for (const id of preview.runIds) {
36
+ const run = this.db.prepare('SELECT * FROM runs WHERE id=?').get(id);
37
+ const graph = this.db.prepare('SELECT g.archived,g.trashed,p.state FROM graphs g JOIN projects p ON p.id=g.project_id WHERE g.id=?').get(String(run.graph_id));
38
+ if (graph.state !== 'active')
39
+ throw new ServiceError('PROJECT_INACTIVE', 'Inactive project history is read-only');
40
+ if (graph.archived || graph.trashed)
41
+ throw new ServiceError('CONFLICT', 'Archived or trashed graph history is read-only');
42
+ if (run.history_state === 'cleared')
43
+ continue;
44
+ this.db.prepare('DELETE FROM run_process_records WHERE run_id=?').run(id);
45
+ this.db.prepare("UPDATE interactions SET payload='null',answer=NULL WHERE run_id=?").run(id);
46
+ this.db.prepare("UPDATE runs SET history_state='cleared' WHERE id=?").run(id);
47
+ this.db.prepare('UPDATE run_runtime SET revision=revision+1 WHERE run_id=?').run(id);
48
+ const runtime = this.db.prepare('SELECT revision FROM run_runtime WHERE run_id=?').get(id);
49
+ this.repo.appendEvent({ eventId: randomUUID(), type: 'run.changed', projectId: String(run.project_id), graphId: String(run.graph_id), entityId: id, revision: Number(runtime?.revision ?? 0), occurredAt: new Date(this.now()).toISOString(), payload: { id, historyState: 'cleared' } });
50
+ }
51
+ return preview;
52
+ });
53
+ }
54
+ records(id) {
55
+ const run = this.db.prepare('SELECT history_state FROM runs WHERE id=?').get(id);
56
+ if (!run)
57
+ throw new ServiceError('NOT_FOUND', 'Run not found');
58
+ return { historyState: run.history_state, records: this.db.prepare('SELECT * FROM run_process_records WHERE run_id=? ORDER BY id').all(id).map(row => ({ id: Number(row.id), occurredAt: String(row.occurred_at), kind: String(row.kind), payload: JSON.parse(String(row.payload)) })) };
59
+ }
60
+ }
61
+ //# sourceMappingURL=history.js.map
package/dist/http.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ import type { IncomingMessage } from 'node:http';
2
+ export declare function readJson(request: IncomingMessage, limit?: number): Promise<Record<string, unknown>>;
package/dist/http.js ADDED
@@ -0,0 +1,42 @@
1
+ import { ServiceError } from './errors.js';
2
+ export function readJson(request, limit = 16 * 1024) {
3
+ if (!/^application[/]json(?:\s*;\s*charset=utf-8)?$/i.test(request.headers['content-type'] ?? ''))
4
+ throw new ServiceError('UNSUPPORTED_MEDIA_TYPE', '请求须使用 application/json。');
5
+ if (request.headers['content-encoding'])
6
+ throw new ServiceError('UNSUPPORTED_MEDIA_TYPE', '暂不接受压缩的 JSON 请求。');
7
+ if (Number(request.headers['content-length'] ?? 0) > limit)
8
+ throw new ServiceError('PAYLOAD_TOO_LARGE', `请求不能超过 ${limit} 字节。`);
9
+ return new Promise((resolve, reject) => {
10
+ let bytes = 0;
11
+ let exceeded = false;
12
+ const chunks = [];
13
+ request.on('data', (chunk) => {
14
+ bytes += chunk.length;
15
+ if (bytes > limit) {
16
+ if (!exceeded) {
17
+ exceeded = true;
18
+ chunks.length = 0;
19
+ reject(new ServiceError('PAYLOAD_TOO_LARGE', `请求不能超过 ${limit} 字节。`));
20
+ }
21
+ return;
22
+ }
23
+ chunks.push(chunk);
24
+ });
25
+ request.once('end', () => {
26
+ if (exceeded)
27
+ return;
28
+ try {
29
+ const value = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(Buffer.concat(chunks)));
30
+ if (!value || typeof value !== 'object' || Array.isArray(value))
31
+ throw new Error();
32
+ resolve(value);
33
+ }
34
+ catch {
35
+ reject(new ServiceError('INVALID_REQUEST', '请求正文必须是有效的 JSON 对象。'));
36
+ }
37
+ });
38
+ request.once('error', reject);
39
+ request.once('aborted', () => reject(new ServiceError('INVALID_REQUEST', '请求已中断。')));
40
+ });
41
+ }
42
+ //# sourceMappingURL=http.js.map
@@ -0,0 +1,11 @@
1
+ import type { DataDirectories } from './directories.js';
2
+ export declare class ImageCredentials {
3
+ private readonly dirs;
4
+ private readonly serviceId;
5
+ private readonly credentialDir;
6
+ constructor(dirs: DataDirectories, serviceId: string);
7
+ private file;
8
+ store(id: string, revision: number, value: string): Promise<void>;
9
+ read(id: string, revision: number): Promise<string | null>;
10
+ revoke(id: string, revision: number): Promise<void>;
11
+ }
@@ -0,0 +1,130 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { chmodSync, closeSync, constants, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { ServiceError } from './errors.js';
5
+ const identifier = (value) => {
6
+ if (!/^[a-zA-Z0-9_-]{1,100}$/.test(value))
7
+ throw new ServiceError('INVALID_REQUEST', '凭据标识无效。');
8
+ return value;
9
+ };
10
+ const scope = (serviceId, providerId, revision) => [identifier(serviceId), identifier(providerId), revision].join(':');
11
+ /** Fixed commands only. Secret input travels on stdin; platform diagnostics stay private. */
12
+ function invoke(command, args, input) {
13
+ return new Promise((resolve, reject) => {
14
+ const child = spawn(command, args, { stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
15
+ const timer = setTimeout(() => child.kill(), 30000);
16
+ timer.unref();
17
+ const output = [];
18
+ let length = 0;
19
+ child.stdout.on('data', (chunk) => {
20
+ length += chunk.length;
21
+ if (length > 64 * 1024)
22
+ child.kill();
23
+ else
24
+ output.push(chunk);
25
+ });
26
+ child.stderr.resume();
27
+ child.stdin.on('error', () => { });
28
+ child.on('error', () => { clearTimeout(timer); reject(new ServiceError('MODEL_UNAVAILABLE', '本机凭据存储不可用。')); });
29
+ child.on('close', code => { clearTimeout(timer); code === 0 && length <= 64 * 1024 ? resolve(Buffer.concat(output).toString('utf8').trimEnd()) : reject(new ServiceError('MODEL_UNAVAILABLE', '本机凭据存储操作失败。')); });
30
+ child.stdin.end(input === undefined ? '' : input + String.fromCharCode(10));
31
+ });
32
+ }
33
+ const encrypt = '$v=[Console]::In.ReadToEnd().TrimEnd([char]10,[char]13); ConvertFrom-SecureString (ConvertTo-SecureString -String $v -AsPlainText -Force)';
34
+ const decrypt = '$v=[Console]::In.ReadToEnd().TrimEnd([char]10,[char]13); $s=ConvertTo-SecureString $v; $p=[Runtime.InteropServices.Marshal]::SecureStringToBSTR($s); try {[Runtime.InteropServices.Marshal]::PtrToStringBSTR($p)} finally {[Runtime.InteropServices.Marshal]::ZeroFreeBSTR($p)}';
35
+ export class ImageCredentials {
36
+ dirs;
37
+ serviceId;
38
+ credentialDir;
39
+ constructor(dirs, serviceId) {
40
+ this.dirs = dirs;
41
+ this.serviceId = serviceId;
42
+ this.credentialDir = join(dirs.config, 'image-credentials');
43
+ if (process.platform === 'win32' || process.platform === 'darwin') {
44
+ mkdirSync(this.credentialDir, { recursive: true, mode: 0o700 });
45
+ const stat = lstatSync(this.credentialDir);
46
+ if (!stat.isDirectory() || stat.isSymbolicLink())
47
+ throw new ServiceError('INVALID_REQUEST', '凭据目录不能是符号链接。');
48
+ if (process.platform === 'darwin')
49
+ chmodSync(this.credentialDir, 0o700);
50
+ }
51
+ }
52
+ file(id, revision) { return join(this.credentialDir, identifier(this.serviceId) + '.' + Buffer.from(identifier(id)).toString('hex') + '.' + revision + (process.platform === 'darwin' ? '.key' : '.dpapi')); }
53
+ async store(id, revision, value) {
54
+ const key = scope(this.serviceId, id, revision);
55
+ if (typeof value !== 'string' || !value.trim() || value.length > 16 * 1024 || value.includes(String.fromCharCode(10)) || value.includes(String.fromCharCode(13)))
56
+ throw new ServiceError('INVALID_REQUEST', '凭据格式无效。');
57
+ if (process.platform === 'darwin') {
58
+ const path = this.file(id, revision);
59
+ const fd = openSync(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
60
+ try {
61
+ writeFileSync(fd, value, 'utf8');
62
+ }
63
+ catch (error) {
64
+ closeSync(fd);
65
+ unlinkSync(path);
66
+ throw error;
67
+ }
68
+ closeSync(fd);
69
+ }
70
+ else if (process.platform === 'linux') {
71
+ await invoke('secret-tool', ['store', '--label=OpenWorkgraph image provider', 'service', 'OpenWorkgraph-image-provider', 'account', key], value);
72
+ }
73
+ else if (process.platform === 'win32') {
74
+ const ciphertext = await invoke('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', encrypt], value);
75
+ if (!ciphertext)
76
+ throw new ServiceError('MODEL_UNAVAILABLE', '本机凭据保护失败。');
77
+ writeFileSync(this.file(id, revision), ciphertext, { flag: 'wx', mode: 0o600 });
78
+ }
79
+ else
80
+ throw new ServiceError('MODEL_UNAVAILABLE', '此平台没有受保护的凭据存储。');
81
+ if (await this.read(id, revision) !== value) {
82
+ await this.revoke(id, revision).catch(() => { });
83
+ throw new ServiceError('MODEL_UNAVAILABLE', 'API Key 写入后无法从本机凭据存储读取。');
84
+ }
85
+ }
86
+ async read(id, revision) {
87
+ const key = scope(this.serviceId, id, revision);
88
+ try {
89
+ let value;
90
+ if (process.platform === 'darwin') {
91
+ const fd = openSync(this.file(id, revision), constants.O_RDONLY | constants.O_NOFOLLOW);
92
+ try {
93
+ const stat = fstatSync(fd);
94
+ if (!stat.isFile() || stat.mode & 0o077 || stat.size > 16 * 1024)
95
+ return null;
96
+ value = readFileSync(fd, 'utf8');
97
+ }
98
+ finally {
99
+ closeSync(fd);
100
+ }
101
+ }
102
+ else if (process.platform === 'linux')
103
+ value = await invoke('secret-tool', ['lookup', 'service', 'OpenWorkgraph-image-provider', 'account', key]);
104
+ else if (process.platform === 'win32') {
105
+ const path = this.file(id, revision), stat = lstatSync(path);
106
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 64 * 1024)
107
+ throw Error('Invalid credential file');
108
+ value = await invoke('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', decrypt], readFileSync(path, 'utf8'));
109
+ }
110
+ else
111
+ return null;
112
+ return value || null;
113
+ }
114
+ catch {
115
+ return null;
116
+ }
117
+ }
118
+ async revoke(id, revision) {
119
+ const key = scope(this.serviceId, id, revision);
120
+ if (process.platform === 'darwin')
121
+ unlinkSync(this.file(id, revision));
122
+ else if (process.platform === 'linux')
123
+ await invoke('secret-tool', ['clear', 'service', 'OpenWorkgraph-image-provider', 'account', key]);
124
+ else if (process.platform === 'win32')
125
+ unlinkSync(this.file(id, revision));
126
+ else
127
+ throw new ServiceError('MODEL_UNAVAILABLE', '本机凭据存储不可用。');
128
+ }
129
+ }
130
+ //# sourceMappingURL=image-credentials.js.map
@@ -0,0 +1,51 @@
1
+ import type { DatabaseSync } from 'node:sqlite';
2
+ import type { ImageProvider, ImageProviderModel } from "./vendor/protocol/index.js";
3
+ import { ImageCredentials } from './image-credentials.js';
4
+ export interface ProviderConfig {
5
+ id: string;
6
+ name: string;
7
+ driver: 'openai';
8
+ endpoint: string;
9
+ enabled: boolean;
10
+ }
11
+ export interface ModelConfig {
12
+ id: string;
13
+ name: string;
14
+ modes: ImageProviderModel['modes'];
15
+ formats: ImageProviderModel['formats'];
16
+ sizes: string[];
17
+ qualities: string[];
18
+ isDefault: boolean;
19
+ }
20
+ export interface DiscoveredModel {
21
+ id: string;
22
+ name: string;
23
+ }
24
+ export interface FrozenProviderConfig {
25
+ endpoint: string;
26
+ driver: 'openai';
27
+ model: ImageProviderModel;
28
+ }
29
+ /** Public directory only: no credential value and no inferred model availability. */
30
+ export declare class ImageProviders {
31
+ private readonly db;
32
+ readonly credentials?: ImageCredentials | undefined;
33
+ private readonly configured;
34
+ private readonly writes;
35
+ constructor(db: DatabaseSync, credentials?: ImageCredentials | undefined);
36
+ refreshCredentials(): Promise<void>;
37
+ credentialAvailable(providerId: string, revision: number): boolean;
38
+ private serialize;
39
+ setCredential(providerId: string, value: string, expected: unknown): Promise<ImageProvider>;
40
+ revokeCredential(providerId: string, expected: unknown): Promise<ImageProvider>;
41
+ /** Public config only. Previous accepted Runs can resolve this even after an edit or disable. */
42
+ frozen(providerId: string, configRevision: number, modelId: string): FrozenProviderConfig;
43
+ private saveVersion;
44
+ list(): ImageProvider[];
45
+ private get;
46
+ setProvider(raw: ProviderConfig, expected: unknown): ImageProvider;
47
+ deleteProvider(providerId: string, expected: unknown): void;
48
+ setModel(providerId: string, raw: ModelConfig, expected: unknown): ImageProvider;
49
+ syncDiscoveredModels(providerId: string, discovered: DiscoveredModel[], expected: unknown): ImageProvider;
50
+ deleteModel(providerId: string, modelId: string, expected: unknown): ImageProvider;
51
+ }
@@ -0,0 +1,271 @@
1
+ import { atomic } from './persistence/database.js';
2
+ import { ServiceError } from './errors.js';
3
+ import { ImageCredentials } from './image-credentials.js';
4
+ import { isPrivateNetworkHost } from './network.js';
5
+ const identity = (value) => {
6
+ if (typeof value !== 'string' || !/^[a-zA-Z0-9_-]{1,100}$/.test(value))
7
+ throw new ServiceError('INVALID_REQUEST', '服务商或模型 ID 无效。');
8
+ return value;
9
+ };
10
+ const modelIdentity = (value) => {
11
+ if (typeof value !== 'string' || !/^[a-zA-Z0-9_.:/-]{1,100}$/.test(value) || value === '.' || value === '..' || value.startsWith('/') || value.endsWith('/') || value.includes('//'))
12
+ throw new ServiceError('INVALID_REQUEST', '图片模型 ID 无效。');
13
+ return value;
14
+ };
15
+ const label = (value) => {
16
+ if (typeof value !== 'string' || !value.trim() || value.length > 150)
17
+ throw new ServiceError('INVALID_REQUEST', '显示名称无效。');
18
+ return value.trim();
19
+ };
20
+ const revision = (value) => {
21
+ if (!Number.isSafeInteger(value) || value < 0 || value >= Number.MAX_SAFE_INTEGER)
22
+ throw new ServiceError('INVALID_REQUEST', '配置修订号无效。');
23
+ return value;
24
+ };
25
+ function endpoint(value) {
26
+ if (typeof value !== 'string')
27
+ throw new ServiceError('INVALID_REQUEST', '服务商地址无效。');
28
+ let url;
29
+ try {
30
+ url = new URL(value);
31
+ }
32
+ catch {
33
+ throw new ServiceError('INVALID_REQUEST', '服务商地址无效。');
34
+ }
35
+ if (!(url.protocol === 'https:' || (url.protocol === 'http:' && isPrivateNetworkHost(url.hostname))) || url.username || url.password || url.search || url.hash || url.port === '0')
36
+ throw new ServiceError('INVALID_REQUEST', '服务商地址必须是 HTTPS;局域网或回环 IP 地址可使用 HTTP,且不得含凭据、查询或片段。');
37
+ const path = url.pathname.replace(/\/+$/, '');
38
+ return url.origin + (path && path !== '/' ? path : '');
39
+ }
40
+ function choices(value, allowed, field) {
41
+ if (!Array.isArray(value) || !value.length || value.length > 30 || value.some(item => typeof item !== 'string' || !allowed.includes(item)) || new Set(value).size !== value.length)
42
+ throw new ServiceError('INVALID_REQUEST', field + ' 能力列表无效。');
43
+ return value;
44
+ }
45
+ function inferredModel(model, isDefault) {
46
+ const id = modelIdentity(model.id), name = label(model.name);
47
+ if (id === 'dall-e-2')
48
+ return { id, name, modes: ['text', 'image'], formats: ['png'], sizes: ['256x256', '512x512', '1024x1024'], qualities: ['auto'], isDefault };
49
+ if (id === 'dall-e-3')
50
+ return { id, name, modes: ['text'], formats: ['png'], sizes: ['1024x1024', '1792x1024', '1024x1792'], qualities: ['auto', 'high'], isDefault };
51
+ return { id, name, modes: ['text', 'text_image'], formats: ['png', 'jpeg', 'webp'], sizes: ['auto', '1024x1024', '1536x1024', '1024x1536'], qualities: ['auto', 'high', 'medium', 'low'], isDefault };
52
+ }
53
+ /** Public directory only: no credential value and no inferred model availability. */
54
+ export class ImageProviders {
55
+ db;
56
+ credentials;
57
+ configured = new Set();
58
+ writes = new Map();
59
+ constructor(db, credentials) {
60
+ this.db = db;
61
+ this.credentials = credentials;
62
+ }
63
+ async refreshCredentials() {
64
+ this.configured.clear();
65
+ for (const row of this.db.prepare('SELECT id,credential_revision FROM image_providers WHERE deleted=0 AND credential_revision IS NOT NULL').all()) {
66
+ if (await this.credentials?.read(String(row.id), Number(row.credential_revision)))
67
+ this.configured.add(String(row.id));
68
+ }
69
+ }
70
+ credentialAvailable(providerId, revision) {
71
+ const row = this.db.prepare('SELECT 1 FROM image_provider_revoked_credentials WHERE provider_id=? AND revision=?').get(identity(providerId), revision);
72
+ return !row && this.configured.has(providerId);
73
+ }
74
+ serialize(id, work) {
75
+ const previous = this.writes.get(id) ?? Promise.resolve();
76
+ const next = previous.catch(() => { }).then(work);
77
+ this.writes.set(id, next);
78
+ void next.finally(() => { if (this.writes.get(id) === next)
79
+ this.writes.delete(id); }).catch(() => { });
80
+ return next;
81
+ }
82
+ async setCredential(providerId, value, expected) {
83
+ const id = identity(providerId), prev = revision(expected);
84
+ if (typeof value !== 'string' || !value.trim() || value.length > 16 * 1024 || value.includes(String.fromCharCode(10)) || value.includes(String.fromCharCode(13)))
85
+ throw new ServiceError('INVALID_REQUEST', '凭据格式无效。');
86
+ if (!this.credentials)
87
+ throw new ServiceError('MODEL_UNAVAILABLE', '本机凭据存储不可用。');
88
+ return this.serialize(id, async () => {
89
+ const before = this.db.prepare('SELECT revision,credential_counter,deleted FROM image_providers WHERE id=?').get(id);
90
+ if (!before || before.deleted)
91
+ throw new ServiceError('NOT_FOUND', '服务商不存在。');
92
+ if (Number(before.revision) !== prev)
93
+ throw new ServiceError('REVISION_CONFLICT', '服务商配置已变化,请刷新后重试。');
94
+ const next = Number(before.credential_counter) + 1;
95
+ await this.credentials.store(id, next, value);
96
+ try {
97
+ const provider = atomic(this.db, () => {
98
+ const changed = this.db.prepare('UPDATE image_providers SET credential_revision=?,credential_counter=?,revision=revision+1 WHERE id=? AND revision=? AND deleted=0').run(next, next, id, prev);
99
+ if (!changed.changes)
100
+ throw new ServiceError('REVISION_CONFLICT', '服务商配置已变化,请刷新后重试。');
101
+ this.db.prepare('UPDATE image_provider_models SET verified_at=NULL WHERE provider_id=?').run(id);
102
+ this.saveVersion(id);
103
+ return this.get(id);
104
+ });
105
+ this.configured.add(id);
106
+ return { ...provider, credentialConfigured: true };
107
+ }
108
+ catch (error) {
109
+ await this.credentials.revoke(id, next).catch(() => { });
110
+ throw error;
111
+ }
112
+ });
113
+ }
114
+ async revokeCredential(providerId, expected) {
115
+ const id = identity(providerId), prev = revision(expected);
116
+ return this.serialize(id, async () => {
117
+ const before = this.db.prepare('SELECT revision,credential_counter,deleted FROM image_providers WHERE id=?').get(id);
118
+ if (!before || before.deleted)
119
+ throw new ServiceError('NOT_FOUND', '服务商不存在。');
120
+ if (Number(before.revision) !== prev)
121
+ throw new ServiceError('REVISION_CONFLICT', '服务商配置已变化,请刷新后重试。');
122
+ const count = Number(before.credential_counter);
123
+ const provider = atomic(this.db, () => {
124
+ for (let rev = 1; rev <= count; rev++)
125
+ this.db.prepare('INSERT OR IGNORE INTO image_provider_revoked_credentials(provider_id,revision) VALUES(?,?)').run(id, rev);
126
+ this.db.prepare('UPDATE image_providers SET credential_revision=NULL,revision=revision+1 WHERE id=? AND revision=?').run(id, prev);
127
+ this.db.prepare('UPDATE image_provider_models SET verified_at=NULL WHERE provider_id=?').run(id);
128
+ this.saveVersion(id);
129
+ return this.get(id);
130
+ });
131
+ this.configured.delete(id);
132
+ for (let rev = 1; rev <= count; rev++)
133
+ await this.credentials?.revoke(id, rev).catch(() => { });
134
+ return { ...provider, credentialConfigured: false };
135
+ });
136
+ }
137
+ /** Public config only. Previous accepted Runs can resolve this even after an edit or disable. */
138
+ frozen(providerId, configRevision, modelId) {
139
+ const row = this.db.prepare('SELECT payload FROM image_provider_config_versions WHERE provider_id=? AND revision=?').get(identity(providerId), configRevision);
140
+ if (!row)
141
+ throw new ServiceError('MODEL_UNAVAILABLE', '冻结服务商配置版本不存在。');
142
+ const payload = JSON.parse(String(row.payload));
143
+ const model = payload.models.find(item => item.id === modelIdentity(modelId));
144
+ if (!model)
145
+ throw new ServiceError('MODEL_UNAVAILABLE', '冻结配置中没有所选模型。');
146
+ return { endpoint: payload.endpoint, driver: payload.driver, model: { ...model, verifiedAt: null } };
147
+ }
148
+ saveVersion(id) {
149
+ const provider = this.get(id);
150
+ this.db.prepare('INSERT INTO image_provider_config_versions(provider_id,revision,payload) VALUES(?,?,?)').run(id, provider.revision, JSON.stringify({ endpoint: provider.endpoint, driver: provider.driver, models: provider.models.map(({ verifiedAt: _verifiedAt, ...model }) => model) }));
151
+ }
152
+ list() {
153
+ const providers = this.db.prepare('SELECT * FROM image_providers WHERE deleted=0 ORDER BY id').all();
154
+ return providers.map(row => {
155
+ const models = this.db.prepare('SELECT * FROM image_provider_models WHERE provider_id=? ORDER BY id').all(String(row.id)).map(model => ({
156
+ id: String(model.id), name: String(model.name), modes: JSON.parse(String(model.modes)), formats: JSON.parse(String(model.formats)), sizes: JSON.parse(String(model.sizes)), qualities: JSON.parse(String(model.qualities)), isDefault: Boolean(model.is_default), verifiedAt: model.verified_at === null ? null : String(model.verified_at),
157
+ }));
158
+ return { id: String(row.id), name: String(row.name), driver: 'openai', endpoint: String(row.endpoint), enabled: Boolean(row.enabled), revision: Number(row.revision), credentialConfigured: this.configured.has(String(row.id)), credentialRevision: row.credential_revision === null ? null : Number(row.credential_revision), models };
159
+ });
160
+ }
161
+ get(id) {
162
+ const provider = this.list().find(item => item.id === id);
163
+ if (!provider)
164
+ throw new ServiceError('NOT_FOUND', '生图服务商不存在。');
165
+ return provider;
166
+ }
167
+ setProvider(raw, expected) {
168
+ const id = identity(raw?.id), name = label(raw.name), target = endpoint(raw.endpoint), prev = revision(expected);
169
+ if (raw.driver !== 'openai' || typeof raw.enabled !== 'boolean')
170
+ throw new ServiceError('INVALID_REQUEST', '服务商配置无效。');
171
+ return atomic(this.db, () => {
172
+ const current = this.db.prepare('SELECT revision,deleted,endpoint FROM image_providers WHERE id=?').get(id);
173
+ if (current?.deleted)
174
+ throw new ServiceError('CONFLICT', '已删除服务商不能重新使用原 ID。');
175
+ if (current ? Number(current.revision) !== prev : prev !== 0)
176
+ throw new ServiceError('REVISION_CONFLICT', '服务商配置已变化,请刷新后重试。');
177
+ if (current)
178
+ this.db.prepare('UPDATE image_providers SET name=?,endpoint=?,enabled=?,revision=revision+1 WHERE id=? AND revision=?').run(name, target, Number(raw.enabled), id, prev);
179
+ else
180
+ this.db.prepare('INSERT INTO image_providers(id,name,driver,endpoint,enabled,revision) VALUES(?,?,?,?,?,1)').run(id, name, raw.driver, target, Number(raw.enabled));
181
+ if (current && current.endpoint !== target)
182
+ this.db.prepare('UPDATE image_provider_models SET verified_at=NULL WHERE provider_id=?').run(id);
183
+ this.saveVersion(id);
184
+ return this.get(id);
185
+ });
186
+ }
187
+ deleteProvider(providerId, expected) {
188
+ const id = identity(providerId), prev = revision(expected);
189
+ atomic(this.db, () => {
190
+ const row = this.db.prepare('SELECT revision,deleted FROM image_providers WHERE id=?').get(id);
191
+ if (!row || row.deleted)
192
+ throw new ServiceError('NOT_FOUND', '生图服务商不存在。');
193
+ if (Number(row.revision) !== prev)
194
+ throw new ServiceError('REVISION_CONFLICT', '服务商配置已变化,请刷新后重试。');
195
+ this.db.prepare('UPDATE image_providers SET deleted=1,enabled=0,revision=revision+1 WHERE id=? AND revision=?').run(id, prev);
196
+ this.db.prepare('UPDATE image_provider_models SET verified_at=NULL WHERE provider_id=?').run(id);
197
+ });
198
+ }
199
+ setModel(providerId, raw, expected) {
200
+ const id = identity(providerId), modelId = modelIdentity(raw?.id), name = label(raw.name), prev = revision(expected);
201
+ const modes = choices(raw.modes, ['text', 'image', 'text_image'], '输入模式');
202
+ const formats = choices(raw.formats, ['png', 'jpeg', 'webp'], '格式');
203
+ const sizes = choices(raw.sizes, raw.sizes ?? [], '尺寸');
204
+ const qualities = choices(raw.qualities, raw.qualities ?? [], '质量');
205
+ if (sizes.some(item => item.length > 60) || qualities.some(item => item.length > 60) || typeof raw.isDefault !== 'boolean')
206
+ throw new ServiceError('INVALID_REQUEST', '模型参数无效。');
207
+ return atomic(this.db, () => {
208
+ const current = this.db.prepare('SELECT revision,deleted FROM image_providers WHERE id=?').get(id);
209
+ if (!current || current.deleted)
210
+ throw new ServiceError('NOT_FOUND', '生图服务商不存在。');
211
+ if (Number(current.revision) !== prev)
212
+ throw new ServiceError('REVISION_CONFLICT', '服务商配置已变化,请刷新后重试。');
213
+ if (raw.isDefault)
214
+ this.db.prepare('UPDATE image_provider_models SET is_default=0 WHERE provider_id=?').run(id);
215
+ // An edited capability or model identity loses any prior real verification.
216
+ this.db.prepare('INSERT INTO image_provider_models(provider_id,id,name,modes,formats,sizes,qualities,is_default) VALUES(?,?,?,?,?,?,?,?) ON CONFLICT(provider_id,id) DO UPDATE SET name=excluded.name,modes=excluded.modes,formats=excluded.formats,sizes=excluded.sizes,qualities=excluded.qualities,is_default=excluded.is_default,verified_at=NULL').run(id, modelId, name, JSON.stringify(modes), JSON.stringify(formats), JSON.stringify(sizes), JSON.stringify(qualities), Number(raw.isDefault));
217
+ this.db.prepare('UPDATE image_providers SET revision=revision+1 WHERE id=? AND revision=?').run(id, prev);
218
+ this.saveVersion(id);
219
+ return this.get(id);
220
+ });
221
+ }
222
+ syncDiscoveredModels(providerId, discovered, expected) {
223
+ const id = identity(providerId), prev = revision(expected);
224
+ if (!Array.isArray(discovered) || discovered.length > 1000)
225
+ throw new ServiceError('INVALID_REQUEST', '模型选择无效。');
226
+ const unique = new Map();
227
+ for (const item of discovered) {
228
+ const modelId = modelIdentity(item?.id), name = label(item?.name);
229
+ if (unique.has(modelId))
230
+ throw new ServiceError('INVALID_REQUEST', '模型选择包含重复项。');
231
+ unique.set(modelId, { id: modelId, name });
232
+ }
233
+ return atomic(this.db, () => {
234
+ const provider = this.db.prepare('SELECT revision,deleted FROM image_providers WHERE id=?').get(id);
235
+ if (!provider || provider.deleted)
236
+ throw new ServiceError('NOT_FOUND', '生图服务商不存在。');
237
+ if (Number(provider.revision) !== prev)
238
+ throw new ServiceError('REVISION_CONFLICT', '服务商配置已变化,请刷新后重试。');
239
+ const existing = this.db.prepare('SELECT * FROM image_provider_models WHERE provider_id=?').all(id);
240
+ const currentDefault = existing.find(row => Boolean(row.is_default) && unique.has(String(row.id)));
241
+ const defaultId = currentDefault ? String(currentDefault.id) : unique.keys().next().value;
242
+ this.db.prepare('DELETE FROM image_provider_models WHERE provider_id=?').run(id);
243
+ for (const item of unique.values()) {
244
+ const config = inferredModel(item, item.id === defaultId);
245
+ const old = existing.find(row => String(row.id) === config.id);
246
+ const same = old && String(old.name) === config.name && String(old.modes) === JSON.stringify(config.modes) && String(old.formats) === JSON.stringify(config.formats) && String(old.sizes) === JSON.stringify(config.sizes) && String(old.qualities) === JSON.stringify(config.qualities);
247
+ this.db.prepare('INSERT INTO image_provider_models(provider_id,id,name,modes,formats,sizes,qualities,is_default,verified_at) VALUES(@provider,@id,@name,@modes,@formats,@sizes,@qualities,@isDefault,@verifiedAt)').run({ provider: id, id: config.id, name: config.name, modes: JSON.stringify(config.modes), formats: JSON.stringify(config.formats), sizes: JSON.stringify(config.sizes), qualities: JSON.stringify(config.qualities), isDefault: Number(config.isDefault), verifiedAt: same ? (old.verified_at ?? null) : null });
248
+ }
249
+ this.db.prepare('UPDATE image_providers SET revision=revision+1 WHERE id=? AND revision=?').run(id, prev);
250
+ this.saveVersion(id);
251
+ return this.get(id);
252
+ });
253
+ }
254
+ deleteModel(providerId, modelId, expected) {
255
+ const id = identity(providerId), model = modelIdentity(modelId), prev = revision(expected);
256
+ return atomic(this.db, () => {
257
+ const provider = this.db.prepare('SELECT revision,deleted FROM image_providers WHERE id=?').get(id);
258
+ if (!provider || provider.deleted)
259
+ throw new ServiceError('NOT_FOUND', '服务商不存在。');
260
+ if (Number(provider.revision) !== prev)
261
+ throw new ServiceError('REVISION_CONFLICT', '服务商配置已变化,请刷新后重试。');
262
+ const removed = this.db.prepare('DELETE FROM image_provider_models WHERE provider_id=? AND id=?').run(id, model);
263
+ if (!removed.changes)
264
+ throw new ServiceError('NOT_FOUND', '图片模型不存在。');
265
+ this.db.prepare('UPDATE image_providers SET revision=revision+1 WHERE id=? AND revision=?').run(id, prev);
266
+ this.saveVersion(id);
267
+ return this.get(id);
268
+ });
269
+ }
270
+ }
271
+ //# sourceMappingURL=image-providers.js.map
@@ -0,0 +1,17 @@
1
+ import type { InputSnapshot } from "./vendor/protocol/index.js";
2
+ export interface MaterializedInputs {
3
+ directory: string;
4
+ files: {
5
+ resourceId: string;
6
+ version: number;
7
+ path: string;
8
+ sha256: string;
9
+ }[];
10
+ }
11
+ export interface MaterializeInputOptions {
12
+ inputDirectory: string;
13
+ blobDirectory: string;
14
+ resolveBlobPath?: (sha256: string) => string;
15
+ }
16
+ /** Async preparation outside SQLite transactions; only the newly created private directory is written. */
17
+ export declare function materializeInputs(snapshot: InputSnapshot, options: MaterializeInputOptions): Promise<MaterializedInputs>;