create-windy 0.3.16 → 0.3.18
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 +14 -36
- package/dist/cli.js +1831 -3178
- package/package.json +1 -1
- package/template/.windy-template.json +2 -2
- package/template/AGENTS.md +7 -1
- package/template/README.md +8 -11
- package/template/apps/agent-server/Dockerfile +2 -1
- package/template/apps/agent-server/README.md +13 -5
- package/template/apps/agent-server/adk_web_agents/canonical_debug/__init__.py +1 -0
- package/template/apps/agent-server/adk_web_agents/canonical_debug/agent.py +125 -0
- package/template/apps/agent-server/project/adk-web-debug.json +4 -0
- package/template/apps/agent-server/src/southwind_agent_server/adk_debug.py +315 -0
- package/template/apps/agent-server/tests/test_adk_web_canonical_debug.py +235 -0
- package/template/apps/server/Dockerfile +1 -1
- package/template/apps/server/src/agent/run-debug-authorization.ts +27 -0
- package/template/apps/server/src/agent/run-facade-adk-debug.test.ts +56 -0
- package/template/apps/server/src/agent/run-facade.test-support.ts +235 -0
- package/template/apps/server/src/agent/run-facade.test.ts +8 -244
- package/template/apps/server/src/agent/run-facade.ts +23 -48
- package/template/apps/server/src/agent/run-operation-registry.ts +51 -0
- package/template/apps/server/src/agent/runtime-bootstrap.ts +5 -2
- package/template/apps/server/src/agent/tool-audit.ts +3 -0
- package/template/apps/server/src/agent/tool-registry.test.ts +29 -0
- package/template/apps/server/src/application-services.ts +83 -15
- package/template/apps/server/src/audit/contracts.ts +8 -0
- package/template/apps/server/src/audit/recovery-queue.test.ts +48 -1
- package/template/apps/server/src/audit/recovery-queue.ts +73 -4
- package/template/apps/server/src/audit/reliable-writer.test.ts +120 -2
- package/template/apps/server/src/audit/reliable-writer.ts +98 -34
- package/template/apps/server/src/guards.ts +16 -0
- package/template/apps/server/src/index.ts +23 -20
- package/template/apps/server/src/jobs/legacy-adapter.integration.test.ts +0 -1
- package/template/apps/server/src/jobs/legacy-adapter.test.ts +6 -0
- package/template/apps/server/src/jobs/legacy-service-runtime.ts +18 -0
- package/template/apps/server/src/module-host/durable-context-unavailable.test.ts +84 -0
- package/template/apps/server/src/module-host/durable-context.test.ts +14 -0
- package/template/apps/server/src/module-host/durable-context.ts +17 -15
- package/template/apps/server/src/module-host/request-context.test.ts +30 -1
- package/template/apps/server/src/module-host/request-context.ts +15 -2
- package/template/apps/server/src/module-host/storage-port.ts +1 -1
- package/template/apps/server/src/platform-bootstrap.ts +16 -0
- package/template/apps/server/src/runtime-adk-debug.test.ts +246 -0
- package/template/apps/server/src/runtime-adk-debug.ts +158 -0
- package/template/apps/server/src/runtime-request-actor.test.ts +55 -0
- package/template/apps/server/src/runtime-request-actor.ts +22 -0
- package/template/apps/server/src/runtime.test.ts +3 -0
- package/template/apps/server/src/runtime.ts +36 -3
- package/template/apps/server/src/scheduler/definitions.test.ts +16 -0
- package/template/apps/web/Dockerfile +1 -1
- package/template/docker-compose.agent-debug.yml +56 -0
- package/template/docker-compose.yml +1 -39
- package/template/docs/architecture/ai-runtime.md +14 -5
- package/template/docs/architecture/module-manifest-composition.md +14 -0
- package/template/docs/development/custom-login-page.md +2 -16
- package/template/docs/platform/agent-runtime.md +57 -16
- package/template/docs/platform/agent-tool-sandbox.md +4 -3
- package/template/docs/platform/audit-reliability.md +33 -5
- package/template/docs/platform/platform-authentication.md +9 -1
- package/template/package.json +4 -3
- package/template/packages/database/drizzle/0043_activate_agent_dependencies.sql +196 -0
- package/template/packages/database/drizzle/meta/0043_snapshot.json +8335 -0
- package/template/packages/database/drizzle/meta/_journal.json +7 -0
- package/template/packages/database/src/schema-notification-delivery.test.ts +15 -6
- package/template/packages/database/src/schema-workflow-notification.test.ts +44 -29
- package/template/scripts/dev-adk-web.ts +107 -0
|
@@ -2,6 +2,7 @@ import type { AuditEvent } from "@southwind-ai/shared";
|
|
|
2
2
|
import type { AuditEventWriter } from "../runtime.js";
|
|
3
3
|
import {
|
|
4
4
|
AuditWriteUnavailableError,
|
|
5
|
+
type AuditRecoveryFailure,
|
|
5
6
|
type AuditWritePolicy,
|
|
6
7
|
type AuditWriteStatus,
|
|
7
8
|
} from "./contracts.js";
|
|
@@ -69,45 +70,62 @@ export class ReliableAuditWriter implements AuditEventWriter {
|
|
|
69
70
|
await this.handlePersistenceFailure(redacted, policy);
|
|
70
71
|
return;
|
|
71
72
|
}
|
|
72
|
-
if (this.current.queued > 0) {
|
|
73
|
-
|
|
74
|
-
await this.recover();
|
|
75
|
-
} catch {
|
|
76
|
-
// 当前事件已经持久化;恢复队列故障只触发告警,不回滚成功写入。
|
|
77
|
-
}
|
|
73
|
+
if (this.current.queued > 0 || this.current.recoveryFailure) {
|
|
74
|
+
await this.recover();
|
|
78
75
|
}
|
|
79
76
|
}
|
|
80
77
|
|
|
81
78
|
async recover(): Promise<void> {
|
|
82
|
-
|
|
83
|
-
await this.
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
await this.queue.replace(pending);
|
|
101
|
-
this.current.queued = pending.length;
|
|
102
|
-
if (pending.length) {
|
|
103
|
-
this.markDegraded("关键审计恢复仍在等待持久化");
|
|
104
|
-
} else {
|
|
105
|
-
this.refreshState();
|
|
79
|
+
await this.withQueueLock(async () => {
|
|
80
|
+
const events = await this.readRecoveryQueue();
|
|
81
|
+
if (!events) return;
|
|
82
|
+
if (!events.length) {
|
|
83
|
+
this.current.queued = 0;
|
|
84
|
+
this.clearRecoveryFailure();
|
|
85
|
+
this.refreshState();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const pending: AuditEvent[] = [];
|
|
89
|
+
for (let index = 0; index < events.length; index += 1) {
|
|
90
|
+
try {
|
|
91
|
+
await this.persistWithRetry(events[index]!);
|
|
92
|
+
this.markSuccess();
|
|
93
|
+
} catch {
|
|
94
|
+
pending.push(...events.slice(index));
|
|
95
|
+
break;
|
|
106
96
|
}
|
|
107
|
-
}
|
|
97
|
+
}
|
|
98
|
+
if (!(await this.replaceRecoveryQueue(pending, events.length))) return;
|
|
99
|
+
this.current.queued = pending.length;
|
|
100
|
+
this.clearRecoveryFailure();
|
|
101
|
+
if (pending.length) {
|
|
102
|
+
this.markDegraded("关键审计恢复仍在等待持久化");
|
|
103
|
+
} else {
|
|
104
|
+
this.refreshState();
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private async readRecoveryQueue(): Promise<AuditEvent[] | undefined> {
|
|
110
|
+
try {
|
|
111
|
+
return await this.queue.read();
|
|
108
112
|
} catch (error) {
|
|
109
|
-
this.
|
|
110
|
-
|
|
113
|
+
this.markRecoveryDegraded(error);
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private async replaceRecoveryQueue(
|
|
119
|
+
events: AuditEvent[],
|
|
120
|
+
currentQueueSize: number,
|
|
121
|
+
): Promise<boolean> {
|
|
122
|
+
try {
|
|
123
|
+
await this.queue.replace(events);
|
|
124
|
+
return true;
|
|
125
|
+
} catch (error) {
|
|
126
|
+
this.current.queued = currentQueueSize;
|
|
127
|
+
this.markRecoveryDegraded(error);
|
|
128
|
+
return false;
|
|
111
129
|
}
|
|
112
130
|
}
|
|
113
131
|
|
|
@@ -133,6 +151,7 @@ export class ReliableAuditWriter implements AuditEventWriter {
|
|
|
133
151
|
await this.queue.replace([...events, event]);
|
|
134
152
|
this.current.queued = events.length + 1;
|
|
135
153
|
});
|
|
154
|
+
this.clearRecoveryFailure();
|
|
136
155
|
this.markDegraded("关键审计已进入可恢复队列");
|
|
137
156
|
} catch {
|
|
138
157
|
this.markDegraded("关键审计写入和恢复队列均不可用");
|
|
@@ -169,8 +188,20 @@ export class ReliableAuditWriter implements AuditEventWriter {
|
|
|
169
188
|
this.options.onAlert?.(this.status());
|
|
170
189
|
}
|
|
171
190
|
|
|
191
|
+
private markRecoveryDegraded(error: unknown): void {
|
|
192
|
+
const recoveryFailure = classifyRecoveryFailure(error);
|
|
193
|
+
this.current.recoveryFailure = recoveryFailure;
|
|
194
|
+
this.current.lastRecoveryFailureAt = this.now().toISOString();
|
|
195
|
+
this.markDegraded(recoveryFailureAlert(recoveryFailure));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private clearRecoveryFailure(): void {
|
|
199
|
+
delete this.current.recoveryFailure;
|
|
200
|
+
delete this.current.lastRecoveryFailureAt;
|
|
201
|
+
}
|
|
202
|
+
|
|
172
203
|
private refreshState(): void {
|
|
173
|
-
if (this.current.queued === 0) {
|
|
204
|
+
if (this.current.queued === 0 && !this.current.recoveryFailure) {
|
|
174
205
|
this.current.state = "healthy";
|
|
175
206
|
delete this.current.alert;
|
|
176
207
|
}
|
|
@@ -199,3 +230,36 @@ function boundedInteger(
|
|
|
199
230
|
function sleep(milliseconds: number): Promise<void> {
|
|
200
231
|
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
201
232
|
}
|
|
233
|
+
|
|
234
|
+
function classifyRecoveryFailure(error: unknown): AuditRecoveryFailure {
|
|
235
|
+
if (error instanceof SyntaxError) {
|
|
236
|
+
return "invalid-format";
|
|
237
|
+
}
|
|
238
|
+
if (
|
|
239
|
+
typeof error === "object" &&
|
|
240
|
+
error !== null &&
|
|
241
|
+
"code" in error &&
|
|
242
|
+
(error.code === "EACCES" || error.code === "EPERM")
|
|
243
|
+
) {
|
|
244
|
+
return "permission-denied";
|
|
245
|
+
}
|
|
246
|
+
if (
|
|
247
|
+
typeof error === "object" &&
|
|
248
|
+
error !== null &&
|
|
249
|
+
"code" in error &&
|
|
250
|
+
typeof error.code === "string"
|
|
251
|
+
) {
|
|
252
|
+
return "io-error";
|
|
253
|
+
}
|
|
254
|
+
return "unknown";
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function recoveryFailureAlert(failure: AuditRecoveryFailure): string {
|
|
258
|
+
const detail = {
|
|
259
|
+
"permission-denied": "文件权限不足",
|
|
260
|
+
"invalid-format": "队列文件格式无效",
|
|
261
|
+
"io-error": "文件读写失败",
|
|
262
|
+
unknown: "未知错误",
|
|
263
|
+
} satisfies Record<AuditRecoveryFailure, string>;
|
|
264
|
+
return `关键审计恢复队列不可用:${detail[failure]}`;
|
|
265
|
+
}
|
|
@@ -29,6 +29,22 @@ export interface RequestActor {
|
|
|
29
29
|
scopeContext?: ScopeContext;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
// @windy-module system.agent begin
|
|
33
|
+
const moduleUserProjection = new WeakMap<RequestActor, string>();
|
|
34
|
+
|
|
35
|
+
export function bindModuleUserProjection(
|
|
36
|
+
actor: RequestActor,
|
|
37
|
+
userId: string,
|
|
38
|
+
): RequestActor {
|
|
39
|
+
moduleUserProjection.set(actor, userId);
|
|
40
|
+
return actor;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function projectedModuleUserId(actor: RequestActor): string | undefined {
|
|
44
|
+
return moduleUserProjection.get(actor);
|
|
45
|
+
}
|
|
46
|
+
// @windy-module system.agent end
|
|
47
|
+
|
|
32
48
|
export interface RequestGuardContext {
|
|
33
49
|
requestId: string;
|
|
34
50
|
actor: RequestActor;
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { loadPlatformConfig } from "@southwind-ai/config";
|
|
2
|
-
import pino from "pino";
|
|
3
1
|
import { registerAdminBootstrapRoute } from "./admin/routes.js";
|
|
4
2
|
import { registerSessionContextRoute } from "./session/routes.js";
|
|
5
3
|
import { registerFoundationRoute } from "./foundation-route.js";
|
|
@@ -19,9 +17,9 @@ import { readPositiveInt } from "./runtime-config.js";
|
|
|
19
17
|
// @windy-module system.agent begin
|
|
20
18
|
import { registerAgentPlatformRuntime } from "./agent/runtime-bootstrap.js";
|
|
21
19
|
import { DeferredAgentBackgroundOperationPortFactory } from "./agent/background-operation-port.js";
|
|
20
|
+
import { resolveCurrentSystemActor } from "./system/actor.js";
|
|
22
21
|
// @windy-module system.agent end
|
|
23
22
|
import { createAuthRuntime } from "./auth/runtime.js";
|
|
24
|
-
import { createServerPersistence } from "./persistence.js";
|
|
25
23
|
// @windy-module system.license begin
|
|
26
24
|
import { registerPlatformLicenseRoutes } from "./license/routes.js";
|
|
27
25
|
import {
|
|
@@ -70,16 +68,8 @@ import {
|
|
|
70
68
|
listenPlatformServer,
|
|
71
69
|
logServerStartup,
|
|
72
70
|
} from "./platform-http-runtime.js";
|
|
73
|
-
|
|
74
|
-
const logger
|
|
75
|
-
const platformConfig = loadPlatformConfig();
|
|
76
|
-
const persistence = createServerPersistence(
|
|
77
|
-
process.env,
|
|
78
|
-
(status) => {
|
|
79
|
-
logger.error({ audit: status }, "审计持久化进入降级状态");
|
|
80
|
-
},
|
|
81
|
-
platformConfig,
|
|
82
|
-
);
|
|
71
|
+
import { createPlatformBootstrap } from "./platform-bootstrap.js";
|
|
72
|
+
const { logger, persistence, platformConfig } = createPlatformBootstrap();
|
|
83
73
|
// @windy-module system.audit begin
|
|
84
74
|
await persistence?.auditWriter.recover();
|
|
85
75
|
// @windy-module system.audit end
|
|
@@ -105,6 +95,10 @@ const useInstalledLicense =
|
|
|
105
95
|
// @windy-module system.license end
|
|
106
96
|
const runtime = createServerRuntime(process.env, {
|
|
107
97
|
actorResolver: persistence?.actorResolver,
|
|
98
|
+
// @windy-module system.agent begin
|
|
99
|
+
adkDebugActorResolver: (actorId) =>
|
|
100
|
+
resolveCurrentSystemActor(systemRepositories, actorId),
|
|
101
|
+
// @windy-module system.agent end
|
|
108
102
|
// @windy-module system.license begin
|
|
109
103
|
licenseResolver: useInstalledLicense
|
|
110
104
|
? platformLicenseRuntime
|
|
@@ -145,7 +139,9 @@ const storageRuntime = createStorageRuntime(
|
|
|
145
139
|
persistence?.db,
|
|
146
140
|
moduleHosts.uploadPolicies,
|
|
147
141
|
);
|
|
142
|
+
// @windy-module system.configuration begin
|
|
148
143
|
configurationRuntime.bindStorage(storageRuntime.uploads);
|
|
144
|
+
// @windy-module system.configuration end
|
|
149
145
|
// @windy-module system.storage end
|
|
150
146
|
// @windy-module system.settings begin
|
|
151
147
|
const platformSettings = createPlatformSettingsRuntime(
|
|
@@ -173,23 +169,37 @@ const moduleTaskDefinitions = createModuleTaskDefinitions(
|
|
|
173
169
|
const moduleBackgroundAi = new DeferredAgentBackgroundOperationPortFactory();
|
|
174
170
|
// @windy-module system.agent end
|
|
175
171
|
const {
|
|
172
|
+
// @windy-module system.bulk-data begin
|
|
176
173
|
bulkDataService,
|
|
174
|
+
// @windy-module system.bulk-data end
|
|
177
175
|
scheduler,
|
|
178
176
|
taskDefinitions: scheduledTaskDefinitions,
|
|
177
|
+
// @windy-module system.notification begin
|
|
179
178
|
notifications: notificationRepository,
|
|
180
179
|
notificationService,
|
|
180
|
+
// @windy-module system.notification end
|
|
181
|
+
// @windy-module system.workflow begin
|
|
181
182
|
workflow: workflowService,
|
|
183
|
+
// @windy-module system.workflow end
|
|
184
|
+
// @windy-module system.data-governance begin
|
|
182
185
|
governedExportService,
|
|
186
|
+
// @windy-module system.data-governance end
|
|
183
187
|
durable: durableJobRuntime,
|
|
184
188
|
} = await createApplicationServices({
|
|
185
189
|
persistence,
|
|
186
190
|
systemRepositories,
|
|
187
191
|
foundation: foundationSnapshot,
|
|
188
192
|
runtime,
|
|
193
|
+
// @windy-module system.storage begin
|
|
189
194
|
storage: storageRuntime,
|
|
195
|
+
// @windy-module system.storage end
|
|
190
196
|
moduleTasks: moduleTaskDefinitions,
|
|
197
|
+
// @windy-module system.bulk-data begin
|
|
191
198
|
bulkRetentionDays: readPositiveInt(process.env.BULK_DATA_RETENTION_DAYS, 7),
|
|
199
|
+
// @windy-module system.bulk-data end
|
|
200
|
+
// @windy-module system.notification begin
|
|
192
201
|
mailConfig: platformConfig.mail,
|
|
202
|
+
// @windy-module system.notification end
|
|
193
203
|
moduleHosts,
|
|
194
204
|
installModuleDurableHandlers: moduleHosts.installDurableHandlers,
|
|
195
205
|
// @windy-module system.agent begin
|
|
@@ -214,11 +224,9 @@ const app = await createPlatformServerApp({
|
|
|
214
224
|
logger,
|
|
215
225
|
observability,
|
|
216
226
|
});
|
|
217
|
-
|
|
218
227
|
// @windy-module system.license begin
|
|
219
228
|
registerPlatformLicenseRoutes(app, platformLicenseRuntime);
|
|
220
229
|
// @windy-module system.license end
|
|
221
|
-
|
|
222
230
|
// @windy-module system.settings begin
|
|
223
231
|
platformSettings.register(app, runtime, features);
|
|
224
232
|
// @windy-module system.settings end
|
|
@@ -254,16 +262,13 @@ registerStorageRoutes(
|
|
|
254
262
|
// @windy-module system.search begin
|
|
255
263
|
searchHost.register(app);
|
|
256
264
|
// @windy-module system.search end
|
|
257
|
-
|
|
258
265
|
registerAdminBootstrapRoute(app, runtime, {
|
|
259
266
|
modules: foundationModules,
|
|
260
267
|
snapshot: foundationSnapshot,
|
|
261
268
|
loadMenus: () => systemRepositories.menuRegistry.tree(),
|
|
262
269
|
});
|
|
263
270
|
registerSessionContextRoute(app, runtime, features);
|
|
264
|
-
|
|
265
271
|
registerFoundationRoute(app, runtime, foundationSnapshot, systemRepositories);
|
|
266
|
-
|
|
267
272
|
// @windy-module system.data-governance begin
|
|
268
273
|
registerGovernedUserExportRoutes(app, governedExportService, runtime, features);
|
|
269
274
|
// @windy-module system.data-governance end
|
|
@@ -323,7 +328,6 @@ installModuleHttpRuntime(moduleHosts, app, {
|
|
|
323
328
|
governedExportService,
|
|
324
329
|
// @windy-module system.data-governance end
|
|
325
330
|
});
|
|
326
|
-
|
|
327
331
|
validateInstalledRuntime({
|
|
328
332
|
modules: foundationModules,
|
|
329
333
|
routes: app.routes,
|
|
@@ -338,7 +342,6 @@ validateInstalledRuntime({
|
|
|
338
342
|
// @windy-module system.search end
|
|
339
343
|
hosts: moduleHosts,
|
|
340
344
|
});
|
|
341
|
-
|
|
342
345
|
const port = listenPlatformServer(app);
|
|
343
346
|
// @windy-module system.scheduler begin
|
|
344
347
|
schedulerWorker.start();
|
|
@@ -5,9 +5,11 @@ import { createServerRuntime } from "../runtime.js";
|
|
|
5
5
|
import { InMemorySchedulerRepository } from "../scheduler/repository.js";
|
|
6
6
|
import { SchedulerService } from "../scheduler/service.js";
|
|
7
7
|
import type { SchedulerClock } from "../scheduler/types.js";
|
|
8
|
+
// @windy-module system.bulk-data begin
|
|
8
9
|
import { InMemoryBulkDataRepository } from "../bulk-data/repository.js";
|
|
9
10
|
import { BulkDataService } from "../bulk-data/service.js";
|
|
10
11
|
import type { BulkResourceHandler } from "../bulk-data/resource-handler.js";
|
|
12
|
+
// @windy-module system.bulk-data end
|
|
11
13
|
import { InMemoryDurableExecutionRepository } from "./in-memory-execution.js";
|
|
12
14
|
import {
|
|
13
15
|
DeferredLegacyExecutor,
|
|
@@ -17,6 +19,7 @@ import { DurableJobService } from "./service.js";
|
|
|
17
19
|
import { DurableJobWorker } from "./worker.js";
|
|
18
20
|
|
|
19
21
|
describe("legacy durable adapters", () => {
|
|
22
|
+
// @windy-module system.bulk-data begin
|
|
20
23
|
test("running Bulk 在进程重启后回收 lease,重复启动与完成不重复执行", async () => {
|
|
21
24
|
let now = new Date("2026-07-16T00:00:00.000Z");
|
|
22
25
|
const legacy = new InMemoryBulkDataRepository();
|
|
@@ -107,6 +110,7 @@ describe("legacy durable adapters", () => {
|
|
|
107
110
|
processedRows: 1,
|
|
108
111
|
});
|
|
109
112
|
});
|
|
113
|
+
// @windy-module system.bulk-data end
|
|
110
114
|
|
|
111
115
|
test("Scheduler 采用未关联 running 记录且重放已完成记录为空操作", async () => {
|
|
112
116
|
const now = new Date("2026-07-16T00:00:00.000Z");
|
|
@@ -179,6 +183,7 @@ describe("legacy durable adapters", () => {
|
|
|
179
183
|
});
|
|
180
184
|
});
|
|
181
185
|
|
|
186
|
+
// @windy-module system.bulk-data begin
|
|
182
187
|
function testBulkHandler(): BulkResourceHandler {
|
|
183
188
|
return {
|
|
184
189
|
scopeMode: "platform",
|
|
@@ -197,3 +202,4 @@ function actor(id: string) {
|
|
|
197
202
|
permissionKeys: [],
|
|
198
203
|
};
|
|
199
204
|
}
|
|
205
|
+
// @windy-module system.bulk-data end
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { DurableHandlerRegistry } from "@southwind-ai/jobs";
|
|
2
|
+
// @windy-module system.bulk-data begin
|
|
2
3
|
import type { BlobStore } from "@southwind-ai/storage";
|
|
4
|
+
// @windy-module system.bulk-data end
|
|
3
5
|
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
6
|
+
// @windy-module system.bulk-data begin
|
|
4
7
|
import { BulkDataService } from "../bulk-data/service.js";
|
|
5
8
|
import type { BulkDataLimits } from "../bulk-data/input-validation.js";
|
|
6
9
|
import type { BulkDataRepository } from "../bulk-data/repository.js";
|
|
7
10
|
import type { BulkResourceHandler } from "../bulk-data/resource-handler.js";
|
|
11
|
+
// @windy-module system.bulk-data end
|
|
8
12
|
import type { createServerRuntime } from "../runtime.js";
|
|
9
13
|
import type { SchedulerRepository } from "../scheduler/repository.js";
|
|
10
14
|
import { SchedulerService } from "../scheduler/service.js";
|
|
@@ -18,19 +22,25 @@ import {
|
|
|
18
22
|
} from "./legacy-adapter.js";
|
|
19
23
|
import { DurableJobService } from "./service.js";
|
|
20
24
|
import { DurableJobWorker, type DurableWorkerOptions } from "./worker.js";
|
|
25
|
+
// @windy-module system.workflow begin
|
|
21
26
|
import type { WorkflowService } from "../workflow/service.js";
|
|
22
27
|
import { WorkflowTimeoutRuntime } from "../workflow/timeout-runtime.js";
|
|
28
|
+
// @windy-module system.workflow end
|
|
23
29
|
|
|
24
30
|
interface LegacyServiceRuntimeInput {
|
|
25
31
|
db?: NodePgDatabase;
|
|
32
|
+
// @windy-module system.bulk-data begin
|
|
26
33
|
bulkRepository: BulkDataRepository;
|
|
27
34
|
bulkHandlers: BulkResourceHandler[];
|
|
28
35
|
bulkLimits: Partial<BulkDataLimits>;
|
|
29
36
|
blobStore: BlobStore;
|
|
37
|
+
// @windy-module system.bulk-data end
|
|
30
38
|
schedulerRepository: SchedulerRepository;
|
|
31
39
|
taskDefinitions: ScheduledTaskDefinition[];
|
|
32
40
|
runtime: ReturnType<typeof createServerRuntime>;
|
|
41
|
+
// @windy-module system.workflow begin
|
|
33
42
|
workflow: WorkflowService;
|
|
43
|
+
// @windy-module system.workflow end
|
|
34
44
|
installModuleDurableHandlers?: (registry: DurableHandlerRegistry) => void;
|
|
35
45
|
durableContextualize?: DurableWorkerOptions["contextualize"];
|
|
36
46
|
}
|
|
@@ -47,13 +57,16 @@ export function createLegacyServiceRuntime(input: LegacyServiceRuntimeInput) {
|
|
|
47
57
|
const worker = new DurableJobWorker(execution, registry, {
|
|
48
58
|
contextualize: input.durableContextualize,
|
|
49
59
|
});
|
|
60
|
+
// @windy-module system.workflow begin
|
|
50
61
|
const workflowTimeout = new WorkflowTimeoutRuntime(
|
|
51
62
|
service,
|
|
52
63
|
worker,
|
|
53
64
|
registry,
|
|
54
65
|
input.workflow,
|
|
55
66
|
);
|
|
67
|
+
// @windy-module system.workflow end
|
|
56
68
|
|
|
69
|
+
// @windy-module system.bulk-data begin
|
|
57
70
|
const bulkExecutor = new DeferredLegacyExecutor();
|
|
58
71
|
const bulkDataService = new BulkDataService(
|
|
59
72
|
input.bulkRepository,
|
|
@@ -71,6 +84,7 @@ export function createLegacyServiceRuntime(input: LegacyServiceRuntimeInput) {
|
|
|
71
84
|
bulkExecutor.attach(({ legacyId, generation }) =>
|
|
72
85
|
bulkDataService.resumeDurable(legacyId, generation),
|
|
73
86
|
);
|
|
87
|
+
// @windy-module system.bulk-data end
|
|
74
88
|
|
|
75
89
|
const schedulerExecutor = new DeferredLegacyExecutor();
|
|
76
90
|
const taskDefinitions = input.taskDefinitions;
|
|
@@ -91,10 +105,14 @@ export function createLegacyServiceRuntime(input: LegacyServiceRuntimeInput) {
|
|
|
91
105
|
);
|
|
92
106
|
|
|
93
107
|
return {
|
|
108
|
+
// @windy-module system.bulk-data begin
|
|
94
109
|
bulkDataService,
|
|
110
|
+
// @windy-module system.bulk-data end
|
|
95
111
|
scheduler,
|
|
96
112
|
taskDefinitions,
|
|
113
|
+
// @windy-module system.workflow begin
|
|
97
114
|
workflowTimeout,
|
|
115
|
+
// @windy-module system.workflow end
|
|
98
116
|
durable: { facts, service, registry, worker },
|
|
99
117
|
};
|
|
100
118
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { DurableHandlerContext } from "@southwind-ai/jobs";
|
|
3
|
+
import { featureKey } from "@southwind-ai/shared";
|
|
4
|
+
import { buildActorScopeContexts } from "../data-access/context.js";
|
|
5
|
+
import { captureAuthorizationScope } from "../data-access/scope-snapshot.js";
|
|
6
|
+
import type { RequestActor } from "../guards.js";
|
|
7
|
+
import { createServerRuntime } from "../runtime.js";
|
|
8
|
+
import { createModuleDurableContextualizer } from "./durable-context.js";
|
|
9
|
+
|
|
10
|
+
const feature = {
|
|
11
|
+
key: featureKey("evidence.catalog"),
|
|
12
|
+
label: "电子物证",
|
|
13
|
+
module: "evidence",
|
|
14
|
+
visible: "visible" as const,
|
|
15
|
+
enabled: true,
|
|
16
|
+
stage: "stable" as const,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
describe("模块 Durable Handler 可选端口", () => {
|
|
20
|
+
test("未挂载 Storage 时仍重建 Actor,并对文件读取 fail-closed", async () => {
|
|
21
|
+
const actor = currentActor();
|
|
22
|
+
const contextualize = createModuleDurableContextualizer({
|
|
23
|
+
hosts: {} as never,
|
|
24
|
+
runtime: createServerRuntime(
|
|
25
|
+
{ NODE_ENV: "test" },
|
|
26
|
+
{ featureCatalog: [feature] },
|
|
27
|
+
),
|
|
28
|
+
features: [feature],
|
|
29
|
+
resolveActor: async () => actor,
|
|
30
|
+
});
|
|
31
|
+
const context = await contextualize({
|
|
32
|
+
claim: { job: job(actor) },
|
|
33
|
+
moduleName: "evidence",
|
|
34
|
+
context: baseContext(),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
expect(context).toMatchObject({ actor: { id: "user-reader" } });
|
|
38
|
+
await expect(
|
|
39
|
+
(
|
|
40
|
+
context as typeof context & {
|
|
41
|
+
storage: { openResourceFile(input: never): Promise<unknown> };
|
|
42
|
+
}
|
|
43
|
+
).storage.openResourceFile({} as never),
|
|
44
|
+
).rejects.toThrow("模块 evidence 的存储 Port 未挂载");
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
function currentActor(): RequestActor {
|
|
49
|
+
return {
|
|
50
|
+
id: "user-reader",
|
|
51
|
+
type: "user",
|
|
52
|
+
name: "物证读取者",
|
|
53
|
+
roleCodes: ["investigator"],
|
|
54
|
+
permissionKeys: ["evidence.read"],
|
|
55
|
+
...buildActorScopeContexts("user-reader", "dept-case", [
|
|
56
|
+
{
|
|
57
|
+
id: "role-investigator",
|
|
58
|
+
code: "investigator",
|
|
59
|
+
dataScope: "all",
|
|
60
|
+
departmentIds: [],
|
|
61
|
+
},
|
|
62
|
+
]),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function job(actor: RequestActor) {
|
|
67
|
+
return {
|
|
68
|
+
id: "job-evidence",
|
|
69
|
+
requestedBy: actor.id,
|
|
70
|
+
authorizationScope: captureAuthorizationScope(actor.scopeContext).snapshot,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function baseContext(): DurableHandlerContext {
|
|
75
|
+
return {
|
|
76
|
+
jobId: "job-evidence",
|
|
77
|
+
stageKey: "main",
|
|
78
|
+
attemptNumber: 1,
|
|
79
|
+
signal: new AbortController().signal,
|
|
80
|
+
heartbeat: async () => undefined,
|
|
81
|
+
progress: async () => undefined,
|
|
82
|
+
publishArtifact: async () => undefined,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
@@ -5,7 +5,9 @@ import { buildActorScopeContexts } from "../data-access/context.js";
|
|
|
5
5
|
import { captureAuthorizationScope } from "../data-access/scope-snapshot.js";
|
|
6
6
|
import type { RequestActor } from "../guards.js";
|
|
7
7
|
import { createServerRuntime } from "../runtime.js";
|
|
8
|
+
// @windy-module system.storage begin
|
|
8
9
|
import { createStorageRuntime } from "../storage/runtime.js";
|
|
10
|
+
// @windy-module system.storage end
|
|
9
11
|
import { createModuleDurableContextualizer } from "./durable-context.js";
|
|
10
12
|
import { initializeModuleHosts } from "./initialize.js";
|
|
11
13
|
import { manifest } from "./test-fixtures.js";
|
|
@@ -82,6 +84,7 @@ describe("模块 Durable Handler 授权上下文", () => {
|
|
|
82
84
|
]);
|
|
83
85
|
});
|
|
84
86
|
|
|
87
|
+
// @windy-module system.storage begin
|
|
85
88
|
test("重启后按 requestedBy 重建 Actor 并允许同案跨上传者读取", async () => {
|
|
86
89
|
const { contextualize, storage, actor, resolverCalls, runtime } =
|
|
87
90
|
await setup();
|
|
@@ -131,7 +134,9 @@ describe("模块 Durable Handler 授权上下文", () => {
|
|
|
131
134
|
}),
|
|
132
135
|
);
|
|
133
136
|
});
|
|
137
|
+
// @windy-module system.storage end
|
|
134
138
|
|
|
139
|
+
// @windy-module system.storage begin
|
|
135
140
|
test("Scope 变化、权限撤销与资源绑定漂移均 fail-closed", async () => {
|
|
136
141
|
const stable = currentActor();
|
|
137
142
|
const scopeChanged = currentActor("self");
|
|
@@ -182,6 +187,7 @@ describe("模块 Durable Handler 授权上下文", () => {
|
|
|
182
187
|
}),
|
|
183
188
|
);
|
|
184
189
|
});
|
|
190
|
+
// @windy-module system.storage end
|
|
185
191
|
});
|
|
186
192
|
|
|
187
193
|
async function setup(
|
|
@@ -220,7 +226,9 @@ async function setup(
|
|
|
220
226
|
},
|
|
221
227
|
],
|
|
222
228
|
});
|
|
229
|
+
// @windy-module system.storage begin
|
|
223
230
|
const storage = createStorageRuntime({}, undefined, hosts.uploadPolicies);
|
|
231
|
+
// @windy-module system.storage end
|
|
224
232
|
const runtime = createServerRuntime(
|
|
225
233
|
{ NODE_ENV: "test" },
|
|
226
234
|
{ featureCatalog: [evidenceFeature] },
|
|
@@ -228,14 +236,18 @@ async function setup(
|
|
|
228
236
|
let calls = 0;
|
|
229
237
|
return {
|
|
230
238
|
actor: currentActor(),
|
|
239
|
+
// @windy-module system.storage begin
|
|
231
240
|
storage,
|
|
241
|
+
// @windy-module system.storage end
|
|
232
242
|
runtime,
|
|
233
243
|
resolverCalls: () => calls,
|
|
234
244
|
contextualize: createModuleDurableContextualizer({
|
|
235
245
|
hosts,
|
|
236
246
|
runtime,
|
|
237
247
|
features: [evidenceFeature],
|
|
248
|
+
// @windy-module system.storage begin
|
|
238
249
|
storage,
|
|
250
|
+
// @windy-module system.storage end
|
|
239
251
|
backgroundAi,
|
|
240
252
|
async resolveActor() {
|
|
241
253
|
calls += 1;
|
|
@@ -283,6 +295,7 @@ function baseContext(attemptNumber: number): DurableHandlerContext {
|
|
|
283
295
|
};
|
|
284
296
|
}
|
|
285
297
|
|
|
298
|
+
// @windy-module system.storage begin
|
|
286
299
|
async function seedFile(storage: ReturnType<typeof createStorageRuntime>) {
|
|
287
300
|
const bytes = new TextEncoder().encode("evidence");
|
|
288
301
|
const sha256 = new Bun.CryptoHasher("sha256").update(bytes).digest("hex");
|
|
@@ -310,3 +323,4 @@ async function seedFile(storage: ReturnType<typeof createStorageRuntime>) {
|
|
|
310
323
|
});
|
|
311
324
|
return storage.uploads.complete(session.id, "user-uploader");
|
|
312
325
|
}
|
|
326
|
+
// @windy-module system.storage end
|
|
@@ -21,6 +21,7 @@ import type { InitializedModuleHosts } from "./initialize.js";
|
|
|
21
21
|
import {
|
|
22
22
|
createModuleStoragePort,
|
|
23
23
|
type ModuleStorageServices,
|
|
24
|
+
unavailableModuleStoragePort,
|
|
24
25
|
} from "./storage-port.js";
|
|
25
26
|
import { createModuleStorageScope } from "./storage-scope.js";
|
|
26
27
|
import { toModuleActor } from "./request-context.js";
|
|
@@ -29,7 +30,7 @@ export interface ModuleDurableContextServices {
|
|
|
29
30
|
readonly hosts: InitializedModuleHosts;
|
|
30
31
|
readonly runtime: ReturnType<typeof createServerRuntime>;
|
|
31
32
|
readonly features: readonly FeatureFlagDefinition[];
|
|
32
|
-
readonly storage
|
|
33
|
+
readonly storage?: ModuleStorageServices;
|
|
33
34
|
readonly backgroundAi?: {
|
|
34
35
|
create(
|
|
35
36
|
moduleName: string,
|
|
@@ -75,20 +76,21 @@ export function createModuleDurableContextualizer(
|
|
|
75
76
|
request,
|
|
76
77
|
actor,
|
|
77
78
|
);
|
|
78
|
-
const storage =
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
79
|
+
const storage = services.storage
|
|
80
|
+
? createModuleStoragePort(
|
|
81
|
+
services.storage,
|
|
82
|
+
createModuleStorageScope({
|
|
83
|
+
moduleName: input.moduleName,
|
|
84
|
+
purposes: services.hosts.uploadPurposesOf(input.moduleName),
|
|
85
|
+
resourceFileAccessPolicies:
|
|
86
|
+
services.hosts.resourceFileAccessPoliciesOf(input.moduleName),
|
|
87
|
+
guardContext,
|
|
88
|
+
runtime: services.runtime,
|
|
89
|
+
features: services.features,
|
|
90
|
+
signal: input.context.signal,
|
|
91
|
+
}),
|
|
92
|
+
)
|
|
93
|
+
: unavailableModuleStoragePort(input.moduleName);
|
|
92
94
|
const moduleContext: ModuleDurableHandlerContext = {
|
|
93
95
|
...input.context,
|
|
94
96
|
actor: toModuleActor(actor),
|