create-windy 0.2.17 → 0.2.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/dist/cli.js +68 -18
- package/package.json +1 -1
- package/template/.windy-template.json +2 -2
- package/template/AGENTS.md +11 -1
- package/template/README.md +8 -5
- package/template/apps/server/Dockerfile +3 -3
- package/template/apps/server/src/index.ts +8 -14
- package/template/apps/server/src/settings/drizzle-watermark-repository.integration.test.ts +57 -0
- package/template/apps/server/src/settings/drizzle-watermark-repository.ts +62 -0
- package/template/apps/server/src/settings/runtime.ts +30 -3
- package/template/apps/server/src/settings/watermark-repository.ts +32 -0
- package/template/apps/server/src/settings/watermark-routes.test.ts +203 -0
- package/template/apps/server/src/settings/watermark-routes.ts +134 -0
- package/template/apps/web/runtime-server.test.ts +4 -4
- package/template/apps/web/src/composables/useWatermarkSettings.ts +75 -33
- package/template/apps/web/src/composables/useWatermarkSettings.webtest.ts +86 -0
- package/template/apps/web/src/composables/watermark-settings.ts +9 -35
- package/template/apps/web/src/composables/watermark-settings.webtest.ts +8 -32
- package/template/apps/web/src/layout/GlobalWatermark.layering.webtest.ts +81 -0
- package/template/apps/web/src/layout/GlobalWatermark.vue +34 -31
- package/template/apps/web/src/layout/GlobalWatermark.webtest.ts +58 -7
- package/template/apps/web/src/pages/settings/GlobalSettingsPage.vue +1 -1
- package/template/apps/web/src/pages/settings/WatermarkSettingsSection.vue +40 -18
- package/template/apps/web/src/services/platform-settings-api.ts +25 -2
- package/template/apps/web/vite.config.ts +3 -1
- package/template/docker-compose.yml +19 -6
- package/template/docs/platform/platform-shell-settings.md +24 -3
- package/template/docs/platform/web-system-crud.md +2 -2
- package/template/drizzle.config.ts +1 -1
- package/template/package.json +4 -1
- package/template/packages/database/drizzle/0031_panoramic_typhoid_mary.sql +12 -0
- package/template/packages/database/drizzle/meta/0031_snapshot.json +7387 -0
- package/template/packages/database/drizzle/meta/_journal.json +8 -1
- package/template/packages/database/src/schema/platform-settings.ts +16 -1
- package/template/packages/modules/src/system-api-permissions.ts +4 -0
- package/template/packages/modules/src/system-api-watermark.ts +26 -0
- package/template/packages/modules/src/system-features.ts +1 -1
- package/template/packages/modules/src/system-watermark-policy.test.ts +31 -0
- package/template/packages/shared/src/platform-settings.ts +13 -0
package/README.md
CHANGED
|
@@ -110,9 +110,10 @@ create-windy 会生成仅供本机使用、被 Git 忽略且权限为 `0600` 的
|
|
|
110
110
|
直接打印在终端。首次登录必须修改密码,新密码需要 12-128 个字符,且不能与当前密码
|
|
111
111
|
相同。
|
|
112
112
|
|
|
113
|
-
`bun run dev`
|
|
114
|
-
与 Web
|
|
115
|
-
|
|
113
|
+
`bun run dev` 会复用已有 Docker 镜像,等待 PostgreSQL、执行 migration,再启动支持
|
|
114
|
+
热更新的 Server 与 Web;首次缺少镜像时才会构建。依赖、Dockerfile 或 `bun.lock`
|
|
115
|
+
变化后运行 `bun run dev:build`。首次运行前需安装并启动 Docker 与 Docker Compose。
|
|
116
|
+
环境变量只负责初始化全新数据库,数据库已有管理员后,重启服务不会重新覆盖其密码。
|
|
116
117
|
|
|
117
118
|
只调试单个应用时可分别使用 `bun run dev:web` 和 `bun run dev:server`。`dev:web` 不会
|
|
118
119
|
隐式启动后端;`dev:server` 也不会自动启动 PostgreSQL 或执行 migration。
|
package/dist/cli.js
CHANGED
|
@@ -412,13 +412,20 @@ var platformModules = [
|
|
|
412
412
|
"apps/web/src/pages/settings",
|
|
413
413
|
"apps/web/src/layout/GlobalWatermark.vue",
|
|
414
414
|
"apps/web/src/layout/GlobalWatermark.webtest.ts",
|
|
415
|
+
"apps/web/src/layout/GlobalWatermark.layering.webtest.ts",
|
|
415
416
|
"apps/web/src/services/platform-settings-api.ts",
|
|
416
417
|
"apps/web/src/composables/useWatermarkSettings.ts",
|
|
418
|
+
"apps/web/src/composables/useWatermarkSettings.webtest.ts",
|
|
417
419
|
"apps/web/src/composables/watermark-settings.ts",
|
|
418
|
-
"apps/web/src/composables/watermark-settings.webtest.ts"
|
|
420
|
+
"apps/web/src/composables/watermark-settings.webtest.ts",
|
|
421
|
+
"packages/modules/src/system-api-watermark.ts",
|
|
422
|
+
"packages/modules/src/system-watermark-policy.test.ts"
|
|
419
423
|
],
|
|
420
424
|
documentation: ["docs/platform/platform-shell-settings.md"],
|
|
421
|
-
databaseTables: [
|
|
425
|
+
databaseTables: [
|
|
426
|
+
"platform_branding_settings",
|
|
427
|
+
"platform_watermark_policies"
|
|
428
|
+
]
|
|
422
429
|
},
|
|
423
430
|
{
|
|
424
431
|
...module2("system.configuration", "配置版本", "类型化配置、应用和回滚。", "recommended", ["system"]),
|
|
@@ -1305,7 +1312,7 @@ var starterCompose = `services:
|
|
|
1305
1312
|
POSTGRES_USER: windy
|
|
1306
1313
|
POSTGRES_PASSWORD: \${POSTGRES_PASSWORD:?请在 .env 中设置 POSTGRES_PASSWORD}
|
|
1307
1314
|
ports:
|
|
1308
|
-
- "
|
|
1315
|
+
- "15432:5432"
|
|
1309
1316
|
volumes:
|
|
1310
1317
|
- postgres-data:/var/lib/postgresql/data
|
|
1311
1318
|
healthcheck:
|
|
@@ -1334,14 +1341,15 @@ var starterCompose = `services:
|
|
|
1334
1341
|
command: ["bun", "run", "--cwd", "apps/server", "dev"]
|
|
1335
1342
|
environment:
|
|
1336
1343
|
NODE_ENV: development
|
|
1337
|
-
PORT:
|
|
1344
|
+
PORT: 9747
|
|
1338
1345
|
DATABASE_URL: postgresql://windy:\${POSTGRES_PASSWORD}@postgres:5432/windy
|
|
1339
1346
|
WINDY_BOOTSTRAP_ADMIN_PASSWORD: \${WINDY_BOOTSTRAP_ADMIN_PASSWORD:?请在 .env 中设置管理员初始密码}
|
|
1340
1347
|
WINDY_AUDIT_QUEUE_FILE: /app/data/audit/recovery-queue.json
|
|
1341
1348
|
WINDY_CONTAINERIZED: "1"
|
|
1349
|
+
BUN_CONFIG_NO_CLEAR_TERMINAL_ON_RELOAD: "true"
|
|
1342
1350
|
WINDY_SYSTEM_USAGE_PATHS: /app/data/audit
|
|
1343
1351
|
ports:
|
|
1344
|
-
- "
|
|
1352
|
+
- "9747:9747"
|
|
1345
1353
|
volumes:
|
|
1346
1354
|
- ./apps/server:/app/apps/server
|
|
1347
1355
|
- ./packages:/app/packages
|
|
@@ -1357,14 +1365,26 @@ var starterCompose = `services:
|
|
|
1357
1365
|
command: ["bun", "run", "--cwd", "apps/web", "dev"]
|
|
1358
1366
|
environment:
|
|
1359
1367
|
NODE_ENV: development
|
|
1360
|
-
WINDY_DEV_PORT:
|
|
1361
|
-
|
|
1368
|
+
WINDY_DEV_PORT: 18746
|
|
1369
|
+
WINDY_DEV_USE_POLLING: "1"
|
|
1370
|
+
API_UPSTREAM: http://server:9747
|
|
1362
1371
|
VITE_DEV_HOST: 0.0.0.0
|
|
1363
1372
|
ports:
|
|
1364
|
-
- "
|
|
1373
|
+
- "18746:18746"
|
|
1365
1374
|
volumes:
|
|
1366
1375
|
- ./apps/web:/app/apps/web
|
|
1367
1376
|
- ./packages:/app/packages
|
|
1377
|
+
healthcheck:
|
|
1378
|
+
test:
|
|
1379
|
+
[
|
|
1380
|
+
"CMD",
|
|
1381
|
+
"bun",
|
|
1382
|
+
"-e",
|
|
1383
|
+
"const r=await fetch('http://127.0.0.1:18746/-/windy-dev');process.exit(r.ok?0:1)",
|
|
1384
|
+
]
|
|
1385
|
+
interval: 5s
|
|
1386
|
+
timeout: 3s
|
|
1387
|
+
retries: 6
|
|
1368
1388
|
depends_on:
|
|
1369
1389
|
- server
|
|
1370
1390
|
|
|
@@ -1435,15 +1455,17 @@ create-windy 已创建仅供本机使用且被 Git 忽略的 \`.env\`,其中
|
|
|
1435
1455
|
文件权限为 \`0600\`。\`bun run dev\` 会通过 Docker Compose 等待 PostgreSQL、执行
|
|
1436
1456
|
Drizzle migration,再启动 Server 与 Web;不要跳过 migration 后直接把 Server 连接到
|
|
1437
1457
|
空数据库。首次启动需要本机已经安装并启动 Docker 与 Docker Compose,并可能需要下载
|
|
1438
|
-
|
|
1458
|
+
基础镜像。后续 \`bun run dev\` 会复用已有镜像,不会重复构建;依赖、Dockerfile 或
|
|
1459
|
+
\`bun.lock\` 变化后运行 \`bun run dev:build\`。
|
|
1439
1460
|
|
|
1440
1461
|
| 服务 | 地址 |
|
|
1441
1462
|
| ---- | ---- |
|
|
1442
|
-
| Web | http://localhost:
|
|
1443
|
-
| Server 健康检查 | http://localhost:
|
|
1444
|
-
| PostgreSQL | localhost:
|
|
1463
|
+
| Web | http://localhost:18746 |
|
|
1464
|
+
| Server 健康检查 | http://localhost:9747/api/health |
|
|
1465
|
+
| PostgreSQL | localhost:15432 |
|
|
1445
1466
|
|
|
1446
|
-
Docker Web 固定使用 \`
|
|
1467
|
+
Docker Web 固定使用 \`18746\`,Web 与 Server 都挂载宿主机源码并保持热更新。只启动
|
|
1468
|
+
单个应用时使用:
|
|
1447
1469
|
|
|
1448
1470
|
\`\`\`bash
|
|
1449
1471
|
bun run dev:web
|
|
@@ -1502,6 +1524,7 @@ ${moduleRows}
|
|
|
1502
1524
|
|
|
1503
1525
|
\`\`\`bash
|
|
1504
1526
|
bun run dev
|
|
1527
|
+
bun run dev:build
|
|
1505
1528
|
bun run dev:web
|
|
1506
1529
|
bun run dev:server
|
|
1507
1530
|
bun run typecheck
|
|
@@ -1574,7 +1597,8 @@ function createStarterPackage(source, projectName, includeOxc = true, projectLic
|
|
|
1574
1597
|
...includeOxc ? {
|
|
1575
1598
|
prepare: "git rev-parse --git-dir >/dev/null 2>&1 || exit 0; husky"
|
|
1576
1599
|
} : {},
|
|
1577
|
-
dev: "docker compose up
|
|
1600
|
+
dev: "docker compose up",
|
|
1601
|
+
"dev:build": "docker compose up --build",
|
|
1578
1602
|
...generatorVersion ? { windy: "create-windy" } : {},
|
|
1579
1603
|
"dev:web": "bun run --cwd apps/web dev",
|
|
1580
1604
|
"dev:server": "bun run --cwd apps/server dev",
|
|
@@ -3788,7 +3812,7 @@ function sentenceWithoutStop(value) {
|
|
|
3788
3812
|
|
|
3789
3813
|
// src/update/cli.ts
|
|
3790
3814
|
import { rm as rm8 } from "node:fs/promises";
|
|
3791
|
-
import { resolve as
|
|
3815
|
+
import { resolve as resolve5 } from "node:path";
|
|
3792
3816
|
|
|
3793
3817
|
// src/update/doctor.ts
|
|
3794
3818
|
import { readFile as readFile11 } from "node:fs/promises";
|
|
@@ -3826,9 +3850,10 @@ import {
|
|
|
3826
3850
|
mkdir as mkdir5,
|
|
3827
3851
|
readFile as readFile10,
|
|
3828
3852
|
rm as rm5,
|
|
3853
|
+
rmdir,
|
|
3829
3854
|
writeFile as writeFile9
|
|
3830
3855
|
} from "node:fs/promises";
|
|
3831
|
-
import { dirname as dirname3, join as join10 } from "node:path";
|
|
3856
|
+
import { dirname as dirname3, join as join10, resolve as resolve4, sep as sep3 } from "node:path";
|
|
3832
3857
|
var updateStatePath = ".windy/update-state.json";
|
|
3833
3858
|
async function applyTransaction(plan) {
|
|
3834
3859
|
if (plan.conflicts.length) {
|
|
@@ -3866,7 +3891,11 @@ async function applyTransaction(plan) {
|
|
|
3866
3891
|
for (const file of changed) {
|
|
3867
3892
|
const target = join10(plan.projectDirectory, file.path);
|
|
3868
3893
|
if (file.action === "delete") {
|
|
3894
|
+
const existed = await exists(target);
|
|
3869
3895
|
await rm5(target, { force: true });
|
|
3896
|
+
if (existed) {
|
|
3897
|
+
await removeEmptyDeletedFileParents(target, plan.projectDirectory);
|
|
3898
|
+
}
|
|
3870
3899
|
continue;
|
|
3871
3900
|
}
|
|
3872
3901
|
if (!file.content)
|
|
@@ -3929,6 +3958,26 @@ async function exists(path2) {
|
|
|
3929
3958
|
return false;
|
|
3930
3959
|
}
|
|
3931
3960
|
}
|
|
3961
|
+
async function removeEmptyDeletedFileParents(deletedFile, projectDirectory) {
|
|
3962
|
+
const projectRoot = resolve4(projectDirectory);
|
|
3963
|
+
let directory = dirname3(resolve4(deletedFile));
|
|
3964
|
+
while (directory !== projectRoot && directory.startsWith(`${projectRoot}${sep3}`)) {
|
|
3965
|
+
try {
|
|
3966
|
+
await rmdir(directory);
|
|
3967
|
+
} catch (error) {
|
|
3968
|
+
if (isDirectoryNotRemovable(error))
|
|
3969
|
+
return;
|
|
3970
|
+
throw error;
|
|
3971
|
+
}
|
|
3972
|
+
directory = dirname3(directory);
|
|
3973
|
+
}
|
|
3974
|
+
}
|
|
3975
|
+
function isDirectoryNotRemovable(error) {
|
|
3976
|
+
if (!(error instanceof Error) || !("code" in error))
|
|
3977
|
+
return false;
|
|
3978
|
+
const code = error.code;
|
|
3979
|
+
return code === "ENOENT" || code === "ENOTEMPTY" || code === "EEXIST";
|
|
3980
|
+
}
|
|
3932
3981
|
function assertSafePath(path2) {
|
|
3933
3982
|
if (!path2 || path2.startsWith("/") || path2.split("/").includes("..")) {
|
|
3934
3983
|
throw new Error(`更新计划包含不安全路径:${path2}`);
|
|
@@ -4013,7 +4062,8 @@ var recipes = [
|
|
|
4013
4062
|
{ id: "starter-0.2.13-to-0.2.14", from: "0.2.13", to: "0.2.14" },
|
|
4014
4063
|
{ id: "starter-0.2.14-to-0.2.15", from: "0.2.14", to: "0.2.15" },
|
|
4015
4064
|
{ id: "starter-0.2.15-to-0.2.16", from: "0.2.15", to: "0.2.16" },
|
|
4016
|
-
{ id: "starter-0.2.16-to-0.2.17", from: "0.2.16", to: "0.2.17" }
|
|
4065
|
+
{ id: "starter-0.2.16-to-0.2.17", from: "0.2.16", to: "0.2.17" },
|
|
4066
|
+
{ id: "starter-0.2.17-to-0.2.18", from: "0.2.17", to: "0.2.18" }
|
|
4017
4067
|
];
|
|
4018
4068
|
function resolveRecipeChain(sourceVersion, targetVersion) {
|
|
4019
4069
|
if (sourceVersion === targetVersion)
|
|
@@ -4482,7 +4532,7 @@ async function runLifecycleCommand(args) {
|
|
|
4482
4532
|
const command = args[0];
|
|
4483
4533
|
if (command !== "doctor" && command !== "update")
|
|
4484
4534
|
return false;
|
|
4485
|
-
const projectDirectory =
|
|
4535
|
+
const projectDirectory = resolve5(readValue2(args, "--cwd") || process.cwd());
|
|
4486
4536
|
if (command === "doctor") {
|
|
4487
4537
|
const report = await diagnoseProject(projectDirectory, args.includes("--repair"));
|
|
4488
4538
|
printDoctor(report);
|
package/package.json
CHANGED
package/template/AGENTS.md
CHANGED
|
@@ -31,9 +31,19 @@
|
|
|
31
31
|
- 这是 Starter 的初始配置;项目所有者可按团队需要保留、替换或扩展。
|
|
32
32
|
- 实际可用命令和提交检查以当前项目的 `package.json` 与 Git hooks 为准。
|
|
33
33
|
|
|
34
|
+
## Windy 升级边界
|
|
35
|
+
|
|
36
|
+
- 业务需求优先通过自有模块和既有扩展点实现,尽量不要直接修改 Windy 提供的平台核心、
|
|
37
|
+
Composition Root、共享脚手架和根工具链配置,以减少后续升级冲突。
|
|
38
|
+
- 修改前先查看 `.windy/files.json` 的 ownership;优先扩展 `project-owned` 路径,
|
|
39
|
+
对 `windy-managed` 路径仅在没有可用扩展点时做最小改动。
|
|
40
|
+
- 确需修改 Windy 受管代码时,使用独立小提交记录原因和影响;升级前先运行
|
|
41
|
+
`bun run windy update --dry-run`,根据三方合并计划处理冲突。
|
|
42
|
+
|
|
34
43
|
## 常用入口
|
|
35
44
|
|
|
36
|
-
- `bun run dev
|
|
45
|
+
- `bun run dev`:复用 Docker 镜像启动 PostgreSQL、自动 migration、Server 与支持热更新的 Web。
|
|
46
|
+
- `bun run dev:build`:依赖、Dockerfile 或 lockfile 变化后重建并启动开发环境。
|
|
37
47
|
- `bun run dev:web`:启动 Web 开发服务。
|
|
38
48
|
- `bun run dev:server`:启动 Server 开发服务。
|
|
39
49
|
- `bun run lint`、`bun run typecheck`、`bun run test`、`bun run build`:执行基础验证;未选择 Oxc 时没有 lint 脚本。
|
package/template/README.md
CHANGED
|
@@ -30,15 +30,17 @@ create-windy 已创建仅供本机使用且被 Git 忽略的 `.env`,其中两
|
|
|
30
30
|
文件权限为 `0600`。`bun run dev` 会通过 Docker Compose 等待 PostgreSQL、执行
|
|
31
31
|
Drizzle migration,再启动 Server 与 Web;不要跳过 migration 后直接把 Server 连接到
|
|
32
32
|
空数据库。首次启动需要本机已经安装并启动 Docker 与 Docker Compose,并可能需要下载
|
|
33
|
-
|
|
33
|
+
基础镜像。后续 `bun run dev` 会复用已有镜像,不会重复构建;依赖、Dockerfile 或
|
|
34
|
+
`bun.lock` 变化后运行 `bun run dev:build`。
|
|
34
35
|
|
|
35
36
|
| 服务 | 地址 |
|
|
36
37
|
| ---- | ---- |
|
|
37
|
-
| Web | http://localhost:
|
|
38
|
-
| Server 健康检查 | http://localhost:
|
|
39
|
-
| PostgreSQL | localhost:
|
|
38
|
+
| Web | http://localhost:18746 |
|
|
39
|
+
| Server 健康检查 | http://localhost:9747/api/health |
|
|
40
|
+
| PostgreSQL | localhost:15432 |
|
|
40
41
|
|
|
41
|
-
Docker Web 固定使用 `
|
|
42
|
+
Docker Web 固定使用 `18746`,Web 与 Server 都挂载宿主机源码并保持热更新。只启动
|
|
43
|
+
单个应用时使用:
|
|
42
44
|
|
|
43
45
|
```bash
|
|
44
46
|
bun run dev:web
|
|
@@ -112,6 +114,7 @@ create-windy 在本机 `.env` 中随机生成的 `WINDY_BOOTSTRAP_ADMIN_PASSWORD
|
|
|
112
114
|
|
|
113
115
|
```bash
|
|
114
116
|
bun run dev
|
|
117
|
+
bun run dev:build
|
|
115
118
|
bun run dev:web
|
|
116
119
|
bun run dev:server
|
|
117
120
|
bun run typecheck
|
|
@@ -29,9 +29,9 @@ COPY packages packages
|
|
|
29
29
|
COPY --chmod=0555 deploy/production/server-entrypoint.sh /usr/local/bin/windy-server-entrypoint
|
|
30
30
|
RUN mkdir -p /app/data/audit /app/data/license && chown -R bun:bun /app/data
|
|
31
31
|
ENV NODE_ENV=production
|
|
32
|
-
ENV PORT=
|
|
32
|
+
ENV PORT=9747
|
|
33
33
|
ENV WINDY_CONTAINERIZED=1
|
|
34
|
-
EXPOSE
|
|
34
|
+
EXPOSE 9747
|
|
35
35
|
USER bun
|
|
36
|
-
HEALTHCHECK --interval=15s --timeout=3s --start-period=15s --retries=4 CMD ["bun", "-e", "const r=await fetch('http://127.0.0.1:
|
|
36
|
+
HEALTHCHECK --interval=15s --timeout=3s --start-period=15s --retries=4 CMD ["bun", "-e", "const r=await fetch('http://127.0.0.1:9747/api/ready');process.exit(r.ok?0:1)"]
|
|
37
37
|
CMD ["bun", "run", "--cwd", "apps/server", "start"]
|
|
@@ -53,8 +53,7 @@ import { registerSchedulerRoutes } from "./scheduler/routes.js";
|
|
|
53
53
|
import { SchedulerWorker } from "./scheduler/worker.js";
|
|
54
54
|
// @windy-module system.scheduler end
|
|
55
55
|
// @windy-module system.settings begin
|
|
56
|
-
import {
|
|
57
|
-
import { createBrandingSettingsRepository } from "./settings/runtime.js";
|
|
56
|
+
import { createPlatformSettingsRuntime } from "./settings/runtime.js";
|
|
58
57
|
// @windy-module system.settings end
|
|
59
58
|
import { registerProfileRoutes } from "./profile/routes.js";
|
|
60
59
|
// @windy-module system.notification begin
|
|
@@ -191,10 +190,10 @@ const storageRuntime = createStorageRuntime(
|
|
|
191
190
|
);
|
|
192
191
|
// @windy-module system.storage end
|
|
193
192
|
// @windy-module system.settings begin
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
);
|
|
193
|
+
const platformSettings = createPlatformSettingsRuntime(persistence?.db, {
|
|
194
|
+
softwareName: platformConfig.name,
|
|
195
|
+
logoUrl: platformConfig.logoUrl,
|
|
196
|
+
});
|
|
198
197
|
// @windy-module system.settings end
|
|
199
198
|
// @windy-module work-order begin
|
|
200
199
|
const exampleModuleRuntime = createExampleModuleRuntime({
|
|
@@ -276,12 +275,7 @@ registerPlatformLicenseRoutes(app, platformLicenseRuntime);
|
|
|
276
275
|
// @windy-module system.license end
|
|
277
276
|
|
|
278
277
|
// @windy-module system.settings begin
|
|
279
|
-
|
|
280
|
-
app,
|
|
281
|
-
brandingSettingsRepository,
|
|
282
|
-
runtime,
|
|
283
|
-
foundationSnapshot.features,
|
|
284
|
-
);
|
|
278
|
+
platformSettings.register(app, runtime, foundationSnapshot.features);
|
|
285
279
|
// @windy-module system.settings end
|
|
286
280
|
// @windy-module system.notification begin
|
|
287
281
|
registerNotificationRoutes(
|
|
@@ -304,7 +298,7 @@ registerStorageRoutes(
|
|
|
304
298
|
uploads: storageRuntime.uploads,
|
|
305
299
|
files: storageRuntime.files,
|
|
306
300
|
isPublicBrandingFile: async (fileId) =>
|
|
307
|
-
(await
|
|
301
|
+
(await platformSettings.branding.get()).logoUrl ===
|
|
308
302
|
`/api/platform/files/${fileId}`,
|
|
309
303
|
},
|
|
310
304
|
runtime,
|
|
@@ -387,7 +381,7 @@ validateServerModuleComposition({
|
|
|
387
381
|
// @windy-module system.search end
|
|
388
382
|
});
|
|
389
383
|
|
|
390
|
-
const port = process.env.PORT ? Number(process.env.PORT) :
|
|
384
|
+
const port = process.env.PORT ? Number(process.env.PORT) : 9_747;
|
|
391
385
|
app.listen(port);
|
|
392
386
|
// @windy-module system.scheduler begin
|
|
393
387
|
schedulerWorker.start();
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { DEFAULT_PLATFORM_WATERMARK_POLICY } from "@southwind-ai/shared";
|
|
4
|
+
import { drizzle } from "drizzle-orm/node-postgres";
|
|
5
|
+
import { migrate } from "drizzle-orm/node-postgres/migrator";
|
|
6
|
+
import { Pool } from "pg";
|
|
7
|
+
import { DrizzleWatermarkPolicyRepository } from "./drizzle-watermark-repository.js";
|
|
8
|
+
|
|
9
|
+
const databaseUrl = process.env.SETTINGS_TEST_DATABASE_URL;
|
|
10
|
+
const describePostgres = databaseUrl ? describe : describe.skip;
|
|
11
|
+
|
|
12
|
+
describePostgres("PostgreSQL watermark policy repository", () => {
|
|
13
|
+
let pool: Pool;
|
|
14
|
+
|
|
15
|
+
beforeAll(async () => {
|
|
16
|
+
pool = new Pool({ connectionString: databaseUrl });
|
|
17
|
+
const db = drizzle(pool);
|
|
18
|
+
await migrate(db, {
|
|
19
|
+
migrationsFolder: resolve(
|
|
20
|
+
import.meta.dir,
|
|
21
|
+
"../../../../packages/database/drizzle",
|
|
22
|
+
),
|
|
23
|
+
migrationsSchema: "public",
|
|
24
|
+
migrationsTable: "__settings_test_migrations",
|
|
25
|
+
});
|
|
26
|
+
await pool.query("truncate table platform_watermark_policies");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
afterAll(async () => {
|
|
30
|
+
await pool?.end();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("保存后可由重建的 Repository 读取同一全局策略", async () => {
|
|
34
|
+
const repository = new DrizzleWatermarkPolicyRepository(
|
|
35
|
+
drizzle(pool),
|
|
36
|
+
DEFAULT_PLATFORM_WATERMARK_POLICY,
|
|
37
|
+
);
|
|
38
|
+
await repository.update(
|
|
39
|
+
{
|
|
40
|
+
enabled: true,
|
|
41
|
+
businessPagesEnabled: true,
|
|
42
|
+
customContent: "内部资料",
|
|
43
|
+
},
|
|
44
|
+
"user_settings",
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const restarted = new DrizzleWatermarkPolicyRepository(
|
|
48
|
+
drizzle(pool),
|
|
49
|
+
DEFAULT_PLATFORM_WATERMARK_POLICY,
|
|
50
|
+
);
|
|
51
|
+
expect(await restarted.get()).toEqual({
|
|
52
|
+
enabled: true,
|
|
53
|
+
businessPagesEnabled: true,
|
|
54
|
+
customContent: "内部资料",
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { platformWatermarkPolicies } from "@southwind-ai/database";
|
|
2
|
+
import type { PlatformWatermarkPolicy } from "@southwind-ai/shared";
|
|
3
|
+
import { eq } from "drizzle-orm";
|
|
4
|
+
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
5
|
+
import type { WatermarkPolicyRepository } from "./watermark-repository.js";
|
|
6
|
+
|
|
7
|
+
const PLATFORM_WATERMARK_ID = "platform";
|
|
8
|
+
|
|
9
|
+
export class DrizzleWatermarkPolicyRepository implements WatermarkPolicyRepository {
|
|
10
|
+
constructor(
|
|
11
|
+
private readonly db: NodePgDatabase,
|
|
12
|
+
private readonly defaults: PlatformWatermarkPolicy,
|
|
13
|
+
) {}
|
|
14
|
+
|
|
15
|
+
async get(): Promise<PlatformWatermarkPolicy> {
|
|
16
|
+
const [row] = await this.db
|
|
17
|
+
.select()
|
|
18
|
+
.from(platformWatermarkPolicies)
|
|
19
|
+
.where(eq(platformWatermarkPolicies.id, PLATFORM_WATERMARK_ID))
|
|
20
|
+
.limit(1);
|
|
21
|
+
return row ? toPolicy(row) : { ...this.defaults };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async update(
|
|
25
|
+
policy: PlatformWatermarkPolicy,
|
|
26
|
+
actorId: string,
|
|
27
|
+
): Promise<PlatformWatermarkPolicy> {
|
|
28
|
+
const [row] = await this.db
|
|
29
|
+
.insert(platformWatermarkPolicies)
|
|
30
|
+
.values({
|
|
31
|
+
id: PLATFORM_WATERMARK_ID,
|
|
32
|
+
...policy,
|
|
33
|
+
createdBy: actorId,
|
|
34
|
+
updatedBy: actorId,
|
|
35
|
+
})
|
|
36
|
+
.onConflictDoUpdate({
|
|
37
|
+
target: platformWatermarkPolicies.id,
|
|
38
|
+
set: {
|
|
39
|
+
enabled: policy.enabled,
|
|
40
|
+
businessPagesEnabled: policy.businessPagesEnabled,
|
|
41
|
+
customContent: policy.customContent,
|
|
42
|
+
updatedAt: new Date(),
|
|
43
|
+
updatedBy: actorId,
|
|
44
|
+
},
|
|
45
|
+
})
|
|
46
|
+
.returning();
|
|
47
|
+
if (!row) throw new Error("水印策略保存失败");
|
|
48
|
+
return toPolicy(row);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function toPolicy(row: {
|
|
53
|
+
enabled: boolean;
|
|
54
|
+
businessPagesEnabled: boolean;
|
|
55
|
+
customContent: string;
|
|
56
|
+
}): PlatformWatermarkPolicy {
|
|
57
|
+
return {
|
|
58
|
+
enabled: row.enabled,
|
|
59
|
+
businessPagesEnabled: row.businessPagesEnabled,
|
|
60
|
+
customContent: row.customContent,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -1,13 +1,40 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
FeatureFlagDefinition,
|
|
3
|
+
PlatformBrandingSettings,
|
|
4
|
+
} from "@southwind-ai/shared";
|
|
5
|
+
import { DEFAULT_PLATFORM_WATERMARK_POLICY } from "@southwind-ai/shared";
|
|
2
6
|
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
7
|
+
import type { createServerRuntime } from "../runtime.js";
|
|
8
|
+
import type { RouteApp } from "../system/route-types.js";
|
|
3
9
|
import { DrizzleBrandingSettingsRepository } from "./drizzle-repository.js";
|
|
10
|
+
import { DrizzleWatermarkPolicyRepository } from "./drizzle-watermark-repository.js";
|
|
4
11
|
import { InMemoryBrandingSettingsRepository } from "./repository.js";
|
|
12
|
+
import { registerBrandingSettingsRoutes } from "./routes.js";
|
|
13
|
+
import { InMemoryWatermarkPolicyRepository } from "./watermark-repository.js";
|
|
14
|
+
import { registerWatermarkPolicyRoutes } from "./watermark-routes.js";
|
|
5
15
|
|
|
6
|
-
export function
|
|
16
|
+
export function createPlatformSettingsRuntime(
|
|
7
17
|
db: NodePgDatabase | undefined,
|
|
8
18
|
defaults: PlatformBrandingSettings,
|
|
9
19
|
) {
|
|
10
|
-
|
|
20
|
+
const branding = db
|
|
11
21
|
? new DrizzleBrandingSettingsRepository(db, defaults)
|
|
12
22
|
: new InMemoryBrandingSettingsRepository(defaults);
|
|
23
|
+
const watermark = db
|
|
24
|
+
? new DrizzleWatermarkPolicyRepository(
|
|
25
|
+
db,
|
|
26
|
+
DEFAULT_PLATFORM_WATERMARK_POLICY,
|
|
27
|
+
)
|
|
28
|
+
: new InMemoryWatermarkPolicyRepository();
|
|
29
|
+
return {
|
|
30
|
+
branding,
|
|
31
|
+
register(
|
|
32
|
+
app: RouteApp,
|
|
33
|
+
server: ReturnType<typeof createServerRuntime>,
|
|
34
|
+
features: FeatureFlagDefinition[],
|
|
35
|
+
) {
|
|
36
|
+
registerBrandingSettingsRoutes(app, branding, server, features);
|
|
37
|
+
registerWatermarkPolicyRoutes(app, watermark, server, features);
|
|
38
|
+
},
|
|
39
|
+
};
|
|
13
40
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_PLATFORM_WATERMARK_POLICY,
|
|
3
|
+
type PlatformWatermarkPolicy,
|
|
4
|
+
} from "@southwind-ai/shared";
|
|
5
|
+
|
|
6
|
+
export interface WatermarkPolicyRepository {
|
|
7
|
+
get(): Promise<PlatformWatermarkPolicy>;
|
|
8
|
+
update(
|
|
9
|
+
policy: PlatformWatermarkPolicy,
|
|
10
|
+
actorId: string,
|
|
11
|
+
): Promise<PlatformWatermarkPolicy>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class InMemoryWatermarkPolicyRepository implements WatermarkPolicyRepository {
|
|
15
|
+
private policy: PlatformWatermarkPolicy;
|
|
16
|
+
|
|
17
|
+
constructor(defaults = DEFAULT_PLATFORM_WATERMARK_POLICY) {
|
|
18
|
+
this.policy = { ...defaults };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async get(): Promise<PlatformWatermarkPolicy> {
|
|
22
|
+
return { ...this.policy };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async update(
|
|
26
|
+
policy: PlatformWatermarkPolicy,
|
|
27
|
+
_actorId: string,
|
|
28
|
+
): Promise<PlatformWatermarkPolicy> {
|
|
29
|
+
this.policy = { ...policy };
|
|
30
|
+
return this.get();
|
|
31
|
+
}
|
|
32
|
+
}
|