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.
Files changed (65) hide show
  1. package/README.md +14 -36
  2. package/dist/cli.js +1831 -3178
  3. package/package.json +1 -1
  4. package/template/.windy-template.json +2 -2
  5. package/template/AGENTS.md +7 -1
  6. package/template/README.md +8 -11
  7. package/template/apps/agent-server/Dockerfile +2 -1
  8. package/template/apps/agent-server/README.md +13 -5
  9. package/template/apps/agent-server/adk_web_agents/canonical_debug/__init__.py +1 -0
  10. package/template/apps/agent-server/adk_web_agents/canonical_debug/agent.py +125 -0
  11. package/template/apps/agent-server/project/adk-web-debug.json +4 -0
  12. package/template/apps/agent-server/src/southwind_agent_server/adk_debug.py +315 -0
  13. package/template/apps/agent-server/tests/test_adk_web_canonical_debug.py +235 -0
  14. package/template/apps/server/Dockerfile +1 -1
  15. package/template/apps/server/src/agent/run-debug-authorization.ts +27 -0
  16. package/template/apps/server/src/agent/run-facade-adk-debug.test.ts +56 -0
  17. package/template/apps/server/src/agent/run-facade.test-support.ts +235 -0
  18. package/template/apps/server/src/agent/run-facade.test.ts +8 -244
  19. package/template/apps/server/src/agent/run-facade.ts +23 -48
  20. package/template/apps/server/src/agent/run-operation-registry.ts +51 -0
  21. package/template/apps/server/src/agent/runtime-bootstrap.ts +5 -2
  22. package/template/apps/server/src/agent/tool-audit.ts +3 -0
  23. package/template/apps/server/src/agent/tool-registry.test.ts +29 -0
  24. package/template/apps/server/src/application-services.ts +83 -15
  25. package/template/apps/server/src/audit/contracts.ts +8 -0
  26. package/template/apps/server/src/audit/recovery-queue.test.ts +48 -1
  27. package/template/apps/server/src/audit/recovery-queue.ts +73 -4
  28. package/template/apps/server/src/audit/reliable-writer.test.ts +120 -2
  29. package/template/apps/server/src/audit/reliable-writer.ts +98 -34
  30. package/template/apps/server/src/guards.ts +16 -0
  31. package/template/apps/server/src/index.ts +23 -20
  32. package/template/apps/server/src/jobs/legacy-adapter.integration.test.ts +0 -1
  33. package/template/apps/server/src/jobs/legacy-adapter.test.ts +6 -0
  34. package/template/apps/server/src/jobs/legacy-service-runtime.ts +18 -0
  35. package/template/apps/server/src/module-host/durable-context-unavailable.test.ts +84 -0
  36. package/template/apps/server/src/module-host/durable-context.test.ts +14 -0
  37. package/template/apps/server/src/module-host/durable-context.ts +17 -15
  38. package/template/apps/server/src/module-host/request-context.test.ts +30 -1
  39. package/template/apps/server/src/module-host/request-context.ts +15 -2
  40. package/template/apps/server/src/module-host/storage-port.ts +1 -1
  41. package/template/apps/server/src/platform-bootstrap.ts +16 -0
  42. package/template/apps/server/src/runtime-adk-debug.test.ts +246 -0
  43. package/template/apps/server/src/runtime-adk-debug.ts +158 -0
  44. package/template/apps/server/src/runtime-request-actor.test.ts +55 -0
  45. package/template/apps/server/src/runtime-request-actor.ts +22 -0
  46. package/template/apps/server/src/runtime.test.ts +3 -0
  47. package/template/apps/server/src/runtime.ts +36 -3
  48. package/template/apps/server/src/scheduler/definitions.test.ts +16 -0
  49. package/template/apps/web/Dockerfile +1 -1
  50. package/template/docker-compose.agent-debug.yml +56 -0
  51. package/template/docker-compose.yml +1 -39
  52. package/template/docs/architecture/ai-runtime.md +14 -5
  53. package/template/docs/architecture/module-manifest-composition.md +14 -0
  54. package/template/docs/development/custom-login-page.md +2 -16
  55. package/template/docs/platform/agent-runtime.md +57 -16
  56. package/template/docs/platform/agent-tool-sandbox.md +4 -3
  57. package/template/docs/platform/audit-reliability.md +33 -5
  58. package/template/docs/platform/platform-authentication.md +9 -1
  59. package/template/package.json +4 -3
  60. package/template/packages/database/drizzle/0043_activate_agent_dependencies.sql +196 -0
  61. package/template/packages/database/drizzle/meta/0043_snapshot.json +8335 -0
  62. package/template/packages/database/drizzle/meta/_journal.json +7 -0
  63. package/template/packages/database/src/schema-notification-delivery.test.ts +15 -6
  64. package/template/packages/database/src/schema-workflow-notification.test.ts +44 -29
  65. 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
- try {
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
- try {
83
- await this.withQueueLock(async () => {
84
- const events = await this.queue.read();
85
- if (!events.length) {
86
- this.current.queued = 0;
87
- this.refreshState();
88
- return;
89
- }
90
- const pending: AuditEvent[] = [];
91
- for (let index = 0; index < events.length; index += 1) {
92
- try {
93
- await this.persistWithRetry(events[index]!);
94
- this.markSuccess();
95
- } catch {
96
- pending.push(...events.slice(index));
97
- break;
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.markDegraded("关键审计恢复队列不可用");
110
- throw error;
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 = pino({ name: "windy-server" });
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();
@@ -34,7 +34,6 @@ describePostgres("PostgreSQL legacy durable adapter", () => {
34
34
  });
35
35
  await pool.query(`
36
36
  truncate table
37
- governed_export_requests,
38
37
  bulk_data_artifacts,
39
38
  bulk_data_jobs,
40
39
  scheduled_task_runs,
@@ -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: ModuleStorageServices;
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 = createModuleStoragePort(
79
- services.storage,
80
- createModuleStorageScope({
81
- moduleName: input.moduleName,
82
- purposes: services.hosts.uploadPurposesOf(input.moduleName),
83
- resourceFileAccessPolicies: services.hosts.resourceFileAccessPoliciesOf(
84
- input.moduleName,
85
- ),
86
- guardContext,
87
- runtime: services.runtime,
88
- features: services.features,
89
- signal: input.context.signal,
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),