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.
- package/dist/cli.js +1691 -1151
- package/package.json +2 -2
- package/template/.agents/skills/windy-business-module/SKILL.md +33 -1
- package/template/.windy-template.json +6 -2
- package/template/AGENTS.md +4 -0
- package/template/README.md +2 -0
- package/template/apps/agent-server/Dockerfile +1 -1
- package/template/apps/agent-server/README.md +1 -1
- package/template/apps/agent-server/native/EXTENSIONS.md +2 -2
- package/template/apps/agent-server/native/parallel-harness.ts +2 -0
- package/template/apps/agent-server/native/storage-lifecycle.test.ts +2 -1
- package/template/apps/agent-server/package.json +42 -41
- package/template/apps/agent-server/plugins/downloads/README.md +1 -1
- package/template/apps/agent-server/plugins/drop-overlay/README.md +1 -1
- package/template/apps/agent-server/plugins/settings/README.md +1 -1
- package/template/apps/agent-server/plugins/skill-library/client-chunks.test.ts +4 -1
- package/template/apps/agent-server/plugins/theme/README.md +5 -1
- package/template/apps/agent-server/plugins/theme/source-tokens.ts +0 -1
- package/template/apps/agent-server/plugins/theme/tokens.json +0 -4
- package/template/apps/agent-server/plugins/tool-catalog/README.md +6 -3
- package/template/apps/agent-server/plugins/tool-catalog/discovery.mjs +13 -10
- package/template/apps/agent-server/plugins/tool-catalog/discovery.test.ts +68 -6
- package/template/apps/agent-server/runtime/compatibility.ts +7 -1
- package/template/apps/agent-server/runtime/dsh-engine.ts +2 -0
- package/template/apps/agent-server/runtime/questions-workflow.nodecheck.ts +2 -0
- package/template/apps/server/Dockerfile +2 -1
- package/template/apps/server/package.json +11 -11
- package/template/apps/server/scripts/validate-module-composition-once.ts +2 -0
- package/template/apps/server/scripts/validate-module-composition.ts +8 -4
- package/template/apps/server/src/admin/routes.test.ts +29 -1
- package/template/apps/server/src/admin/routes.ts +20 -2
- package/template/apps/server/src/agent/client-session/completion.ts +1 -1
- package/template/apps/server/src/agent/client-session/manifest-admission.ts +1 -1
- package/template/apps/server/src/agent/client-session/request.ts +1 -1
- package/template/apps/server/src/agent/context-version-routes.ts +37 -13
- package/template/apps/server/src/agent/run-facade.ts +1 -1
- package/template/apps/server/src/agent/run-input-verification.ts +1 -1
- package/template/apps/server/src/agent/run-routes.ts +1 -14
- package/template/apps/server/src/agent/run-start-preparation.ts +1 -1
- package/template/apps/server/src/agent/runtime-bootstrap-input.ts +2 -0
- package/template/apps/server/src/agent/runtime-bootstrap.ts +49 -157
- package/template/apps/server/src/agent/runtime-disabled-composition.ts +2 -1
- package/template/apps/server/src/agent/session-invalidation/authorization-revocation.ts +54 -0
- package/template/apps/server/src/agent/session-runtime-composition.ts +1 -51
- package/template/apps/server/src/agent/unavailable-routes.ts +13 -2
- package/template/apps/server/src/agent-workflow/runtime-composition.ts +3 -0
- package/template/apps/server/src/agent-workflow/service.ts +17 -1
- package/template/apps/server/src/ai-artifact/media-resource-route.test.ts +134 -0
- package/template/apps/server/src/ai-artifact/routes.ts +1 -1
- package/template/apps/server/src/ai-artifact/runtime.ts +12 -0
- package/template/apps/server/src/ai-runtime-readiness/operation-port.test-support.ts +1 -1
- package/template/apps/server/src/ai-runtime-readiness/operation-port.ts +8 -0
- package/template/apps/server/src/ai-runtime-security/internal-token.ts +14 -0
- package/template/apps/server/src/app/antivirus-composition.test.ts +63 -0
- package/template/apps/server/src/app/antivirus-composition.ts +29 -0
- package/template/apps/server/src/app/dsh-workbench-socket-audit.test.ts +2 -1
- package/template/apps/server/src/configuration/definition.ts +15 -11
- package/template/apps/server/src/document/facade-support.ts +2 -2
- package/template/apps/server/src/document/facade.ts +2 -2
- package/template/apps/server/src/document/output-artifact-composition.ts +9 -5
- package/template/apps/server/src/document/output-artifact.test-support.ts +1 -1
- package/template/apps/server/src/embedding/facade.ts +1 -1
- package/template/apps/server/src/embedding/input.ts +1 -1
- package/template/apps/server/src/embedding/routes.ts +1 -1
- package/template/apps/server/src/index.ts +60 -60
- package/template/apps/server/src/license/routes.ts +4 -1
- package/template/apps/server/src/license/runtime-business-access.test.ts +261 -0
- package/template/apps/server/src/media-runtime/composition.ts +157 -0
- package/template/apps/server/src/media-runtime/media-only-composition.ts +44 -0
- package/template/apps/server/src/media-runtime/module-ai-port-factory.test.ts +69 -0
- package/template/apps/server/src/media-runtime/module-ai-port-factory.ts +72 -0
- package/template/apps/server/src/{agent → media-runtime}/runtime-readiness-sources.ts +14 -6
- package/template/apps/server/src/media-runtime/shared-composition.ts +104 -0
- package/template/apps/server/src/module-composition.test.ts +22 -0
- package/template/apps/server/src/module-composition.ts +48 -0
- package/template/apps/server/src/module-host/ai-operation-port.ts +97 -30
- package/template/apps/server/src/module-host/ai-operation-speech-normalize.test.ts +1 -1
- package/template/apps/server/src/module-host/http-runtime.ts +9 -3
- package/template/apps/server/src/module-host/static-route-authorization.test.ts +6 -3
- package/template/apps/server/src/module-host/storage-authorization-binding.test.ts +4 -0
- package/template/apps/server/src/module-runtime-validation.ts +2 -0
- package/template/apps/server/src/observability/diagnostic-index.ts +26 -1
- package/template/apps/server/src/observability/diagnostic-repository.ts +40 -11
- package/template/apps/server/src/platform-http-runtime.ts +1 -1
- package/template/apps/server/src/runtime-feature.ts +5 -1
- package/template/apps/server/src/runtime.ts +3 -0
- package/template/apps/server/src/settings/drizzle-watermark-repository.ts +35 -4
- package/template/apps/server/src/settings/watermark-repository.ts +14 -0
- package/template/apps/server/src/settings/watermark-routes.test.ts +84 -2
- package/template/apps/server/src/settings/watermark-routes.ts +109 -0
- package/template/apps/server/src/settings/watermark-transfer.ts +79 -0
- package/template/apps/server/src/speech/facade-validation.ts +1 -1
- package/template/apps/server/src/speech/facade.ts +2 -2
- package/template/apps/server/src/startup-composition-validation.test.ts +32 -0
- package/template/apps/server/src/startup-finalization.ts +2 -0
- package/template/apps/server/src/startup-runtime.ts +77 -0
- package/template/apps/server/src/system/ai-runtime-build.ts +3 -0
- package/template/apps/server/src/system/ai-runtime-configuration.test.ts +36 -0
- package/template/apps/server/src/system/ai-runtime-configuration.ts +58 -0
- package/template/apps/server/src/system/diagnostic-routes.test.ts +45 -0
- package/template/apps/server/src/system/diagnostic-routes.ts +35 -0
- package/template/apps/server/src/system/environment-policy-reader.test.ts +74 -0
- package/template/apps/server/src/system/environment-policy-reader.ts +62 -0
- package/template/apps/server/src/system/feature-environment-assessment.test.ts +148 -0
- package/template/apps/server/src/system/feature-environment-assessment.ts +179 -0
- package/template/apps/server/src/system/host-diagnostics.test.ts +155 -0
- package/template/apps/server/src/system/host-diagnostics.ts +225 -0
- package/template/apps/server/src/system/menu-registry.test.ts +26 -0
- package/template/apps/server/src/system/menu-registry.ts +9 -7
- package/template/apps/server/src/system/operations.test.ts +5 -0
- package/template/apps/server/src/system/operations.ts +36 -12
- package/template/apps/server/src/system/role-authorization-postgres.integration.test.ts +181 -0
- package/template/apps/web/Dockerfile +7 -3
- package/template/apps/web/dev/dev-port.test.ts +6 -6
- package/template/apps/web/dev/dev-port.ts +1 -1
- package/template/apps/web/dev/start.ts +6 -1
- package/template/apps/web/index.html +1 -0
- package/template/apps/web/package.json +16 -16
- package/template/apps/web/runtime-server.test.ts +22 -1
- package/template/apps/web/runtime-server.ts +6 -0
- package/template/apps/web/src/App.vue +6 -3
- package/template/apps/web/src/assets/windy.svg +4 -0
- package/template/apps/web/src/components/auth/LoginForm.vue +2 -2
- package/template/apps/web/src/components/auth/PasswordChangeForm.vue +14 -3
- package/template/apps/web/src/components/common/FormDialog.vue +1 -1
- package/template/apps/web/src/components/license/LicenseFlow.webtest.ts +3 -0
- package/template/apps/web/src/components/license/LicenseMachineCodeQr.vue +99 -0
- package/template/apps/web/src/components/license/LicenseMachineCodeQr.webtest.ts +83 -0
- package/template/apps/web/src/components/license/LicensePairingCard.vue +1 -1
- package/template/apps/web/src/components/license/LicenseStatusCard.vue +14 -2
- package/template/apps/web/src/composables/useLoginFlow.ts +2 -2
- package/template/apps/web/src/composables/usePlatformSettings.ts +1 -1
- package/template/apps/web/src/composables/useWatermarkSettings.ts +1 -0
- package/template/apps/web/src/layout/AppSidebar.vue +5 -9
- package/template/apps/web/src/layout/AppSidebar.webtest.ts +4 -1
- package/template/apps/web/src/pages/auth/ChangePasswordPage.vue +67 -6
- package/template/apps/web/src/pages/auth/LoginPage.vue +3 -12
- package/template/apps/web/src/pages/configuration/components/ModuleConfigurationEditor.vue +30 -0
- package/template/apps/web/src/pages/configuration/components/ModuleConfigurationWorkbench.vue +25 -14
- package/template/apps/web/src/pages/configuration/components/TimeZoneSelect.vue +55 -0
- package/template/apps/web/src/pages/profile/PersonalSettingsPage.vue +64 -11
- package/template/apps/web/src/pages/profile/PersonalSettingsPage.webtest.ts +21 -1
- package/template/apps/web/src/pages/settings/BrandingSettingsSection.vue +7 -18
- package/template/apps/web/src/pages/settings/BrandingSettingsSection.webtest.ts +9 -4
- package/template/apps/web/src/pages/settings/GlobalSettingsPage.vue +51 -6
- package/template/apps/web/src/pages/settings/GlobalSettingsPage.webtest.ts +19 -3
- package/template/apps/web/src/pages/settings/WatermarkSettingsSection.vue +1 -0
- package/template/apps/web/src/pages/settings/WatermarkYamlTransfer.vue +211 -0
- package/template/apps/web/src/pages/system/DiagnosticLookupPage.vue +18 -3
- package/template/apps/web/src/pages/system/DiagnosticLookupPage.webtest.ts +4 -1
- package/template/apps/web/src/pages/system/SystemOperationsPage.vue +23 -0
- package/template/apps/web/src/pages/system/components/DiagnosticLogList.vue +120 -0
- package/template/apps/web/src/pages/system/components/OperationEnvironmentPanel.vue +237 -0
- package/template/apps/web/src/pages/system/components/OperationFeatureAssessmentPanel.vue +131 -0
- package/template/apps/web/src/pages/system/components/PagedSelectionList.vue +31 -24
- package/template/apps/web/src/pages/system/components/SystemResourceActions.vue +2 -1
- package/template/apps/web/src/pages/system/components/SystemResourceEditor.vue +11 -2
- package/template/apps/web/src/pages/system/components/SystemResourcePage.vue +1 -0
- package/template/apps/web/src/pages/system/components/SystemResourcePage.webtest.ts +6 -2
- package/template/apps/web/src/pages/system/components/agent-skills/AgentSkillImportDialog.vue +18 -9
- package/template/apps/web/src/pages/system/components/agent-workbench/SessionReplayTimeline.vue +18 -9
- package/template/apps/web/src/pages/system/components/agent-workbench/TestRunComposer.vue +66 -34
- package/template/apps/web/src/pages/system/components/agent-workbench/ToolCatalogPanel.vue +48 -33
- package/template/apps/web/src/pages/system/components/agent-workbench/ToolCatalogPanel.webtest.ts +9 -2
- package/template/apps/web/src/pages/system/components/agent-workbench/ToolRuleDetail.vue +23 -16
- package/template/apps/web/src/pages/system/components/dictionary-bulk/DictionaryBulkJobDetail.vue +10 -7
- package/template/apps/web/src/pages/system/components/user-account/RolePermissionSummary.vue +81 -57
- package/template/apps/web/src/pages/system/components/user-account/UserRoleSelector.vue +6 -4
- package/template/apps/web/src/pages/system/components/user-account/UserRoleSelector.webtest.ts +20 -4
- package/template/apps/web/src/pages/system/resource-config.ts +9 -0
- package/template/apps/web/src/pages/work-orders/WorkOrderListPage.vue +1 -1
- package/template/apps/web/src/router/disabled-feature-loading.webtest.ts +78 -0
- package/template/apps/web/src/router/index.ts +3 -6
- package/template/apps/web/src/router/manifest-routes.ts +2 -3
- package/template/apps/web/src/router/platform-admin-pages.ts +21 -62
- package/template/apps/web/src/router/platform-admin-route-bindings.ts +4 -4
- package/template/apps/web/src/services/diagnostics-api.ts +13 -1
- package/template/apps/web/src/services/operations-api.ts +4 -0
- package/template/apps/web/src/services/platform-settings-api.ts +35 -1
- package/template/apps/web/vite.config.ts +9 -3
- package/template/deploy/environment/windy.environment.example.yaml +53 -0
- package/template/docker-compose.yml +4 -2
- package/template/docs/architecture/agent-admin-workbench-implementation.md +1 -1
- package/template/docs/architecture/agent-admin-workbench-plan.md +1 -1
- package/template/docs/architecture/agent-workflow.md +8 -0
- package/template/docs/architecture/durable-jobs.md +17 -0
- package/template/docs/architecture/embedded-dsh-integration.md +1 -1
- package/template/docs/development/dsh-plugins.md +1 -1
- package/template/docs/development/performance-engineering.md +24 -0
- package/template/docs/operations/admin-settings-yaml.md +29 -0
- package/template/docs/operations/dsh-agent-migration.md +2 -2
- package/template/docs/operations/environment-capabilities.md +185 -0
- package/template/docs/platform/module-configuration.md +18 -3
- package/template/docs/platform/platform-authentication.md +1 -1
- package/template/package.json +10 -8
- package/template/packages/config/package.json +2 -2
- package/template/packages/config/src/definition.ts +14 -4
- package/template/packages/config/src/environment-loader.ts +27 -3
- package/template/packages/config/src/platform-ai-runtime.test.ts +94 -0
- package/template/packages/config/src/platform.ts +5 -2
- package/template/packages/database/drizzle/0065_durable_job_lease_snapshot_lookup.sql +1 -0
- package/template/packages/database/drizzle/0066_agent_source_sequence_bigint.sql +3 -0
- package/template/packages/database/drizzle/0067_media_runtime_access.sql +46 -0
- package/template/packages/database/drizzle/meta/0065_snapshot.json +15370 -0
- package/template/packages/database/drizzle/meta/0066_snapshot.json +15370 -0
- package/template/packages/database/drizzle/meta/0067_snapshot.json +15370 -0
- package/template/packages/database/drizzle/meta/_journal.json +21 -0
- package/template/packages/database/package.json +3 -3
- package/template/packages/database/src/schema/agent-session.ts +4 -3
- package/template/packages/database/src/schema/durable-jobs.ts +4 -0
- package/template/packages/database/src/schema/index.ts +3 -1
- package/template/packages/database/src/schema-durable-recovery.test.ts +21 -1
- package/template/packages/database/src/schema-source-sequence.test.ts +24 -0
- package/template/packages/example-work-order/package.json +2 -2
- package/template/packages/modules/package.json +1 -1
- package/template/packages/modules/src/admin-ui-boundary.test.ts +33 -0
- package/template/packages/modules/src/composition.ts +11 -28
- package/template/packages/modules/src/manifest.ts +2 -0
- package/template/packages/modules/src/menu-composition.ts +24 -0
- package/template/packages/modules/src/system-admin-routes.ts +2 -2
- package/template/packages/modules/src/system-admin-ui-catalog.ts +6 -1
- package/template/packages/modules/src/system-ai-api-bindings.ts +0 -16
- package/template/packages/modules/src/system-api-control-plane.ts +9 -1
- package/template/packages/modules/src/system-api-permissions.ts +7 -0
- package/template/packages/modules/src/system-api-watermark.ts +21 -0
- package/template/packages/modules/src/system-audit-actions.ts +3 -1
- package/template/packages/modules/src/system-features.ts +21 -3
- package/template/packages/modules/src/system-media-api-bindings.ts +97 -0
- package/template/packages/modules/src/system-module-audit-selection.ts +2 -0
- package/template/packages/modules/src/system-module-catalog.test.ts +13 -6
- package/template/packages/modules/src/system-module-catalog.ts +6 -241
- package/template/packages/modules/src/system-module-definitions.ts +260 -0
- package/template/packages/modules/src/system-modules-access.test.ts +2 -0
- package/template/packages/modules/src/system-modules.test.ts +7 -1
- package/template/packages/modules/src/system-modules.ts +25 -13
- package/template/packages/modules/src/system-permissions.ts +12 -0
- package/template/packages/server-sdk/package.json +1 -1
- package/template/packages/shared/index.ts +3 -0
- package/template/packages/shared/package.json +1 -1
- package/template/packages/shared/src/environment-assessment.ts +68 -0
- package/template/packages/shared/src/environment-policy.ts +205 -0
- package/template/packages/shared/src/license-console-capabilities.ts +7 -0
- package/template/packages/shared/src/license.ts +1 -0
- package/template/packages/shared/src/module-configuration.ts +2 -1
- package/template/scripts/operations/collect-device-info.py +230 -0
- package/template/apps/server/src/agent/module-ai-port-factory.ts +0 -41
- /package/template/apps/server/src/{agent → ai-runtime-security}/execution-grants.ts +0 -0
- /package/template/apps/server/src/{agent → ai-runtime-security}/idempotency-key.test.ts +0 -0
- /package/template/apps/server/src/{agent → ai-runtime-security}/idempotency-key.ts +0 -0
- /package/template/apps/server/src/{agent → ai-runtime-security}/provider-input-governance.test.ts +0 -0
- /package/template/apps/server/src/{agent → ai-runtime-security}/provider-input-governance.ts +0 -0
- /package/template/apps/server/src/{agent → media-runtime}/required-environment.ts +0 -0
- /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("
|
|
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
|
-
"
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
+
}
|
|
@@ -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 (
|
|
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:
|
|
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
|
-
|
|
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(
|
|
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 "../
|
|
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 "../
|
|
12
|
-
import { scopedRuntimeIdempotencyKey } from "../
|
|
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";
|