create-windy 0.2.19 → 0.2.21

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 (127) hide show
  1. package/README.md +5 -1
  2. package/dist/cli.js +392 -160
  3. package/package.json +1 -1
  4. package/template/.windy-template.json +2 -2
  5. package/template/README.md +1 -0
  6. package/template/apps/agent-server/Dockerfile +20 -0
  7. package/template/apps/agent-server/README.md +46 -0
  8. package/template/apps/agent-server/pyproject.toml +44 -0
  9. package/template/apps/agent-server/src/southwind_agent_server/__init__.py +5 -0
  10. package/template/apps/agent-server/src/southwind_agent_server/adk_adapter.py +39 -0
  11. package/template/apps/agent-server/src/southwind_agent_server/app.py +263 -0
  12. package/template/apps/agent-server/src/southwind_agent_server/config.py +141 -0
  13. package/template/apps/agent-server/src/southwind_agent_server/contracts.py +99 -0
  14. package/template/apps/agent-server/src/southwind_agent_server/deterministic.py +52 -0
  15. package/template/apps/agent-server/src/southwind_agent_server/engine.py +85 -0
  16. package/template/apps/agent-server/src/southwind_agent_server/openai_engine.py +197 -0
  17. package/template/apps/agent-server/src/southwind_agent_server/openai_provider.py +246 -0
  18. package/template/apps/agent-server/src/southwind_agent_server/service.py +180 -0
  19. package/template/apps/agent-server/src/southwind_agent_server/sqlite_store.py +317 -0
  20. package/template/apps/agent-server/src/southwind_agent_server/sse.py +41 -0
  21. package/template/apps/agent-server/src/southwind_agent_server/store.py +144 -0
  22. package/template/apps/agent-server/src/southwind_agent_server/tool_host.py +80 -0
  23. package/template/apps/agent-server/tests/test_api.py +207 -0
  24. package/template/apps/agent-server/tests/test_config.py +56 -0
  25. package/template/apps/agent-server/tests/test_openai_provider.py +224 -0
  26. package/template/apps/agent-server/tests/test_sqlite_store.py +93 -0
  27. package/template/apps/agent-server/uv.lock +925 -0
  28. package/template/apps/server/src/agent/execution-grants.ts +89 -0
  29. package/template/apps/server/src/agent/remote-runtime.ts +128 -0
  30. package/template/apps/server/src/agent/run-contracts.ts +43 -0
  31. package/template/apps/server/src/agent/run-facade.test.ts +257 -0
  32. package/template/apps/server/src/agent/run-facade.ts +190 -0
  33. package/template/apps/server/src/agent/run-routes.ts +222 -0
  34. package/template/apps/server/src/agent/runtime-bootstrap.ts +53 -0
  35. package/template/apps/server/src/agent/tool-host.test.ts +242 -0
  36. package/template/apps/server/src/agent/tool-host.ts +153 -0
  37. package/template/apps/server/src/application-services.ts +2 -2
  38. package/template/apps/server/src/audit/search-summary.ts +15 -8
  39. package/template/apps/server/src/example-modules.ts +58 -70
  40. package/template/apps/server/src/foundation.ts +8 -4
  41. package/template/apps/server/src/guards.test.ts +13 -0
  42. package/template/apps/server/src/guards.ts +12 -2
  43. package/template/apps/server/src/index.ts +48 -95
  44. package/template/apps/server/src/installed-business-modules.ts +23 -4
  45. package/template/apps/server/src/module-bootstrap.ts +83 -0
  46. package/template/apps/server/src/module-composition.test.ts +54 -1
  47. package/template/apps/server/src/module-composition.ts +58 -0
  48. package/template/apps/server/src/module-host/initialize-contracts.ts +67 -0
  49. package/template/apps/server/src/module-host/initialize.test.ts +192 -0
  50. package/template/apps/server/src/module-host/initialize.ts +328 -0
  51. package/template/apps/server/src/module-host/request-context.test.ts +66 -0
  52. package/template/apps/server/src/module-host/request-context.ts +156 -0
  53. package/template/apps/server/src/module-host/role-presets.test.ts +94 -0
  54. package/template/apps/server/src/module-host/role-presets.ts +76 -0
  55. package/template/apps/server/src/module-host/route-installer.test.ts +317 -0
  56. package/template/apps/server/src/module-host/route-installer.ts +97 -0
  57. package/template/apps/server/src/module-host/route-registration.test.ts +160 -0
  58. package/template/apps/server/src/module-host/search-providers.ts +31 -0
  59. package/template/apps/server/src/module-host/storage-port.test.ts +165 -0
  60. package/template/apps/server/src/module-host/storage-port.ts +59 -0
  61. package/template/apps/server/src/module-host/task-definitions.ts +41 -0
  62. package/template/apps/server/src/module-host/test-fixtures.ts +26 -0
  63. package/template/apps/server/src/module-host/tool-handlers.ts +25 -0
  64. package/template/apps/server/src/module-host.test.ts +202 -58
  65. package/template/apps/server/src/module-host.ts +35 -113
  66. package/template/apps/server/src/module-runtime-validation.ts +40 -0
  67. package/template/apps/server/src/route-guards.test.ts +112 -1
  68. package/template/apps/server/src/runtime-feature.ts +66 -31
  69. package/template/apps/server/src/runtime.test.ts +1 -0
  70. package/template/apps/server/src/runtime.ts +3 -1
  71. package/template/apps/server/src/system/built-in-roles.ts +1 -1
  72. package/template/apps/server/src/work-order/foundation-modules.test.ts +30 -10
  73. package/template/apps/server/src/work-order/routes.test.ts +30 -18
  74. package/template/apps/server/src/work-order/routes.ts +104 -135
  75. package/template/apps/server/src/work-order/search-api.test.ts +14 -3
  76. package/template/apps/server/src/work-order/search-provider.test.ts +23 -15
  77. package/template/apps/server/src/work-order/search-provider.ts +17 -12
  78. package/template/apps/server/src/work-order/task.test.ts +16 -11
  79. package/template/apps/web/Dockerfile +4 -1
  80. package/template/apps/web/src/layout/GlobalWatermark.layering.webtest.ts +4 -1
  81. package/template/apps/web/src/layout/GlobalWatermark.vue +2 -0
  82. package/template/apps/web/src/layout/GlobalWatermark.webtest.ts +4 -1
  83. package/template/docker-compose.yml +25 -0
  84. package/template/docs/architecture/ai-runtime.md +744 -0
  85. package/template/docs/architecture/object-storage.md +12 -0
  86. package/template/docs/platform/agent-runtime.md +128 -0
  87. package/template/docs/platform/agent-tools.md +8 -1
  88. package/template/package.json +1 -0
  89. package/template/packages/config/index.test.ts +100 -0
  90. package/template/packages/config/index.ts +1 -0
  91. package/template/packages/config/package.json +2 -1
  92. package/template/packages/config/src/ai-openai-compatible.ts +131 -0
  93. package/template/packages/config/src/platform.ts +28 -1
  94. package/template/packages/database/package.json +1 -1
  95. package/template/packages/database/src/runner.ts +7 -9
  96. package/template/packages/example-work-order/index.ts +1 -0
  97. package/template/packages/example-work-order/package.json +2 -0
  98. package/template/packages/example-work-order/src/server-module.ts +61 -0
  99. package/template/packages/jobs/package.json +1 -1
  100. package/template/packages/jobs/src/types.ts +7 -1
  101. package/template/packages/modules/package.json +1 -1
  102. package/template/packages/modules/src/catalog-validation.test.ts +212 -0
  103. package/template/packages/modules/src/catalog-validation.ts +19 -2
  104. package/template/packages/modules/src/migration-bundle-validation.test.ts +191 -0
  105. package/template/packages/server-sdk/index.ts +53 -0
  106. package/template/packages/server-sdk/package.json +18 -3
  107. package/template/packages/server-sdk/src/actor.ts +15 -0
  108. package/template/packages/server-sdk/src/ai-operation-registration.ts +31 -0
  109. package/template/packages/server-sdk/src/audit-port.ts +22 -0
  110. package/template/packages/server-sdk/src/jobs-port.ts +24 -0
  111. package/template/packages/server-sdk/src/module-host.ts +41 -1
  112. package/template/packages/server-sdk/src/namespace.test.ts +26 -0
  113. package/template/packages/server-sdk/src/namespace.ts +13 -0
  114. package/template/packages/server-sdk/src/request-context.ts +21 -0
  115. package/template/packages/server-sdk/src/role-preset.ts +18 -0
  116. package/template/packages/server-sdk/src/route-definition.ts +26 -0
  117. package/template/packages/server-sdk/src/search-provider-port.ts +48 -0
  118. package/template/packages/server-sdk/src/storage-port.ts +25 -0
  119. package/template/packages/server-sdk/src/task-registration.ts +18 -0
  120. package/template/packages/server-sdk/src/tool-host-port.ts +22 -0
  121. package/template/packages/server-sdk/src/tool-registration.ts +29 -0
  122. package/template/packages/shared/index.ts +1 -0
  123. package/template/packages/shared/package.json +1 -1
  124. package/template/packages/shared/src/license-catalog.test.ts +73 -1
  125. package/template/packages/shared/src/license-catalog.ts +95 -0
  126. package/template/packages/{database/src/bundle-validation.ts → shared/src/migration-bundle-validation.ts} +2 -1
  127. package/template/apps/server/src/work-order/task.ts +0 -30
