create-windy 0.9.1 → 0.10.1

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 (253) hide show
  1. package/dist/cli.js +1691 -1151
  2. package/package.json +2 -2
  3. package/template/.agents/skills/windy-business-module/SKILL.md +33 -1
  4. package/template/.windy-template.json +6 -2
  5. package/template/AGENTS.md +4 -0
  6. package/template/README.md +2 -0
  7. package/template/apps/agent-server/Dockerfile +1 -1
  8. package/template/apps/agent-server/README.md +1 -1
  9. package/template/apps/agent-server/native/EXTENSIONS.md +2 -2
  10. package/template/apps/agent-server/native/parallel-harness.ts +2 -0
  11. package/template/apps/agent-server/native/storage-lifecycle.test.ts +2 -1
  12. package/template/apps/agent-server/package.json +42 -41
  13. package/template/apps/agent-server/plugins/downloads/README.md +1 -1
  14. package/template/apps/agent-server/plugins/drop-overlay/README.md +1 -1
  15. package/template/apps/agent-server/plugins/settings/README.md +1 -1
  16. package/template/apps/agent-server/plugins/skill-library/client-chunks.test.ts +4 -1
  17. package/template/apps/agent-server/plugins/theme/README.md +5 -1
  18. package/template/apps/agent-server/plugins/theme/source-tokens.ts +0 -1
  19. package/template/apps/agent-server/plugins/theme/tokens.json +0 -4
  20. package/template/apps/agent-server/plugins/tool-catalog/README.md +6 -3
  21. package/template/apps/agent-server/plugins/tool-catalog/discovery.mjs +13 -10
  22. package/template/apps/agent-server/plugins/tool-catalog/discovery.test.ts +68 -6
  23. package/template/apps/agent-server/runtime/compatibility.ts +7 -1
  24. package/template/apps/agent-server/runtime/dsh-engine.ts +2 -0
  25. package/template/apps/agent-server/runtime/questions-workflow.nodecheck.ts +2 -0
  26. package/template/apps/server/Dockerfile +2 -1
  27. package/template/apps/server/package.json +11 -11
  28. package/template/apps/server/scripts/validate-module-composition-once.ts +2 -0
  29. package/template/apps/server/scripts/validate-module-composition.ts +8 -4
  30. package/template/apps/server/src/admin/routes.test.ts +29 -1
  31. package/template/apps/server/src/admin/routes.ts +20 -2
  32. package/template/apps/server/src/agent/client-session/completion.ts +1 -1
  33. package/template/apps/server/src/agent/client-session/manifest-admission.ts +1 -1
  34. package/template/apps/server/src/agent/client-session/request.ts +1 -1
  35. package/template/apps/server/src/agent/context-version-routes.ts +37 -13
  36. package/template/apps/server/src/agent/run-facade.ts +1 -1
  37. package/template/apps/server/src/agent/run-input-verification.ts +1 -1
  38. package/template/apps/server/src/agent/run-routes.ts +1 -14
  39. package/template/apps/server/src/agent/run-start-preparation.ts +1 -1
  40. package/template/apps/server/src/agent/runtime-bootstrap-input.ts +2 -0
  41. package/template/apps/server/src/agent/runtime-bootstrap.ts +49 -157
  42. package/template/apps/server/src/agent/runtime-disabled-composition.ts +2 -1
  43. package/template/apps/server/src/agent/session-invalidation/authorization-revocation.ts +54 -0
  44. package/template/apps/server/src/agent/session-runtime-composition.ts +1 -51
  45. package/template/apps/server/src/agent/unavailable-routes.ts +13 -2
  46. package/template/apps/server/src/agent-workflow/runtime-composition.ts +3 -0
  47. package/template/apps/server/src/agent-workflow/service.ts +17 -1
  48. package/template/apps/server/src/ai-artifact/media-resource-route.test.ts +134 -0
  49. package/template/apps/server/src/ai-artifact/routes.ts +1 -1
  50. package/template/apps/server/src/ai-artifact/runtime.ts +12 -0
  51. package/template/apps/server/src/ai-runtime-readiness/operation-port.test-support.ts +1 -1
  52. package/template/apps/server/src/ai-runtime-readiness/operation-port.ts +8 -0
  53. package/template/apps/server/src/ai-runtime-security/internal-token.ts +14 -0
  54. package/template/apps/server/src/app/antivirus-composition.test.ts +63 -0
  55. package/template/apps/server/src/app/antivirus-composition.ts +29 -0
  56. package/template/apps/server/src/app/dsh-workbench-socket-audit.test.ts +2 -1
  57. package/template/apps/server/src/configuration/definition.ts +15 -11
  58. package/template/apps/server/src/document/facade-support.ts +2 -2
  59. package/template/apps/server/src/document/facade.ts +2 -2
  60. package/template/apps/server/src/document/output-artifact-composition.ts +9 -5
  61. package/template/apps/server/src/document/output-artifact.test-support.ts +1 -1
  62. package/template/apps/server/src/embedding/facade.ts +1 -1
  63. package/template/apps/server/src/embedding/input.ts +1 -1
  64. package/template/apps/server/src/embedding/routes.ts +1 -1
  65. package/template/apps/server/src/index.ts +60 -60
  66. package/template/apps/server/src/license/routes.ts +4 -1
  67. package/template/apps/server/src/license/runtime-business-access.test.ts +261 -0
  68. package/template/apps/server/src/media-runtime/composition.ts +157 -0
  69. package/template/apps/server/src/media-runtime/media-only-composition.ts +44 -0
  70. package/template/apps/server/src/media-runtime/module-ai-port-factory.test.ts +69 -0
  71. package/template/apps/server/src/media-runtime/module-ai-port-factory.ts +72 -0
  72. package/template/apps/server/src/{agent → media-runtime}/runtime-readiness-sources.ts +14 -6
  73. package/template/apps/server/src/media-runtime/shared-composition.ts +104 -0
  74. package/template/apps/server/src/module-composition.test.ts +22 -0
  75. package/template/apps/server/src/module-composition.ts +48 -0
  76. package/template/apps/server/src/module-host/ai-operation-port.ts +97 -30
  77. package/template/apps/server/src/module-host/ai-operation-speech-normalize.test.ts +1 -1
  78. package/template/apps/server/src/module-host/http-runtime.ts +9 -3
  79. package/template/apps/server/src/module-host/static-route-authorization.test.ts +6 -3
  80. package/template/apps/server/src/module-host/storage-authorization-binding.test.ts +4 -0
  81. package/template/apps/server/src/module-runtime-validation.ts +2 -0
  82. package/template/apps/server/src/observability/diagnostic-index.ts +26 -1
  83. package/template/apps/server/src/observability/diagnostic-repository.ts +40 -11
  84. package/template/apps/server/src/platform-http-runtime.ts +1 -1
  85. package/template/apps/server/src/runtime-feature.ts +5 -1
  86. package/template/apps/server/src/runtime.ts +3 -0
  87. package/template/apps/server/src/settings/drizzle-watermark-repository.ts +35 -4
  88. package/template/apps/server/src/settings/watermark-repository.ts +14 -0
  89. package/template/apps/server/src/settings/watermark-routes.test.ts +84 -2
  90. package/template/apps/server/src/settings/watermark-routes.ts +109 -0
  91. package/template/apps/server/src/settings/watermark-transfer.ts +79 -0
  92. package/template/apps/server/src/speech/facade-validation.ts +1 -1
  93. package/template/apps/server/src/speech/facade.ts +2 -2
  94. package/template/apps/server/src/startup-composition-validation.test.ts +32 -0
  95. package/template/apps/server/src/startup-finalization.ts +2 -0
  96. package/template/apps/server/src/startup-runtime.ts +77 -0
  97. package/template/apps/server/src/system/ai-runtime-build.ts +3 -0
  98. package/template/apps/server/src/system/ai-runtime-configuration.test.ts +36 -0
  99. package/template/apps/server/src/system/ai-runtime-configuration.ts +58 -0
  100. package/template/apps/server/src/system/diagnostic-routes.test.ts +45 -0
  101. package/template/apps/server/src/system/diagnostic-routes.ts +35 -0
  102. package/template/apps/server/src/system/environment-policy-reader.test.ts +74 -0
  103. package/template/apps/server/src/system/environment-policy-reader.ts +62 -0
  104. package/template/apps/server/src/system/feature-environment-assessment.test.ts +148 -0
  105. package/template/apps/server/src/system/feature-environment-assessment.ts +179 -0
  106. package/template/apps/server/src/system/host-diagnostics.test.ts +155 -0
  107. package/template/apps/server/src/system/host-diagnostics.ts +225 -0
  108. package/template/apps/server/src/system/menu-registry.test.ts +26 -0
  109. package/template/apps/server/src/system/menu-registry.ts +9 -7
  110. package/template/apps/server/src/system/operations.test.ts +5 -0
  111. package/template/apps/server/src/system/operations.ts +36 -12
  112. package/template/apps/server/src/system/role-authorization-postgres.integration.test.ts +181 -0
  113. package/template/apps/web/Dockerfile +7 -3
  114. package/template/apps/web/dev/dev-port.test.ts +6 -6
  115. package/template/apps/web/dev/dev-port.ts +1 -1
  116. package/template/apps/web/dev/start.ts +6 -1
  117. package/template/apps/web/index.html +1 -0
  118. package/template/apps/web/package.json +16 -16
  119. package/template/apps/web/runtime-server.test.ts +22 -1
  120. package/template/apps/web/runtime-server.ts +6 -0
  121. package/template/apps/web/src/App.vue +6 -3
  122. package/template/apps/web/src/assets/windy.svg +4 -0
  123. package/template/apps/web/src/components/auth/LoginForm.vue +2 -2
  124. package/template/apps/web/src/components/auth/PasswordChangeForm.vue +14 -3
  125. package/template/apps/web/src/components/common/FormDialog.vue +1 -1
  126. package/template/apps/web/src/components/license/LicenseFlow.webtest.ts +3 -0
  127. package/template/apps/web/src/components/license/LicenseMachineCodeQr.vue +99 -0
  128. package/template/apps/web/src/components/license/LicenseMachineCodeQr.webtest.ts +83 -0
  129. package/template/apps/web/src/components/license/LicensePairingCard.vue +1 -1
  130. package/template/apps/web/src/components/license/LicenseStatusCard.vue +14 -2
  131. package/template/apps/web/src/composables/useLoginFlow.ts +2 -2
  132. package/template/apps/web/src/composables/usePlatformSettings.ts +1 -1
  133. package/template/apps/web/src/composables/useWatermarkSettings.ts +1 -0
  134. package/template/apps/web/src/layout/AppSidebar.vue +5 -9
  135. package/template/apps/web/src/layout/AppSidebar.webtest.ts +4 -1
  136. package/template/apps/web/src/pages/auth/ChangePasswordPage.vue +67 -6
  137. package/template/apps/web/src/pages/auth/LoginPage.vue +3 -12
  138. package/template/apps/web/src/pages/configuration/components/ModuleConfigurationEditor.vue +30 -0
  139. package/template/apps/web/src/pages/configuration/components/ModuleConfigurationWorkbench.vue +25 -14
  140. package/template/apps/web/src/pages/configuration/components/TimeZoneSelect.vue +55 -0
  141. package/template/apps/web/src/pages/profile/PersonalSettingsPage.vue +64 -11
  142. package/template/apps/web/src/pages/profile/PersonalSettingsPage.webtest.ts +21 -1
  143. package/template/apps/web/src/pages/settings/BrandingSettingsSection.vue +7 -18
  144. package/template/apps/web/src/pages/settings/BrandingSettingsSection.webtest.ts +9 -4
  145. package/template/apps/web/src/pages/settings/GlobalSettingsPage.vue +51 -6
  146. package/template/apps/web/src/pages/settings/GlobalSettingsPage.webtest.ts +19 -3
  147. package/template/apps/web/src/pages/settings/WatermarkSettingsSection.vue +1 -0
  148. package/template/apps/web/src/pages/settings/WatermarkYamlTransfer.vue +211 -0
  149. package/template/apps/web/src/pages/system/DiagnosticLookupPage.vue +18 -3
  150. package/template/apps/web/src/pages/system/DiagnosticLookupPage.webtest.ts +4 -1
  151. package/template/apps/web/src/pages/system/SystemOperationsPage.vue +23 -0
  152. package/template/apps/web/src/pages/system/components/DiagnosticLogList.vue +120 -0
  153. package/template/apps/web/src/pages/system/components/OperationEnvironmentPanel.vue +237 -0
  154. package/template/apps/web/src/pages/system/components/OperationFeatureAssessmentPanel.vue +131 -0
  155. package/template/apps/web/src/pages/system/components/PagedSelectionList.vue +31 -24
  156. package/template/apps/web/src/pages/system/components/SystemResourceActions.vue +2 -1
  157. package/template/apps/web/src/pages/system/components/SystemResourceEditor.vue +11 -2
  158. package/template/apps/web/src/pages/system/components/SystemResourcePage.vue +1 -0
  159. package/template/apps/web/src/pages/system/components/SystemResourcePage.webtest.ts +6 -2
  160. package/template/apps/web/src/pages/system/components/agent-skills/AgentSkillImportDialog.vue +18 -9
  161. package/template/apps/web/src/pages/system/components/agent-workbench/SessionReplayTimeline.vue +18 -9
  162. package/template/apps/web/src/pages/system/components/agent-workbench/TestRunComposer.vue +66 -34
  163. package/template/apps/web/src/pages/system/components/agent-workbench/ToolCatalogPanel.vue +48 -33
  164. package/template/apps/web/src/pages/system/components/agent-workbench/ToolCatalogPanel.webtest.ts +9 -2
  165. package/template/apps/web/src/pages/system/components/agent-workbench/ToolRuleDetail.vue +23 -16
  166. package/template/apps/web/src/pages/system/components/dictionary-bulk/DictionaryBulkJobDetail.vue +10 -7
  167. package/template/apps/web/src/pages/system/components/user-account/RolePermissionSummary.vue +81 -57
  168. package/template/apps/web/src/pages/system/components/user-account/UserRoleSelector.vue +6 -4
  169. package/template/apps/web/src/pages/system/components/user-account/UserRoleSelector.webtest.ts +20 -4
  170. package/template/apps/web/src/pages/system/resource-config.ts +9 -0
  171. package/template/apps/web/src/pages/work-orders/WorkOrderListPage.vue +1 -1
  172. package/template/apps/web/src/router/disabled-feature-loading.webtest.ts +78 -0
  173. package/template/apps/web/src/router/index.ts +3 -6
  174. package/template/apps/web/src/router/manifest-routes.ts +2 -3
  175. package/template/apps/web/src/router/platform-admin-pages.ts +21 -62
  176. package/template/apps/web/src/router/platform-admin-route-bindings.ts +4 -4
  177. package/template/apps/web/src/services/diagnostics-api.ts +13 -1
  178. package/template/apps/web/src/services/operations-api.ts +4 -0
  179. package/template/apps/web/src/services/platform-settings-api.ts +35 -1
  180. package/template/apps/web/vite.config.ts +9 -3
  181. package/template/deploy/environment/windy.environment.example.yaml +53 -0
  182. package/template/docker-compose.yml +4 -2
  183. package/template/docs/architecture/agent-admin-workbench-implementation.md +1 -1
  184. package/template/docs/architecture/agent-admin-workbench-plan.md +1 -1
  185. package/template/docs/architecture/agent-workflow.md +8 -0
  186. package/template/docs/architecture/durable-jobs.md +17 -0
  187. package/template/docs/architecture/embedded-dsh-integration.md +1 -1
  188. package/template/docs/development/dsh-plugins.md +1 -1
  189. package/template/docs/development/performance-engineering.md +24 -0
  190. package/template/docs/operations/admin-settings-yaml.md +29 -0
  191. package/template/docs/operations/dsh-agent-migration.md +2 -2
  192. package/template/docs/operations/environment-capabilities.md +185 -0
  193. package/template/docs/platform/module-configuration.md +18 -3
  194. package/template/docs/platform/platform-authentication.md +1 -1
  195. package/template/package.json +10 -8
  196. package/template/packages/config/package.json +2 -2
  197. package/template/packages/config/src/definition.ts +14 -4
  198. package/template/packages/config/src/environment-loader.ts +27 -3
  199. package/template/packages/config/src/platform-ai-runtime.test.ts +94 -0
  200. package/template/packages/config/src/platform.ts +5 -2
  201. package/template/packages/database/drizzle/0065_durable_job_lease_snapshot_lookup.sql +1 -0
  202. package/template/packages/database/drizzle/0066_agent_source_sequence_bigint.sql +3 -0
  203. package/template/packages/database/drizzle/0067_media_runtime_access.sql +46 -0
  204. package/template/packages/database/drizzle/meta/0065_snapshot.json +15370 -0
  205. package/template/packages/database/drizzle/meta/0066_snapshot.json +15370 -0
  206. package/template/packages/database/drizzle/meta/0067_snapshot.json +15370 -0
  207. package/template/packages/database/drizzle/meta/_journal.json +21 -0
  208. package/template/packages/database/package.json +3 -3
  209. package/template/packages/database/src/schema/agent-session.ts +4 -3
  210. package/template/packages/database/src/schema/durable-jobs.ts +4 -0
  211. package/template/packages/database/src/schema/index.ts +3 -1
  212. package/template/packages/database/src/schema-durable-recovery.test.ts +21 -1
  213. package/template/packages/database/src/schema-source-sequence.test.ts +24 -0
  214. package/template/packages/example-work-order/package.json +2 -2
  215. package/template/packages/modules/package.json +1 -1
  216. package/template/packages/modules/src/admin-ui-boundary.test.ts +33 -0
  217. package/template/packages/modules/src/composition.ts +11 -28
  218. package/template/packages/modules/src/manifest.ts +2 -0
  219. package/template/packages/modules/src/menu-composition.ts +24 -0
  220. package/template/packages/modules/src/system-admin-routes.ts +2 -2
  221. package/template/packages/modules/src/system-admin-ui-catalog.ts +6 -1
  222. package/template/packages/modules/src/system-ai-api-bindings.ts +0 -16
  223. package/template/packages/modules/src/system-api-control-plane.ts +9 -1
  224. package/template/packages/modules/src/system-api-permissions.ts +7 -0
  225. package/template/packages/modules/src/system-api-watermark.ts +21 -0
  226. package/template/packages/modules/src/system-audit-actions.ts +3 -1
  227. package/template/packages/modules/src/system-features.ts +21 -3
  228. package/template/packages/modules/src/system-media-api-bindings.ts +97 -0
  229. package/template/packages/modules/src/system-module-audit-selection.ts +2 -0
  230. package/template/packages/modules/src/system-module-catalog.test.ts +13 -6
  231. package/template/packages/modules/src/system-module-catalog.ts +6 -241
  232. package/template/packages/modules/src/system-module-definitions.ts +260 -0
  233. package/template/packages/modules/src/system-modules-access.test.ts +2 -0
  234. package/template/packages/modules/src/system-modules.test.ts +7 -1
  235. package/template/packages/modules/src/system-modules.ts +25 -13
  236. package/template/packages/modules/src/system-permissions.ts +12 -0
  237. package/template/packages/server-sdk/package.json +1 -1
  238. package/template/packages/shared/index.ts +3 -0
  239. package/template/packages/shared/package.json +1 -1
  240. package/template/packages/shared/src/environment-assessment.ts +68 -0
  241. package/template/packages/shared/src/environment-policy.ts +205 -0
  242. package/template/packages/shared/src/license-console-capabilities.ts +7 -0
  243. package/template/packages/shared/src/license.ts +1 -0
  244. package/template/packages/shared/src/module-configuration.ts +2 -1
  245. package/template/scripts/operations/collect-device-info.py +230 -0
  246. package/template/apps/server/src/agent/module-ai-port-factory.ts +0 -41
  247. /package/template/apps/server/src/{agent → ai-runtime-security}/execution-grants.ts +0 -0
  248. /package/template/apps/server/src/{agent → ai-runtime-security}/idempotency-key.test.ts +0 -0
  249. /package/template/apps/server/src/{agent → ai-runtime-security}/idempotency-key.ts +0 -0
  250. /package/template/apps/server/src/{agent → ai-runtime-security}/provider-input-governance.test.ts +0 -0
  251. /package/template/apps/server/src/{agent → ai-runtime-security}/provider-input-governance.ts +0 -0
  252. /package/template/apps/server/src/{agent → media-runtime}/required-environment.ts +0 -0
  253. /package/template/apps/server/src/{agent → media-runtime}/runtime-readiness-sources.test.ts +0 -0