@@ -1,141 +1,123 @@
1
1
  import {
2
2
  createWorkOrderSchema,
3
3
  updateWorkOrderSchema,
4
- workOrderModule,
5
4
  } from "@southwind-ai/example-work-order";
6
- import type { ApiPermissionBinding } from "@southwind-ai/shared";
7
- import { recordMutationAudit } from "../audit/policy.js";
8
- import { accessCall } from "../system/resource-access-response.js";
9
- import { evaluateRouteGuards } from "../route-guards.js";
10
- import type { createServerRuntime } from "../runtime.js";
11
- import {
12
- forbidden,
13
- notFound,
14
- resourceEvent,
15
- routeContext,
16
- } from "../system/route-helpers.js";
17
- import type { RouteApp } from "../system/route-types.js";
5
+ import type { ModuleRouteDefinition } from "@southwind-ai/server-sdk";
6
+ import { searchAuditMetadata } from "../audit/search-summary.js";
7
+ import { requestActorFromModule } from "../module-host/request-context.js";
18
8
  import { parsePagination } from "../pagination.js";
9
+ import { accessCall } from "../system/resource-access-response.js";
10
+ import { notFound } from "../system/route-helpers.js";
19
11
  import type { WorkOrderService } from "./service.js";
20
- import { searchAuditEvent } from "../audit/search-summary.js";
21
12
 