@@ -7,7 +7,7 @@ import {
7
7
  } from "./static-route-authorization.js";
8
8
 
9
9
  // @windy-module system.license begin
10
- test("平台静态 Route 使用 Manifest Feature 与 Permission Guard", async () => {
10
+ test("License 基础状态免管理 Feature,安装操作仍经过 Manifest Guard", async () => {
11
11
  const modules = systemModuleManifests();
12
12
  const runtime = createServerRuntime(
13
13
  { WINDY_DEV_ADMIN_TOKEN: "token_1" },
@@ -20,10 +20,13 @@ test("平台静态 Route 使用 Manifest Feature 与 Permission Guard", async ()
20
20
  }),
21
21
  );
22
22
 
23
+ await expect(
24
+ authorizer.authorize(context, "GET", "/api/license/runtime"),
25
+ ).resolves.toBeUndefined();
23
26
  const denied = await authorizer.authorize(
24
27
  context,
25
- "GET",
26
- "/api/license/runtime",
28
+ "POST",
29
+ "/api/license/runtime/install",
27
30
  );
28
31
 
29
32
  expect(denied?.status).toBe(403);
@@ -8,11 +8,13 @@ import {
8
8
  type UploadPolicy,
9
9
  } from "@southwind-ai/storage";
10
10
  import type { ModuleUploadDecision } from "./storage-port.js";
11
+ // @windy-module system.agent begin
11
12
  import { buildScopeContext } from "../data-access/context.js";
12
13
  import { captureAuthorizationScope } from "../data-access/scope-snapshot.js";
13
14
  import { bindModuleUserProjection } from "../guards.js";
14
15
  import { createServerRuntime } from "../runtime.js";
15
16
  import { createModuleStorageScope } from "./storage-scope.js";
17
+ // @windy-module system.agent end
16
18
  import { createModuleStorageWritePort } from "./storage-write-port.js";
17
19
 
18
20
  const policy: UploadPolicy = {
@@ -146,6 +148,7 @@ describe("模块上传会话授权绑定", () => {
146
148
  ).toBe(true);
147
149
  });
148
150
 
151
+ // @windy-module system.agent begin
149
152
  test("Host 投影 service actor 时仍以业务用户 Scope 颁发绑定", async () => {
150
153
  const scopeContext = buildScopeContext("user_projected", undefined, [
151
154
  {
@@ -189,6 +192,7 @@ describe("模块上传会话授权绑定", () => {
189
192
  scopeHash: captureAuthorizationScope(scopeContext).hash,
190
193
  });
191
194
  });
195
+ // @windy-module system.agent end
192
196
  });
193
197
 
194
198
  interface SetupOptions {
@@ -4,6 +4,7 @@ import type { InitializedModuleHosts } from "./module-host.js";
4
4
 
5
5
  export function validateInstalledRuntime(input: {
6
6
  modules: ModuleManifest[];
7
+ disabledFeatureKeys?: ReadonlySet<string>;
7
8
  routes: readonly { method: string; path: string }[];
8
9
  tasks?: readonly { key: string }[];
9
10
  tools?: ReadonlyMap<string, unknown>;
@@ -15,6 +16,7 @@ export function validateInstalledRuntime(input: {
15
16
  );
16
17
  validateServerModuleComposition({
17
18
  modules: input.modules,
19
+ disabledFeatureKeys: input.disabledFeatureKeys,
18
20
  routes: input.routes,
19
21
  frameworkRoutes: ["GET /api/swagger", "GET /api/swagger/json"],
20
22
  tasks: input.tasks,
@@ -1,5 +1,9 @@
1
1
  import { randomUUIDv7 } from "bun";
2
- import type { DiagnosticRecord } from "@southwind-ai/shared";
2
+ import type {
3
+ DiagnosticRecord,
4
+ PageRequest,
5
+ PageResult,
6
+ } from "@southwind-ai/shared";
3
7
  import type { NodePgDatabase } from "drizzle-orm/node-postgres";
4
8
  import { DrizzleDiagnosticIndex } from "./diagnostic-repository.js";
5
9
 
@@ -12,6 +16,7 @@ const ID_PATTERN =
12
16
  export interface DiagnosticIndex {
13
17
  record(record: DiagnosticRecord): Promise<void>;
14
18
  find(id: string, now?: Date): Promise<DiagnosticRecord | undefined>;
19
+ list(page: PageRequest, now?: Date): Promise<PageResult<DiagnosticRecord>>;
15
20
  purgeExpired(now?: Date): Promise<number>;
16
21
  }
17
22
 
@@ -76,6 +81,26 @@ export class InMemoryDiagnosticIndex implements DiagnosticIndex {
76
81
  : undefined;
77
82
  }
78
83
 
84
+ async list(
85
+ { page, pageSize }: PageRequest,
86
+ now = new Date(),
87
+ ): Promise<PageResult<DiagnosticRecord>> {
88
+ const active = [...this.items.values()]
89
+ .filter((record) => new Date(record.expiresAt) > now)
90
+ .sort(
91
+ (left, right) =>
92
+ right.occurredAt.localeCompare(left.occurredAt) ||
93
+ right.diagnosticId.localeCompare(left.diagnosticId),
94
+ );
95
+ const start = (page - 1) * pageSize;
96
+ return {
97
+ items: structuredClone(active.slice(start, start + pageSize)),
98
+ total: active.length,
99
+ page,
100
+ pageSize,
101
+ };
102
+ }
103
+
79
104
  async purgeExpired(now = new Date()): Promise<number> {
80
105
  let count = 0;
81
106
  for (const [id, item] of this.items) {
@@ -1,7 +1,11 @@
1
- import { and, eq, gt, inArray, lte } from "drizzle-orm";
1
+ import { and, desc, eq, gt, inArray, lte } from "drizzle-orm";
2
2
  import type { NodePgDatabase } from "drizzle-orm/node-postgres";
3
3
  import { diagnosticEvents } from "@southwind-ai/database";
4
- import type { DiagnosticRecord } from "@southwind-ai/shared";
4
+ import type {
5
+ DiagnosticRecord,
6
+ PageRequest,
7
+ PageResult,
8
+ } from "@southwind-ai/shared";
5
9
  import type { DiagnosticIndex } from "./diagnostic-index.js";
6
10
 
7
11
  export class DrizzleDiagnosticIndex implements DiagnosticIndex {
@@ -32,15 +36,30 @@ export class DrizzleDiagnosticIndex implements DiagnosticIndex {
32
36
  ),
33
37
  )
34
38
  .limit(1);
35
- return row
36
- ? {
37
- ...row,
38
- service: "server",
39
- stage: "http",
40
- occurredAt: row.occurredAt.toISOString(),
41
- expiresAt: row.expiresAt.toISOString(),
42
- }
43
- : undefined;
39
+ return row ? toRecord(row) : undefined;
40
+ }
41
+
42
+ async list(
43
+ { page, pageSize }: PageRequest,
44
+ now = new Date(),
45
+ ): Promise<PageResult<DiagnosticRecord>> {
46
+ const active = gt(diagnosticEvents.expiresAt, now);
47
+ const total = await this.db.$count(diagnosticEvents, active);
48
+ const start = (page - 1) * pageSize;
49
+ const rows =
50
+ start < total
51
+ ? await this.db
52
+ .select()
53
+ .from(diagnosticEvents)
54
+ .where(active)
55
+ .orderBy(
56
+ desc(diagnosticEvents.occurredAt),
57
+ desc(diagnosticEvents.diagnosticId),
58
+ )
59
+ .limit(pageSize)
60
+ .offset(start)
61
+ : [];
62
+ return { items: rows.map(toRecord), total, page, pageSize };
44
63
  }
45
64
 
46
65
  async purgeExpired(now = new Date()): Promise<number> {
@@ -65,3 +84,13 @@ export class DrizzleDiagnosticIndex implements DiagnosticIndex {
65
84
  return removed.length;
66
85
  }
67
86
  }
87
+
88
+ function toRecord(row: typeof diagnosticEvents.$inferSelect): DiagnosticRecord {
89
+ return {
90
+ ...row,
91
+ service: "server",
92
+ stage: "http",
93
+ occurredAt: row.occurredAt.toISOString(),
94
+ expiresAt: row.expiresAt.toISOString(),
95
+ };
96
+ }
@@ -117,7 +117,7 @@ export function listenPlatformServer(
117
117
  ): number {
118
118
  const port = process.env.PORT ? Number(process.env.PORT) : 9_747;
119
119
  app.listen(port);
120
- return port;
120
+ return app.server?.port ?? port;
121
121
  }
122
122
 
123
123
  export function logServerStartup(
@@ -39,6 +39,7 @@ export class RuntimeFeatureCatalog {
39
39
  private readonly resolver: FeatureFlagRuntimeResolver | undefined,
40
40
  private readonly production: boolean,
41
41
  private readonly disabledModuleNames: ReadonlySet<string> = new Set(),
42
+ private readonly disabledFeatureKeys: ReadonlySet<string> = new Set(),
42
43
  ) {
43
44
  for (const definition of definitions) {
44
45
  this.definitions.set(definition.key, definition);
@@ -108,7 +109,10 @@ export class RuntimeFeatureCatalog {
108
109
  private async resolveOwnState(
109
110
  manifestFeature: FeatureFlagDefinition,
110
111
  ): Promise<FeatureFlagDefinition> {
111
- if (this.disabledModuleNames.has(manifestFeature.module)) {
112
+ if (
113
+ this.disabledModuleNames.has(manifestFeature.module) ||
114
+ this.disabledFeatureKeys.has(manifestFeature.key)
115
+ ) {
112
116
  return disabledFeature(manifestFeature);
113
117
  }
114
118
  if (!this.resolver) {
@@ -70,6 +70,7 @@ export interface ServerRuntimeOptions {
70
70
  featureResolver?: FeatureFlagRuntimeResolver;
71
71
  featureCatalog?: readonly FeatureFlagDefinition[];
72
72
  disabledModuleNames?: ReadonlySet<string>;
73
+ disabledFeatureKeys?: ReadonlySet<string>;
73
74
  auditWriter?: AuditEventWriter;
74
75
  auditReader?: AuditEventReader;
75
76
  }
@@ -197,11 +198,13 @@ export function createServerRuntime(
197
198
  options.featureResolver,
198
199
  env.NODE_ENV === "production",
199
200
  options.disabledModuleNames ?? new Set(),
201
+ options.disabledFeatureKeys,
200
202
  );
201
203
  const requestSourcePolicy = requestSourcePolicyFromEnv(env);
202
204
 
203
205
  return {
204
206
  auditSink,
207
+ featureDefinitions: options.featureCatalog ?? [],
205
208
  async createGuardContext(
206
209
  request: Request,
207
210
  metadata: { clientIp?: string } = {},
@@ -1,6 +1,6 @@
1
1
  import { platformWatermarkPolicies } from "@southwind-ai/database";
2
2
  import type { PlatformWatermarkPolicy } from "@southwind-ai/shared";
3
- import { eq } from "drizzle-orm";
3
+ import { and, eq, sql } from "drizzle-orm";
4
4
  import type { NodePgDatabase } from "drizzle-orm/node-postgres";
5
5
  import type { WatermarkPolicyRepository } from "./watermark-repository.js";
6
6
 
@@ -25,6 +25,25 @@ export class DrizzleWatermarkPolicyRepository implements WatermarkPolicyReposito
25
25
  policy: PlatformWatermarkPolicy,
26
26
  actorId: string,
27
27
  ): Promise<PlatformWatermarkPolicy> {
28
+ const updated = await this.write(policy, actorId);
29
+ if (!updated) throw new Error("水印策略保存失败");
30
+ return updated;
31
+ }
32
+
33
+ updateIfCurrent(
34
+ before: PlatformWatermarkPolicy,
35
+ policy: PlatformWatermarkPolicy,
36
+ actorId: string,
37
+ ) {
38
+ return this.write(policy, actorId, before);
39
+ }
40
+
41
+ private async write(
42
+ policy: PlatformWatermarkPolicy,
43
+ actorId: string,
44
+ before?: PlatformWatermarkPolicy,
45
+ ) {
46
+ const table = platformWatermarkPolicies;
28
47
  const [row] = await this.db
29
48
  .insert(platformWatermarkPolicies)
30
49
  .values({
@@ -35,7 +54,20 @@ export class DrizzleWatermarkPolicyRepository implements WatermarkPolicyReposito
35
54
  updatedBy: actorId,
36
55
  })
37
56
  .onConflictDoUpdate({
38
- target: platformWatermarkPolicies.id,
57
+ target: table.id,
58
+ setWhere:
59
+ before &&
60
+ and(
61
+ eq(
62
+ sql<boolean>`(${table.enabled} and ${table.businessPagesEnabled})`,
63
+ before.enabled,
64
+ ),
65
+ eq(table.density, before.density),
66
+ eq(table.contentFields, before.contentFields),
67
+ eq(table.customContent, before.customContent),
68
+ eq(table.opacityPercent, before.opacityPercent),
69
+ eq(table.fontSize, before.fontSize),
70
+ ),
39
71
  set: {
40
72
  enabled: policy.enabled,
41
73
  businessPagesEnabled: policy.enabled,
@@ -49,8 +81,7 @@ export class DrizzleWatermarkPolicyRepository implements WatermarkPolicyReposito
49
81
  },
50
82
  })
51
83
  .returning();
52
- if (!row) throw new Error("水印策略保存失败");
53
- return toPolicy(row);
84
+ return row ? toPolicy(row) : undefined;
54
85
  }
55
86
  }
56
87
 
@@ -4,6 +4,11 @@ import {
4
4
  } from "@southwind-ai/shared";
5
5
 
6
6
  export interface WatermarkPolicyRepository {
7
+ updateIfCurrent(
8
+ before: PlatformWatermarkPolicy,
9
+ policy: PlatformWatermarkPolicy,
10
+ actorId: string,
11
+ ): Promise<PlatformWatermarkPolicy | undefined>;
7
12
  get(): Promise<PlatformWatermarkPolicy>;
8
13
  update(
9
14
  policy: PlatformWatermarkPolicy,
@@ -22,6 +27,15 @@ export class InMemoryWatermarkPolicyRepository implements WatermarkPolicyReposit
22
27
  return { ...this.policy };
23
28
  }
24
29
 
30
+ async updateIfCurrent(
31
+ before: PlatformWatermarkPolicy,
32
+ policy: PlatformWatermarkPolicy,
33
+ actorId: string,
34
+ ) {
35
+ if (JSON.stringify(this.policy) !== JSON.stringify(before)) return;
36
+ return this.update(policy, actorId);
37
+ }
38
+
25
39
  async update(
26
40
  policy: PlatformWatermarkPolicy,
27
41
  _actorId: string,
@@ -17,7 +17,9 @@ class FakeRouteApp implements RouteApp {
17
17
  this.handlers.set(`GET ${path}`, handler);
18
18
  }
19
19
 
20
- post(): void {}
20
+ post(path: string, handler: (context: unknown) => unknown): void {
21
+ this.handlers.set(`POST ${path}`, handler);
22
+ }
21
23
 
22
24
  put(path: string, handler: (context: unknown) => unknown): void {
23
25
  this.handlers.set(`PUT ${path}`, handler);
@@ -25,7 +27,11 @@ class FakeRouteApp implements RouteApp {
25
27
 
26
28
  delete(): void {}
27
29
 
28
- invoke(method: "GET" | "PUT", path: string, context: unknown = {}): unknown {
30
+ invoke(
31
+ method: "GET" | "POST" | "PUT",
32
+ path: string,
33
+ context: unknown = {},
34
+ ): unknown {
29
35
  const handler = this.handlers.get(`${method} ${path}`);
30
36
  if (!handler) throw new Error(`未注册路由 ${method} ${path}`);
31
37
  return handler(context);
@@ -206,6 +212,82 @@ describe("平台水印策略路由", () => {
206
212
  });
207
213
  });
208
214
 
215
+ describe("水印 YAML 迁移", () => {
216
+ const path = "/system/settings/watermark/yaml";
217
+
218
+ test("导出白名单字段,预览不写入,拒绝过期确认并保留目标自定义内容", async () => {
219
+ const fixture = await createFixture(settingsActor(true));
220
+ const { app, guardContext, repository, runtime } = fixture;
221
+ const current = {
222
+ ...(await repository.get()),
223
+ customContent: "实例专用文字",
224
+ };
225
+ await update(fixture, current);
226
+
227
+ const exported = (await app.invoke("GET", path, {
228
+ guardContext,
229
+ })) as Response;
230
+ const yaml = (await exported.text()).replace("medium", "high");
231
+ expect(yaml).not.toContain("customContent");
232
+ expect(yaml).not.toContain(current.customContent);
233
+ const preview = async () =>
234
+ (await app.invoke("POST", `${path}/preview`, {
235
+ guardContext,
236
+ body: { yaml },
237
+ })) as { revision: string; changes: unknown[] };
238
+ const apply = (revision: string) =>
239
+ app.invoke("POST", path, {
240
+ guardContext,
241
+ body: { yaml, revision },
242
+ });
243
+
244
+ const first = await preview();
245
+ expect(first.changes).toEqual([
246
+ { key: "density", before: "medium", after: "high" },
247
+ ]);
248
+ expect(await repository.get()).toEqual(current);
249
+
250
+ await update(fixture, { ...current, density: "low" });
251
+ expect(((await apply(first.revision)) as Response).status).toBe(409);
252
+ expect((await repository.get()).density).toBe("low");
253
+ expect(await apply((await preview()).revision)).toMatchObject({
254
+ density: "high",
255
+ customContent: current.customContent,
256
+ });
257
+ expect(runtime.auditSink.list()).toEqual(
258
+ expect.arrayContaining([
259
+ expect.objectContaining({
260
+ action: "config.update",
261
+ metadata: { source: "yaml", changedFields: ["density"] },
262
+ }),
263
+ ]),
264
+ );
265
+ });
266
+
267
+ test("只读用户无法预览,未知字段不能导入", async () => {
268
+ const reader = await createFixture(settingsActor(false));
269
+ const exported = (await reader.app.invoke("GET", path, {
270
+ guardContext: reader.guardContext,
271
+ })) as Response;
272
+ const yaml = await exported.text();
273
+ expect(
274
+ (
275
+ (await reader.app.invoke("POST", `${path}/preview`, {
276
+ guardContext: reader.guardContext,
277
+ body: { yaml },
278
+ })) as Response
279
+ ).status,
280
+ ).toBe(403);
281
+
282
+ const admin = await createFixture(settingsActor(true));
283
+ const invalid = (await admin.app.invoke("POST", `${path}/preview`, {
284
+ guardContext: admin.guardContext,
285
+ body: { yaml: `${yaml} secret: redacted\n` },
286
+ })) as Response;
287
+ expect(invalid.status).toBe(400);
288
+ });
289
+ });
290
+
209
291
  async function update(
210
292
  fixture: Awaited<ReturnType<typeof createFixture>>,
211
293
  body: PlatformWatermarkPolicy,
@@ -1,3 +1,4 @@
1
+ import { systemApiPermissionBindings } from "@southwind-ai/modules";
1
2
  import type {
2
3
  ApiPermissionBinding,
3
4
  FeatureFlagDefinition,
@@ -15,6 +16,15 @@ import { resourceEvent, routeContext } from "../system/route-helpers.js";
15
16
  import type { RouteApp } from "../system/route-types.js";
16
17
  import type { WatermarkPolicyRepository } from "./watermark-repository.js";
17
18
 
19
+ import {
20
+ exportWatermark,
21
+ readWatermarkTransfer,
22
+ watermarkPreview,
23
+ } from "./watermark-transfer.js";
24
+
25
+ const YAML_PATH = "/system/settings/watermark/yaml";
26
+ const PREVIEW_PATH = `${YAML_PATH}/preview`;
27
+
18
28
  type ServerRuntime = ReturnType<typeof createServerRuntime>;
19
29
 
20
30
  const READ_BINDING = watermarkBinding("GET", "system.watermark.read");
@@ -28,6 +38,80 @@ export function registerWatermarkPolicyRoutes(
28
38
  ): void {
29
39
  const feature = findFeature(features, "system.watermark");
30
40
 
41
+ const exportBinding = yamlBinding("GET", YAML_PATH);
42
+ const previewBinding = yamlBinding("POST", PREVIEW_PATH);
43
+ const importBinding = yamlBinding("POST", YAML_PATH);
44
+
45
+ app.get(YAML_PATH, async (rawContext) => {
46
+ const { guardContext } = routeContext(rawContext);
47
+ const denied = await guard(runtime, guardContext, exportBinding, feature);
48
+ if (denied) return denied;
49
+ const yaml = exportWatermark(await repository.get());
50
+ return new Response(yaml, {
51
+ headers: {
52
+ "Content-Type": "application/yaml; charset=utf-8",
53
+ "Content-Disposition":
54
+ 'attachment; filename="windy-admin-settings.yaml"',
55
+ "Cache-Control": "no-store",
56
+ },
57
+ });
58
+ });
59
+
60
+ async function transfer(rawContext: unknown, apply: boolean) {
61
+ const { body, guardContext } = routeContext(rawContext);
62
+ const denied = await guard(
63
+ runtime,
64
+ guardContext,
65
+ apply ? importBinding : previewBinding,
66
+ feature,
67
+ );
68
+ if (denied) return denied;
69
+
70
+ const request = body as { yaml?: unknown; revision?: unknown } | null;
71
+ const yaml = readWatermarkTransfer(request?.yaml);
72
+ if (!yaml)
73
+ return invalidYaml(
74
+ "文件须为 8 KiB 以内的版本 1 配置,且只能包含支持的字段",
75
+ );
76
+
77
+ const before = await repository.get();
78
+ const parsed = parseWatermarkPolicy({
79
+ ...yaml,
80
+ businessPagesEnabled: yaml.enabled,
81
+ customContent: before.customContent,
82
+ });
83
+ if (!parsed.ok) return invalidYaml(parsed.message);
84
+
85
+ const next = { ...parsed.value, customContent: before.customContent };
86
+ const preview = watermarkPreview(before, next, guardContext.actor.id);
87
+ if (!apply) return preview;
88
+ if (request?.revision !== preview.revision) return stalePreview();
89
+ if (!preview.changes.length) return before;
90
+
91
+ const updated = await repository.updateIfCurrent(
92
+ before,
93
+ next,
94
+ guardContext.actor.id,
95
+ );
96
+ if (!updated) return stalePreview();
97
+ await runtime.auditSink.recordCriticalEvent(
98
+ resourceEvent(
99
+ guardContext,
100
+ "config.update",
101
+ "platform-watermark",
102
+ "platform",
103
+ {
104
+ source: "yaml",
105
+ changedFields: changedFields(before, updated),
106
+ },
107
+ ),
108
+ );
109
+ return updated;
110
+ }
111
+
112
+ app.post(PREVIEW_PATH, (context) => transfer(context, false));
113
+ app.post(YAML_PATH, (context) => transfer(context, true));
114
+
31
115
  app.get("/platform/watermark-policy", () => repository.get());
32
116
 
33
117
  app.get("/system/settings/watermark", async (rawContext) => {
@@ -67,6 +151,31 @@ export function registerWatermarkPolicyRoutes(
67
151
  });
68
152
  }
69
153
 
154
+ function yamlBinding(method: "GET" | "POST", path: string) {
155
+ const binding = systemApiPermissionBindings().find(
156
+ (entry) => entry.method === method && entry.path === `/api${path}`,
157
+ );
158
+ if (!binding) throw new Error(`水印 YAML API 未注册:${method} ${path}`);
159
+ return binding;
160
+ }
161
+
162
+ function invalidYaml(message: string) {
163
+ return Response.json(
164
+ { error: "INVALID_ADMIN_CONFIG_YAML", message },
165
+ { status: 400 },
166
+ );
167
+ }
168
+
169
+ function stalePreview() {
170
+ return Response.json(
171
+ {
172
+ error: "STALE_ADMIN_CONFIG_PREVIEW",
173
+ message: "配置或文件已变化,请重新预览",
174
+ },
175
+ { status: 409 },
176
+ );
177
+ }
178
+
70
179
  async function guard(
71
180
  runtime: ServerRuntime,
72
181
  context: ReturnType<typeof routeContext>["guardContext"],
@@ -0,0 +1,79 @@
1
+ import { createHash } from "node:crypto";
2
+ import type { PlatformWatermarkPolicy } from "@southwind-ai/shared";
3
+ import { parseDocument, stringify, visit } from "yaml";
4
+
5
+ const fields = [
6
+ "enabled",
7
+ "density",
8
+ "contentFields",
9
+ "opacityPercent",
10
+ "fontSize",
11
+ ] as const;
12
+
13
+ export function portableWatermark(policy: PlatformWatermarkPolicy) {
14
+ return {
15
+ enabled: policy.enabled,
16
+ density: policy.density,
17
+ contentFields: policy.contentFields,
18
+ opacityPercent: policy.opacityPercent,
19
+ fontSize: policy.fontSize,
20
+ };
21
+ }
22
+
23
+ export function exportWatermark(policy: PlatformWatermarkPolicy) {
24
+ return stringify({ version: 1, watermark: portableWatermark(policy) });
25
+ }
26
+
27
+ export function readWatermarkTransfer(input: unknown) {
28
+ if (typeof input !== "string" || Buffer.byteLength(input) > 8192) return;
29
+ try {
30
+ const doc = parseDocument(input, { schema: "core", uniqueKeys: true });
31
+ if (doc.errors.length || doc.warnings.length) return;
32
+ let safe = true;
33
+ visit(doc, {
34
+ Alias() {
35
+ safe = false;
36
+ },
37
+ Node(_key, node) {
38
+ if (node.tag || node.anchor) safe = false;
39
+ },
40
+ });
41
+ if (!safe) return;
42
+ const value: unknown = doc.toJS({ maxAliasCount: 0 });
43
+ if (!hasFields(value, ["version", "watermark"]) || value.version !== 1)
44
+ return;
45
+ if (!hasFields(value.watermark, fields)) return;
46
+ return value.watermark;
47
+ } catch {
48
+ return;
49
+ }
50
+ }
51
+
52
+ function hasFields(
53
+ value: unknown,
54
+ keys: readonly string[],
55
+ ): value is Record<string, unknown> {
56
+ return (
57
+ !!value &&
58
+ typeof value === "object" &&
59
+ !Array.isArray(value) &&
60
+ Object.keys(value).length === keys.length &&
61
+ keys.every((key) => Object.hasOwn(value, key))
62
+ );
63
+ }
64
+
65
+ export function watermarkPreview(
66
+ before: PlatformWatermarkPolicy,
67
+ after: PlatformWatermarkPolicy,
68
+ actorId: string,
69
+ ) {
70
+ const current = portableWatermark(before);
71
+ const next = portableWatermark(after);
72
+ const changes = fields
73
+ .filter((key) => JSON.stringify(current[key]) !== JSON.stringify(next[key]))
74
+ .map((key) => ({ key, before: current[key], after: next[key] }));
75
+ const revision = createHash("sha256")
76
+ .update(JSON.stringify([actorId, current, next]))
77
+ .digest("hex");
78
+ return { changes, revision };
79
+ }
@@ -9,7 +9,7 @@ import type { RequestGuardContext } from "../guards.js";
9
9
  import {
10
10
  governProviderInput,
11
11
  ProviderInputGovernanceError,
12
- } from "../agent/provider-input-governance.js";
12
+ } from "../ai-runtime-security/provider-input-governance.js";
13
13
  import { SpeechRuntimeError, type SpeechRuntimePort } from "./contracts.js";
14
14
  import { SpeechFacadeError } from "./errors.js";
15
15
  import { legacySpeechCapabilities } from "./facade-contract.js";
@@ -8,8 +8,8 @@ import type {
8
8
  } from "@southwind-ai/server-sdk";
9
9
  import { evaluateAccessGuards } from "../access-guards.js";
10
10
  import type { AiArtifactRuntime } from "../ai-artifact/runtime.js";
11
- import { ExecutionGrantStore } from "../agent/execution-grants.js";
12
- import { scopedRuntimeIdempotencyKey } from "../agent/idempotency-key.js";
11
+ import { ExecutionGrantStore } from "../ai-runtime-security/execution-grants.js";
12
+ import { scopedRuntimeIdempotencyKey } from "../ai-runtime-security/idempotency-key.js";
13
13
  import { auditActor, type RequestGuardContext } from "../guards.js";
14
14
  import type { InitializedModuleHosts } from "../module-host.js";
15
15
  import { toModuleActor } from "../module-host.js";