22
- type ServerRuntime = ReturnType<typeof createServerRuntime>;
23
- const manifest = workOrderModule();
24
- const feature = manifest.features[0];
25
-
26
- export function registerWorkOrderRoutes(
27
- app: RouteApp,
28
- service: WorkOrderService,
29
- runtime: ServerRuntime,
30
- ) {
31
- app.get("/work-orders", (raw) =>
32
- guarded(raw, binding("GET", "/api/work-orders"), runtime, async () => {
33
- const context = routeContext(raw);
34
- const pagination = parsePagination(context.query);
35
- if (pagination instanceof Response) return pagination;
36
- const result = await accessCall(() =>
37
- service.list(
38
- { ...pagination, q: context.query.q },
39
- context.guardContext.actor,
40
- ),
41
- );
42
- if (!(result instanceof Response) && context.query.q?.trim()) {
43
- runtime.auditSink.recordEvent(
44
- searchAuditEvent(
45
- context.guardContext,
46
- "work-order",
47
- context.query.q,
48
- result.total,
13
+ /**
14
+ * 示例工单的 Guarded Route 定义。
15
+ *
16
+ * 只声明 method/path/permissionKey 与 Handler;Guard、审计上下文与受限 Port
17
+ * 由宿主在 installModuleRoutes 时统一绑定,不再手工查询 Manifest 绑定。
18
+ * `requireService` 延迟解析宿主装配完成后才存在的模块服务。
19
+ */
20
+ export function createWorkOrderRouteDefinitions(
21
+ requireService: () => WorkOrderService,
22
+ ): ModuleRouteDefinition[] {
23
+ return [
24
+ {
25
+ method: "GET",
26
+ path: "/api/work-orders",
27
+ permissionKey: "work-order.ticket.read",
28
+ handler: async (context) => {
29
+ const pagination = parsePagination(context.query);
30
+ if (pagination instanceof Response) return pagination;
31
+ const result = await accessCall(() =>
32
+ requireService().list(
33
+ { ...pagination, q: context.query.q },
34
+ requestActorFromModule(context.actor),
49
35
  ),
50
36
  );
51
- }
52
- return result;
53
- }),
54
- );
55
-
56
- app.get("/work-orders/:id", (raw) =>
57
- guarded(raw, binding("GET", "/api/work-orders/:id"), runtime, async () => {
58
- const context = routeContext(raw);
59
- const id = context.params.id || "";
60
- const item = await accessCall(() =>
61
- service.get(id, context.guardContext.actor),
62
- );
63
- return item instanceof Response
64
- ? item
65
- : item || notFound("work-order", id);
66
- }),
67
- );
68
-
69
- app.post("/work-orders", (raw) =>
70
- guarded(raw, binding("POST", "/api/work-orders"), runtime, async () => {
71
- const context = routeContext(raw);
72
- const parsed = createWorkOrderSchema.safeParse(context.body);
73
- if (!parsed.success) return invalid(parsed.error.issues[0]?.message);
74
- const item = await accessCall(() =>
75
- service.create(parsed.data, context.guardContext.actor),
76
- );
77
- if (item instanceof Response) return item;
78
- await audit(runtime, context.guardContext, "crud.create", item.id);
79
- return item;
80
- }),
81
- );
82
-
83
- app.put("/work-orders/:id", (raw) =>
84
- guarded(raw, binding("PUT", "/api/work-orders/:id"), runtime, async () => {
85
- const context = routeContext(raw);
86
- const id = context.params.id || "";
87
- const parsed = updateWorkOrderSchema.safeParse(context.body);
88
- if (!parsed.success) return invalid(parsed.error.issues[0]?.message);
89
- const item = await accessCall(() =>
90
- service.update(id, parsed.data, context.guardContext.actor),
91
- );
92
- if (item instanceof Response) return item;
93
- if (!item) return notFound("work-order", id);
94
- await audit(runtime, context.guardContext, "crud.update", id);
95
- return item;
96
- }),
97
- );
98
-
99
- app.delete("/work-orders/:id", (raw) =>
100
- guarded(
101
- raw,
102
- binding("DELETE", "/api/work-orders/:id"),
103
- runtime,
104
- async () => {
105
- const context = routeContext(raw);
37
+ if (!(result instanceof Response) && context.query.q?.trim()) {
38
+ await context.audit.emit("data.search", {
39
+ target: { type: "work-order" },
40
+ metadata: searchAuditMetadata(context.query.q, result.total),
41
+ });
42
+ }
43
+ return result;
44
+ },
45
+ },
46
+ {
47
+ method: "GET",
48
+ path: "/api/work-orders/:id",
49
+ permissionKey: "work-order.ticket.read",
50
+ handler: async (context) => {
106
51
  const id = context.params.id || "";
107
52
  const item = await accessCall(() =>
108
- service.archive(id, context.guardContext.actor),
53
+ requireService().get(id, requestActorFromModule(context.actor)),
54
+ );
55
+ return item instanceof Response
56
+ ? item
57
+ : item || notFound("work-order", id);
58
+ },
59
+ },
60
+ {
61
+ method: "POST",
62
+ path: "/api/work-orders",
63
+ permissionKey: "work-order.ticket.create",
64
+ handler: async (context) => {
65
+ const parsed = createWorkOrderSchema.safeParse(context.body);
66
+ if (!parsed.success) return invalid(parsed.error.issues[0]?.message);
67
+ const item = await accessCall(() =>
68
+ requireService().create(
69
+ parsed.data,
70
+ requestActorFromModule(context.actor),
71
+ ),
72
+ );
73
+ if (item instanceof Response) return item;
74
+ await context.audit.emit("crud.create", {
75
+ target: { type: "work-order", id: item.id },
76
+ });
77
+ return item;
78
+ },
79
+ },
80
+ {
81
+ method: "PUT",
82
+ path: "/api/work-orders/:id",
83
+ permissionKey: "work-order.ticket.manage",
84
+ handler: async (context) => {
85
+ const id = context.params.id || "";
86
+ const parsed = updateWorkOrderSchema.safeParse(context.body);
87
+ if (!parsed.success) return invalid(parsed.error.issues[0]?.message);
88
+ const item = await accessCall(() =>
89
+ requireService().update(
90
+ id,
91
+ parsed.data,
92
+ requestActorFromModule(context.actor),
93
+ ),
109
94
  );
110
95
  if (item instanceof Response) return item;
111
96
  if (!item) return notFound("work-order", id);
112
- await audit(runtime, context.guardContext, "crud.delete", id);
97
+ await context.audit.emit("crud.update", {
98
+ target: { type: "work-order", id },
99
+ });
113
100
  return item;
114
101
  },
115
- ),
116
- );
117
- }
118
-
119
- async function guarded(
120
- raw: unknown,
121
- permission: ApiPermissionBinding,
122
- runtime: ServerRuntime,
123
- handler: () => unknown,
124
- ) {
125
- const decision = await evaluateRouteGuards(
126
- runtime,
127
- routeContext(raw).guardContext,
128
- { permission, feature },
129
- );
130
- return decision.allowed ? handler() : forbidden(decision.reason);
131
- }
132
-
133
- function binding(method: string, path: string): ApiPermissionBinding {
134
- const result = manifest.apiPermissions.find(
135
- (item) => item.method === method && item.path === path,
136
- );
137
- if (!result) throw new Error(`示例工单缺少 API 绑定:${method} ${path}`);
138
- return result;
102
+ },
103
+ {
104
+ method: "DELETE",
105
+ path: "/api/work-orders/:id",
106
+ permissionKey: "work-order.ticket.manage",
107
+ handler: async (context) => {
108
+ const id = context.params.id || "";
109
+ const item = await accessCall(() =>
110
+ requireService().archive(id, requestActorFromModule(context.actor)),
111
+ );
112
+ if (item instanceof Response) return item;
113
+ if (!item) return notFound("work-order", id);
114
+ await context.audit.emit("crud.delete", {
115
+ target: { type: "work-order", id },
116
+ });
117
+ return item;
118
+ },
119
+ },
120
+ ];
139
121
  }
140
122
 
141
123
  function invalid(message = "工单内容无效") {
@@ -144,16 +126,3 @@ function invalid(message = "工单内容无效") {
144
126
  { status: 400 },
145
127
  );
146
128
  }
147
-
148
- function audit(
149
- runtime: ServerRuntime,
150
- context: ReturnType<typeof routeContext>["guardContext"],
151
- action: "crud.create" | "crud.update" | "crud.delete",
152
- id: string,
153
- ) {
154
- return recordMutationAudit(
155
- runtime,
156
- "work-orders",
157
- resourceEvent(context, action, "work-order", id),
158
- );
159
- }
@@ -1,6 +1,12 @@
1
1
  import { describe, expect, test } from "bun:test";
2
- import { workOrderModule, type WorkOrder } from "@southwind-ai/example-work-order";
3
- import { systemFoundationModule, type ModuleManifest } from "@southwind-ai/modules";
2
+ import {
3
+ workOrderModule,
4
+ type WorkOrder,
5
+ } from "@southwind-ai/example-work-order";
6
+ import {
7
+ systemFoundationModule,
8
+ type ModuleManifest,
9
+ } from "@southwind-ai/modules";
4
10
  import { licenseVersionKey } from "@southwind-ai/shared";
5
11
  import { buildScopeContext } from "../data-access/context.js";
6
12
  import { InMemoryScopedEntityRepository } from "../data-access/scoped-repository.js";
@@ -11,6 +17,7 @@ import { searchTestContext } from "../search/test-support.js";
11
17
  import type { ManagedDepartment } from "../system/entities.js";
12
18
  import { InMemoryEntityRepository } from "../system/repository.js";
13
19
  import type { RouteApp } from "../system/route-types.js";
20
+ import { adaptModuleSearchProvider } from "../module-host.js";
14
21
  import { workOrderDataScopeBinding } from "./repository.js";
15
22
  import { WorkOrderSearchProvider } from "./search-provider.js";
16
23
  import { WorkOrderService } from "./service.js";
@@ -45,7 +52,11 @@ describe("搜索 API 数据范围", () => {
45
52
  );
46
53
  const service = new SearchService(
47
54
  modules,
48
- [new WorkOrderSearchProvider(workOrders)],
55
+ [
56
+ adaptModuleSearchProvider(
57
+ new WorkOrderSearchProvider(() => workOrders),
58
+ ),
59
+ ],
49
60
  runtime,
50
61
  );
51
62
  const context = searchTestContext([
@@ -1,5 +1,8 @@
1
1
  import { describe, expect, test } from "bun:test";
2
- import { workOrderModule, type WorkOrder } from "@southwind-ai/example-work-order";
2
+ import {
3
+ workOrderModule,
4
+ type WorkOrder,
5
+ } from "@southwind-ai/example-work-order";
3
6
  import type { ModuleManifest } from "@southwind-ai/modules";
4
7
  import { licenseVersionKey } from "@southwind-ai/shared";
5
8
  import { buildScopeContext } from "../data-access/context.js";
@@ -11,6 +14,7 @@ import type { SearchProvider } from "../search/contracts.js";
11
14
  import { SearchService } from "../search/service.js";
12
15
  import type { ManagedDepartment } from "../system/entities.js";
13
16
  import { InMemoryEntityRepository } from "../system/repository.js";
17
+ import { adaptModuleSearchProvider } from "../module-host.js";
14
18
  import { workOrderDataScopeBinding } from "./repository.js";
15
19
  import {
16
20
  WorkOrderSearchProvider,
@@ -40,10 +44,13 @@ describe("示例工单 SearchProvider", () => {
40
44
  throw new Error("搜索不得调用裸 Repository.list");
41
45
  };
42
46
  const service = searchService([
43
- new WorkOrderSearchProvider(
44
- new WorkOrderService(
45
- repository,
46
- new InMemoryEntityRepository<ManagedDepartment>(),
47
+ adaptModuleSearchProvider(
48
+ new WorkOrderSearchProvider(
49
+ () =>
50
+ new WorkOrderService(
51
+ repository,
52
+ new InMemoryEntityRepository<ManagedDepartment>(),
53
+ ),
47
54
  ),
48
55
  ),
49
56
  ]);
@@ -84,17 +91,18 @@ describe("示例工单 SearchProvider", () => {
84
91
  });
85
92
 
86
93
  test("Provider 游标映射到 scoped Repository 分页", async () => {
87
- const provider = new WorkOrderSearchProvider(
88
- new WorkOrderService(
89
- new InMemoryScopedEntityRepository<WorkOrder>(
90
- [
91
- ticket("ticket_1", "user_self", "故障一"),
92
- ticket("ticket_2", "user_self", "故障二"),
93
- ],
94
- workOrderDataScopeBinding,
95
- ),
96
- new InMemoryEntityRepository<ManagedDepartment>(),
94
+ const workOrders = new WorkOrderService(
95
+ new InMemoryScopedEntityRepository<WorkOrder>(
96
+ [
97
+ ticket("ticket_1", "user_self", "故障一"),
98
+ ticket("ticket_2", "user_self", "故障二"),
99
+ ],
100
+ workOrderDataScopeBinding,
97
101
  ),
102
+ new InMemoryEntityRepository<ManagedDepartment>(),
103
+ );
104
+ const provider = adaptModuleSearchProvider(
105
+ new WorkOrderSearchProvider(() => workOrders),
98
106
  );
99
107
  const service = searchService([provider]);
100
108
  const first = await service.search(
@@ -1,9 +1,10 @@
1
1
  import type {
2
- SearchExecutionContext,
3
- SearchProvider,
4
- SearchProviderPage,
5
- SearchProviderQuery,
6
- } from "../search/contracts.js";
2
+ ModuleSearchContext,
3
+ ModuleSearchPage,
4
+ ModuleSearchProvider,
5
+ ModuleSearchQuery,
6
+ } from "@southwind-ai/server-sdk";
7
+ import { requestActorFromModule } from "../module-host/request-context.js";
7
8
  import { SearchProtocolError } from "../search/errors.js";
8
9
  import { maskSearchText } from "../search/masking.js";
9
10
  import type { WorkOrderService } from "./service.js";
@@ -11,21 +12,25 @@ import type { WorkOrderService } from "./service.js";
11
12
  export const WORK_ORDER_SEARCH_PROVIDER_KEY = "work-order.ticket.search";
12
13
  export const WORK_ORDER_SEARCH_PROVIDER_VERSION = "0.1.0";
13
14
 
14
- export class WorkOrderSearchProvider implements SearchProvider {
15
+ /**
16
+ * 示例工单搜索 Provider,实现 server-sdk 的受限 Provider 协议。
17
+ * `requireService` 延迟解析宿主装配完成后才存在的模块服务。
18
+ */
19
+ export class WorkOrderSearchProvider implements ModuleSearchProvider {
15
20
  readonly key = WORK_ORDER_SEARCH_PROVIDER_KEY;
16
21
  readonly providerVersion = WORK_ORDER_SEARCH_PROVIDER_VERSION;
17
22
 
18
- constructor(private readonly service: WorkOrderService) {}
23
+ constructor(private readonly requireService: () => WorkOrderService) {}
19
24
 
20
25
  async search(
21
- request: SearchProviderQuery,
22
- context: SearchExecutionContext,
23
- ): Promise<SearchProviderPage> {
26
+ request: ModuleSearchQuery,
27
+ context: ModuleSearchContext,
28
+ ): Promise<ModuleSearchPage> {
24
29
  if (request.signal.aborted) throw new Error("search aborted");
25
30
  const page = parseProviderCursor(request.cursor);
26
- const result = await this.service.list(
31
+ const result = await this.requireService().list(
27
32
  { q: request.text, page, pageSize: request.limit },
28
- context.actor,
33
+ requestActorFromModule(context.actor),
29
34
  );
30
35
  if (request.signal.aborted) throw new Error("search aborted");
31
36
  return {
@@ -1,21 +1,30 @@
1
1
  import { describe, expect, test } from "bun:test";
2
- import type { WorkOrder } from "@southwind-ai/example-work-order";
2
+ import { type WorkOrder } from "@southwind-ai/example-work-order";
3
+ import { createWorkOrderModule } from "../example-modules.js";
3
4
  import { createFoundationSnapshot } from "../foundation.js";
5
+ import {
6
+ createModuleTaskDefinitions,
7
+ initializeModuleHosts,
8
+ } from "../module-host.js";
4
9
  import { createServerRuntime } from "../runtime.js";
5
10
  import { InMemorySchedulerRepository } from "../scheduler/repository.js";
6
11
  import { SchedulerService } from "../scheduler/service.js";
7
12
  import type { ManagedDepartment } from "../system/entities.js";
8
13
  import { InMemoryEntityRepository } from "../system/repository.js";
9
14
  import { InMemoryScopedEntityRepository } from "../data-access/scoped-repository.js";
10
- import { WorkOrderService } from "./service.js";
11
15
  import { workOrderDataScopeBinding } from "./repository.js";
12
- import { createWorkOrderTaskDefinitions } from "./task.js";
13
16
 
14
17
  describe("示例工单后台任务", () => {
15
18
  test("通过统一调度 Guard 执行并把业务快照写入历史", async () => {
16
19
  const runtime = createServerRuntime({ WINDY_DEV_ADMIN_TOKEN: "token" });
17
- const service = new WorkOrderService(
18
- new InMemoryScopedEntityRepository<WorkOrder>(
20
+ const module = createWorkOrderModule();
21
+ const hosts = await initializeModuleHosts({
22
+ modules: [module.manifest],
23
+ installations: [module],
24
+ });
25
+ module.attach({
26
+ departments: new InMemoryEntityRepository<ManagedDepartment>(),
27
+ repository: new InMemoryScopedEntityRepository<WorkOrder>(
19
28
  [
20
29
  item("ticket_high", "high", "open"),
21
30
  item("ticket_progress", "medium", "in-progress"),
@@ -23,14 +32,10 @@ describe("示例工单后台任务", () => {
23
32
  ],
24
33
  workOrderDataScopeBinding,
25
34
  ),
26
- new InMemoryEntityRepository<ManagedDepartment>(),
27
- );
35
+ });
28
36
  const scheduler = new SchedulerService(
29
37
  new InMemorySchedulerRepository(),
30
- createWorkOrderTaskDefinitions(
31
- createFoundationSnapshot().features,
32
- service,
33
- ),
38
+ createModuleTaskDefinitions(hosts, createFoundationSnapshot().features),
34
39
  runtime,
35
40
  );
36
41
  await scheduler.initialize();
@@ -4,6 +4,7 @@ FROM oven/bun:1.3.14-alpine AS deps
4
4
  WORKDIR /app
5
5
 
6
6
  COPY package.json bun.lock ./
7
+ # windy-copy:deps begin
7
8
  COPY apps/license/package.json apps/license/package.json
8
9
  COPY apps/server/package.json apps/server/package.json
9
10
  COPY apps/signing-service/package.json apps/signing-service/package.json
@@ -20,14 +21,16 @@ COPY packages/modules/package.json packages/modules/package.json
20
21
  COPY packages/server-sdk/package.json packages/server-sdk/package.json
21
22
  COPY packages/shared/package.json packages/shared/package.json
22
23
  COPY packages/storage/package.json packages/storage/package.json
24
+ # windy-copy:deps end
23
25
  RUN bun install --frozen-lockfile
24
26
 
25
27
  FROM deps AS build
28
+ # windy-copy:build begin
26
29
  COPY apps/web apps/web
27
- COPY packages/example-work-order packages/example-work-order
28
30
  COPY packages/license-sdk packages/license-sdk
29
31
  COPY packages/modules packages/modules
30
32
  COPY packages/shared packages/shared
33
+ # windy-copy:build end
31
34
  RUN bun run --cwd apps/web build
32
35
 
33
36
  FROM deps AS runner
@@ -42,7 +42,10 @@ const state = vi.hoisted(() => {
42
42
  });
43
43
 
44
44
  vi.mock("@/composables/useAccessSnapshot", () => ({
45
- useAccessSnapshot: () => ({ snapshot: state.access }),
45
+ useAccessSnapshot: () => ({
46
+ snapshot: state.access,
47
+ ensureLoaded: vi.fn(async () => state.access.value),
48
+ }),
46
49
  }));
47
50
 
48
51
  vi.mock("@/composables/useAuthSession", () => ({
@@ -18,6 +18,8 @@ const access = useAccessSnapshot();
18
18
  const watermark = useWatermarkSettings(auth.actor);
19
19
  onMounted(() => {
20
20
  void watermark.ensureLoaded();
21
+ // 业务页路由守卫不会预取访问快照,这里兜底加载,失败时保持水印隐藏。
22
+ void access.ensureLoaded().catch(() => undefined);
21
23
  });
22
24
  const allowed = computed(() => {
23
25
  const snapshot = access.snapshot.value;
@@ -45,7 +45,10 @@ const state = vi.hoisted(() => ({
45
45
  }));
46
46
 
47
47
  vi.mock("@/composables/useAccessSnapshot", () => ({
48
- useAccessSnapshot: () => ({ snapshot: state.access }),
48
+ useAccessSnapshot: () => ({
49
+ snapshot: state.access,
50
+ ensureLoaded: vi.fn(async () => state.access.value),
51
+ }),
49
52
  }));
50
53
 
51
54
  vi.mock("@/composables/useAuthSession", () => ({
@@ -42,6 +42,10 @@ services:
42
42
  WINDY_CONTAINERIZED: "1"
43
43
  BUN_CONFIG_NO_CLEAR_TERMINAL_ON_RELOAD: "true"
44
44
  WINDY_SYSTEM_USAGE_PATHS: /app/data/audit
45
+ PLATFORM_ENABLE_AI: ${PLATFORM_ENABLE_AI:-false}
46
+ AGENT_SERVER_URL: http://agent-server:8080
47
+ AGENT_TOOL_HOST_URL: http://server:9747
48
+ AGENT_SERVER_INTERNAL_TOKEN: ${AGENT_SERVER_INTERNAL_TOKEN:-}
45
49
  ports:
46
50
  - "9747:9747"
47
51
  volumes:
@@ -52,6 +56,26 @@ services:
52
56
  migrate:
53
57
  condition: service_completed_successfully
54
58
 
59
+ agent-server:
60
+ profiles: ["agent"]
61
+ build:
62
+ context: .
63
+ dockerfile: apps/agent-server/Dockerfile
64
+ environment:
65
+ AGENT_SERVER_INTERNAL_TOKEN: ${AGENT_SERVER_INTERNAL_TOKEN:-}
66
+ AGENT_RUN_STORE_PATH: /app/data/agent/runs.sqlite3
67
+ QWEN_API_URL: ${QWEN_API_URL:-}
68
+ QWEN_API_KEY: ${QWEN_API_KEY:-}
69
+ QWEN_MODEL: ${QWEN_MODEL:-}
70
+ AI_PROVIDER_CONNECT_TIMEOUT_MS: ${AI_PROVIDER_CONNECT_TIMEOUT_MS:-5000}
71
+ AI_PROVIDER_REQUEST_TIMEOUT_MS: ${AI_PROVIDER_REQUEST_TIMEOUT_MS:-60000}
72
+ AI_PROVIDER_MAX_RESPONSE_BYTES: ${AI_PROVIDER_MAX_RESPONSE_BYTES:-2097152}
73
+ volumes:
74
+ - agent-run-data:/app/data/agent
75
+ depends_on:
76
+ server:
77
+ condition: service_started
78
+
55
79
  web:
56
80
  build:
57
81
  context: .
@@ -85,3 +109,4 @@ services:
85
109
  volumes:
86
110
  postgres-data:
87
111
  server-audit-data:
112
+ agent-run-data: