create-windy 0.2.29 → 0.2.31

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 (45) hide show
  1. package/dist/cli.js +385 -9
  2. package/package.json +1 -1
  3. package/template/.windy-template.json +2 -2
  4. package/template/README.md +3 -0
  5. package/template/apps/agent-server/README.md +4 -2
  6. package/template/apps/agent-server/src/southwind_agent_server/config.py +48 -0
  7. package/template/apps/agent-server/src/southwind_agent_server/contracts.py +22 -2
  8. package/template/apps/agent-server/src/southwind_agent_server/engine.py +10 -1
  9. package/template/apps/agent-server/src/southwind_agent_server/openai_engine.py +20 -2
  10. package/template/apps/agent-server/src/southwind_agent_server/service.py +2 -0
  11. package/template/apps/agent-server/tests/test_api.py +28 -0
  12. package/template/apps/agent-server/tests/test_config.py +46 -0
  13. package/template/apps/agent-server/tests/test_openai_provider.py +61 -1
  14. package/template/apps/server/src/agent/remote-runtime.test.ts +14 -0
  15. package/template/apps/server/src/agent/remote-runtime.ts +2 -0
  16. package/template/apps/server/src/agent/run-contracts.ts +6 -0
  17. package/template/apps/server/src/agent/run-facade.ts +6 -0
  18. package/template/apps/server/src/agent/tool-host.test.ts +25 -0
  19. package/template/apps/server/src/agent/tool-host.ts +10 -1
  20. package/template/apps/server/src/work-order/drizzle-repository.ts +1 -2
  21. package/template/apps/web/src/router/shared-scaffold-neutrality.webtest.ts +7 -5
  22. package/template/docker-compose.yml +1 -0
  23. package/template/docs/architecture/ai-egress.md +80 -0
  24. package/template/docs/architecture/ai-runtime.md +13 -1
  25. package/template/docs/architecture/migration-bundle.md +151 -0
  26. package/template/docs/platform/agent-runtime.md +20 -1
  27. package/template/module-schema.lineages.json +8 -0
  28. package/template/packages/config/index.test.ts +20 -0
  29. package/template/packages/config/src/ai-openai-compatible.ts +58 -2
  30. package/template/packages/database/drizzle/0033_rich_george_stacy.sql +2 -0
  31. package/template/packages/database/drizzle/meta/0033_snapshot.json +7239 -0
  32. package/template/packages/database/drizzle/meta/_journal.json +7 -0
  33. package/template/packages/database/src/schema/index.ts +0 -3
  34. package/template/packages/example-work-order/drizzle/0000_bitter_the_santerians.sql +2 -0
  35. package/template/packages/example-work-order/drizzle/meta/0000_snapshot.json +179 -0
  36. package/template/packages/example-work-order/drizzle/meta/_journal.json +13 -0
  37. package/template/packages/example-work-order/drizzle.config.ts +8 -0
  38. package/template/packages/example-work-order/index.ts +1 -0
  39. package/template/packages/example-work-order/package.json +4 -0
  40. package/template/packages/{database/src/schema/work-orders.ts → example-work-order/src/drizzle-schema.ts} +16 -2
  41. package/template/packages/modules/index.ts +1 -0
  42. package/template/packages/modules/src/ai-network-policy.ts +50 -0
  43. package/template/packages/modules/src/ai-operation-composition.test.ts +50 -0
  44. package/template/packages/modules/src/ai-operation-composition.ts +2 -0
  45. package/template/packages/modules/src/manifest.ts +13 -1
package/dist/cli.js CHANGED
@@ -394,8 +394,9 @@ var optionalModuleManifests = [
394
394
  "apps/server/src/example-permissions.ts"
395
395
  ],
396
396
  documentation: ["docs/development/example-work-order.md"],
397
- schemaFiles: ["packages/database/src/schema/work-orders.ts"],
398
- databaseTables: ["work_orders"]
397
+ schemaFiles: ["packages/example-work-order/src/drizzle-schema.ts"],
398
+ databaseTables: ["work_orders"],
399
+ databaseLineageOwner: "module"
399
400
  }
400
401
  ];
401
402
  function module(name, label, description, dependencies) {
@@ -414,6 +415,7 @@ function module(name, label, description, dependencies) {
414
415
  documentation: [],
415
416
  schemaFiles: [],
416
417
  databaseTables: [],
418
+ databaseLineageOwner: "platform",
417
419
  transientDatabaseTables: [],
418
420
  databaseStatementTokens: [],
419
421
  uninstallPolicy: "retain-data"
@@ -628,6 +630,7 @@ function module2(name, label, description, selection, dependencies = []) {
628
630
  documentation: [],
629
631
  schemaFiles: [],
630
632
  databaseTables: [],
633
+ databaseLineageOwner: "platform",
631
634
  transientDatabaseTables: [],
632
635
  databaseStatementTokens: [],
633
636
  uninstallPolicy: "retain-data"
@@ -735,6 +738,7 @@ var helpText = `create-windy <项目名> [选项]
735
738
  create-windy update --check [--cwd <目录>]
736
739
  create-windy update --dry-run [--to <版本>] [--cwd <目录>]
737
740
  create-windy update [--to <版本>] [--cwd <目录>]
741
+ create-windy schema handoff [--config <JSON>] [--cwd <目录>]
738
742
 
739
743
  选项:
740
744
  --profile private-enterprise 单组织、单租户、私有部署 profile
@@ -1200,8 +1204,11 @@ var ignoredFiles = new Set([
1200
1204
  ".windy/update-state.json"
1201
1205
  ]);
1202
1206
  var projectOwnedRoots = [
1207
+ "module-schema.lineages.json",
1208
+ "module-schema",
1203
1209
  "apps/web/src/app",
1204
1210
  "apps/server/src/installed-business-modules.ts",
1211
+ "apps/server/src/domain-schema",
1205
1212
  "apps/server/src/work-order",
1206
1213
  "packages/example-work-order"
1207
1214
  ];
@@ -1320,10 +1327,13 @@ async function materializeSelectedModules(targetDirectory, selectedNames) {
1320
1327
  removedPackageNames.add(dependency.name);
1321
1328
  }
1322
1329
  await stripModuleBlocks(targetDirectory, selected);
1323
- await pruneDatabaseArtifacts(targetDirectory, removed.flatMap(({ databaseTables, transientDatabaseTables }) => [
1324
- ...databaseTables,
1325
- ...transientDatabaseTables
1326
- ]), removed.flatMap(({ databaseStatementTokens }) => databaseStatementTokens));
1330
+ await pruneDatabaseArtifacts(targetDirectory, [
1331
+ ...removed.flatMap(({ databaseTables, transientDatabaseTables }) => [
1332
+ ...databaseTables,
1333
+ ...transientDatabaseTables
1334
+ ]),
1335
+ ...moduleDistributionCatalog.filter(({ databaseLineageOwner }) => databaseLineageOwner === "module").flatMap(({ databaseTables }) => databaseTables)
1336
+ ], removed.flatMap(({ databaseStatementTokens }) => databaseStatementTokens));
1327
1337
  const removedRoots = removed.flatMap((manifest) => [
1328
1338
  ...manifest.packages,
1329
1339
  ...manifest.fileRoots,
@@ -1575,6 +1585,7 @@ var agentComposeService = ` agent-server:
1575
1585
  QWEN_API_URL: \${QWEN_API_URL:-}
1576
1586
  QWEN_API_KEY: \${QWEN_API_KEY:-}
1577
1587
  QWEN_MODEL: \${QWEN_MODEL:-}
1588
+ AI_PROVIDER_HTTPS_HOST_ALLOWLIST: \${AI_PROVIDER_HTTPS_HOST_ALLOWLIST:-}
1578
1589
  AI_PROVIDER_CONNECT_TIMEOUT_MS: \${AI_PROVIDER_CONNECT_TIMEOUT_MS:-5000}
1579
1590
  AI_PROVIDER_REQUEST_TIMEOUT_MS: \${AI_PROVIDER_REQUEST_TIMEOUT_MS:-60000}
1580
1591
  AI_PROVIDER_MAX_RESPONSE_BYTES: \${AI_PROVIDER_MAX_RESPONSE_BYTES:-2097152}
@@ -1596,12 +1607,13 @@ function environmentExample(includeExample, includeAgent = true) {
1596
1607
  POSTGRES_PASSWORD=
1597
1608
  WINDY_BOOTSTRAP_ADMIN_PASSWORD=
1598
1609
  ${includeAgent ? `
1599
- # Agent profile 仅由 Server 读取;不要使用 VITE_ 前缀。
1610
+ # Agent profile 仅由服务端 Agent Runtime 读取;不要使用 VITE_ 前缀。
1600
1611
  PLATFORM_ENABLE_AI=false
1601
1612
  AGENT_SERVER_INTERNAL_TOKEN=
1602
1613
  QWEN_API_URL=
1603
1614
  QWEN_API_KEY=
1604
1615
  QWEN_MODEL=
1616
+ AI_PROVIDER_HTTPS_HOST_ALLOWLIST=
1605
1617
  # AI_PROVIDER_CONNECT_TIMEOUT_MS=5000
1606
1618
  # AI_PROVIDER_REQUEST_TIMEOUT_MS=60000
1607
1619
  # AI_PROVIDER_MAX_RESPONSE_BYTES=2097152
@@ -1624,6 +1636,7 @@ License 签发中心、独立签名服务、Vault、发布治理、内部任务
1624
1636
  规划属于软件供应方控制面,不进入客户 Starter。
1625
1637
 
1626
1638
  - 架构:从 [Module Manifest 组合](./architecture/module-manifest-composition.md) 开始。
1639
+ - 数据库:业务表使用 [Migration Bundle 与独立 Drizzle lineage](./architecture/migration-bundle.md)。
1627
1640
  - 平台开发:从 [系统 CRUD API](./platform/system-crud-api.md) 和 [CRUD 生成器](./development/crud-generator.md) 开始。
1628
1641
  ${includeOfflineLicense ? "- License 消费端:[离线激活与安装协议](./license/license-offline-activation-code.md);运行时能力由 npm 依赖 `@southwind-ai/license` 提供。" : ""}
1629
1642
  - 运维:[可观测性](./operations/observability.md) 与 [运维总览](./platform/operations-overview.md)。
@@ -1701,7 +1714,9 @@ bun run dev:agent
1701
1714
  \`\`\`
1702
1715
 
1703
1716
  公网 Provider URL 必须使用 HTTPS;本机、容器服务名和内网地址允许 HTTP。
1704
- \`QWEN_API_KEY\` 只进入 Agent Server,不会进入 Web 环境或返回给客户端。运行与模块
1717
+ \`QWEN_API_KEY\`、Endpoint 和 Host allowlist 只进入 Agent Server,不会进入 Web
1718
+ 环境或返回给客户端。公网 HTTPS Endpoint 的主机必须显式加入
1719
+ \`AI_PROVIDER_HTTPS_HOST_ALLOWLIST\`。运行与模块
1705
1720
  接入细节见 [Agent Runtime 接入](./docs/platform/agent-runtime.md)。
1706
1721
 
1707
1722
  MCP 客户端使用 \`http://localhost:9747/api/mcp\`,并通过平台 Bearer Token 或 Session
@@ -1748,6 +1763,9 @@ create-windy 在本机 \`.env\` 中随机生成的 \`WINDY_BOOTSTRAP_ADMIN_PASSW
1748
1763
  | ---------- | -------- | ------------ | ---- |
1749
1764
  ${moduleRows}
1750
1765
 
1766
+ 项目自有业务模块以 \`apps/server/src/installed-business-modules.ts\` 装配的 Module Manifest
1767
+ 为准;shared-scaffold README 不复制项目业务 Key,避免平台升级固化产品命名。
1768
+
1751
1769
  精确生成来源和选择记录在 \`.windy/generation.json\`。当前版本已提供可回滚的
1752
1770
  \`windy update\`;通用 \`module add/remove\` 尚未交付,不要把 Feature Flag 关闭当成
1753
1771
  模块已经卸载。
@@ -1876,6 +1894,7 @@ var templateRootFiles = [
1876
1894
  "bunfig.toml",
1877
1895
  "docker-compose.yml",
1878
1896
  "drizzle.config.ts",
1897
+ "module-schema.lineages.json",
1879
1898
  "package.json"
1880
1899
  ];
1881
1900
  var templateDirectories = [
@@ -1898,10 +1917,12 @@ var templateDirectories = [
1898
1917
  var templateDocumentationFiles = [
1899
1918
  "docs/adapters/ruoyi-vue-compatibility.md",
1900
1919
  "docs/architecture/admin-surface-boundary.md",
1920
+ "docs/architecture/ai-egress.md",
1901
1921
  "docs/architecture/ai-runtime.md",
1902
1922
  "docs/architecture/data-scope-query-enforcement.md",
1903
1923
  "docs/architecture/durable-jobs.md",
1904
1924
  "docs/architecture/minimal-approval-workflow.md",
1925
+ "docs/architecture/migration-bundle.md",
1905
1926
  "docs/architecture/module-manifest-composition.md",
1906
1927
  "docs/architecture/object-storage.md",
1907
1928
  "docs/architecture/search-provider-protocol.md",
@@ -4338,7 +4359,9 @@ var recipes = [
4338
4359
  { id: "starter-0.2.25-to-0.2.26", from: "0.2.25", to: "0.2.26" },
4339
4360
  { id: "starter-0.2.26-to-0.2.27", from: "0.2.26", to: "0.2.27" },
4340
4361
  { id: "starter-0.2.27-to-0.2.28", from: "0.2.27", to: "0.2.28" },
4341
- { id: "starter-0.2.28-to-0.2.29", from: "0.2.28", to: "0.2.29" }
4362
+ { id: "starter-0.2.28-to-0.2.29", from: "0.2.28", to: "0.2.29" },
4363
+ { id: "starter-0.2.29-to-0.2.30", from: "0.2.29", to: "0.2.30" },
4364
+ { id: "starter-0.2.30-to-0.2.31", from: "0.2.30", to: "0.2.31" }
4342
4365
  ];
4343
4366
  function resolveRecipeChain(sourceVersion, targetVersion) {
4344
4367
  if (sourceVersion === targetVersion)
@@ -4466,11 +4489,32 @@ var known023DownstreamHashes = new Map([
4466
4489
  "81fb08f26e5baa3238606d62b9c46761a68c90f3967ea639b178c80c74c69fc1"
4467
4490
  ]
4468
4491
  ]);
4492
+ var known029DownstreamHashes = new Map([
4493
+ [
4494
+ "README.md",
4495
+ "a8b10907e151c28768678c016d102f98c3414981a65a314d75b2049fb1f9fe35"
4496
+ ],
4497
+ [
4498
+ "drizzle.config.ts",
4499
+ "c765b611d68b3a42375ea3530048fc7f32b1c7161257fa16f9dd093b580963e3"
4500
+ ],
4501
+ [
4502
+ "packages/database/src/schema/index.ts",
4503
+ "250682d5182b580a9e1fae27e7a596e6a503b41d6f5dca9e4322f5c6094d0976"
4504
+ ],
4505
+ [
4506
+ "apps/web/src/router/shared-scaffold-neutrality.webtest.ts",
4507
+ "420f1093eefcccf2a321e12c403628854c9a4a0b459ee14e96883557cd71dcaa"
4508
+ ]
4509
+ ]);
4469
4510
  var wrongExampleDescription = "创建时包含了 `work-order` 示例模块,用于演示 Module Manifest 接入;它不是生产工单产品,可以在正式开发前替换。";
4470
4511
  var correctCoreDescription = "创建时未选择 `work-order` 示例模块,当前项目只包含平台模块。";
4471
4512
  var wrongExampleRow = /^\|\s*示例工单\s*\|\s*已包含\s*\|$/m;
4472
4513
  var correctCoreRow = /^\|\s*示例工单\s*\|\s*未包含\s*\|$/m;
4473
4514
  function repairKnownDerivedFile(input) {
4515
+ const ownershipRepair = repairKnown029Downstream(input);
4516
+ if (ownershipRepair !== undefined)
4517
+ return ownershipRepair;
4474
4518
  const neutralRouterRepair = repairKnown026Downstream(input);
4475
4519
  if (neutralRouterRepair !== undefined)
4476
4520
  return neutralRouterRepair;
@@ -4494,6 +4538,46 @@ function repairKnownDerivedFile(input) {
4494
4538
  }
4495
4539
  return input.content.replace(wrongExampleDescription, correctCoreDescription).replace(wrongExampleRow, targetRow);
4496
4540
  }
4541
+ function repairKnown029Downstream(input) {
4542
+ if (input.sourceVersion !== "0.2.29" || known029DownstreamHashes.get(input.path) !== contentHash(input.content)) {
4543
+ return;
4544
+ }
4545
+ if (input.path === "drizzle.config.ts" || input.path === "apps/web/src/router/shared-scaffold-neutrality.webtest.ts") {
4546
+ return input.targetContent;
4547
+ }
4548
+ if (input.path === "packages/database/src/schema/index.ts") {
4549
+ return removeUnknownSchemaExports(input.content, input.targetContent);
4550
+ }
4551
+ if (input.path === "README.md") {
4552
+ return removeProjectModuleRows(input.content, input.targetContent);
4553
+ }
4554
+ }
4555
+ function removeUnknownSchemaExports(content, targetContent) {
4556
+ const targetExports = new Set(targetContent.split(`
4557
+ `).filter((line) => line.startsWith("export ") && line.includes(" from ")));
4558
+ return content.split(`
4559
+ `).filter((line) => !line.startsWith("export ") || !line.includes(" from ") || targetExports.has(line)).join(`
4560
+ `);
4561
+ }
4562
+ function removeProjectModuleRows(content, targetContent) {
4563
+ const neutralNote = extractProjectModuleNote(targetContent);
4564
+ const withoutProjectRows = content.split(`
4565
+ `).filter((line) => !/^\|.*\|\s*产品模块\s*\|.*\|$/.test(line)).join(`
4566
+ `);
4567
+ if (!neutralNote || withoutProjectRows.includes(neutralNote.trim())) {
4568
+ return withoutProjectRows;
4569
+ }
4570
+ return replaceKnown(withoutProjectRows, `
4571
+ 精确生成来源和选择记录在`, `
4572
+ ${neutralNote}
4573
+ 精确生成来源和选择记录在`);
4574
+ }
4575
+ function extractProjectModuleNote(content) {
4576
+ const start = content.indexOf("项目自有业务模块以");
4577
+ const end = content.indexOf(`
4578
+ 精确生成来源和选择记录在`, start);
4579
+ return start < 0 || end < 0 ? undefined : content.slice(start, end);
4580
+ }
4497
4581
  function repairKnown026Downstream(input) {
4498
4582
  if (input.sourceVersion !== "0.2.26")
4499
4583
  return;
@@ -5458,9 +5542,301 @@ function compareVersions2(left, right) {
5458
5542
  return 0;
5459
5543
  }
5460
5544
 
5545
+ // src/module-schema/cli.ts
5546
+ import { resolve as resolve7 } from "node:path";
5547
+
5548
+ // src/module-schema/handoff.ts
5549
+ import {
5550
+ access as access4,
5551
+ cp,
5552
+ mkdir as mkdir8,
5553
+ mkdtemp as mkdtemp3,
5554
+ readFile as readFile18,
5555
+ readdir as readdir7,
5556
+ rename,
5557
+ rm as rm9,
5558
+ writeFile as writeFile13
5559
+ } from "node:fs/promises";
5560
+ import { dirname as dirname5, join as join18, relative as relative4 } from "node:path";
5561
+
5562
+ // src/module-schema/config.ts
5563
+ import { readFile as readFile17 } from "node:fs/promises";
5564
+ import { isAbsolute, relative as relative3, resolve as resolve6 } from "node:path";
5565
+ async function readModuleSchemaLineageConfig(projectDirectory, configPath) {
5566
+ const absolutePath = resolveProjectPath(projectDirectory, configPath);
5567
+ const parsed = JSON.parse(await readFile17(absolutePath, "utf8"));
5568
+ if (!isConfig(parsed)) {
5569
+ throw new Error("Module schema lineage 配置无效");
5570
+ }
5571
+ validatePaths(projectDirectory, parsed);
5572
+ validateUniqueness(parsed);
5573
+ if (parsed.modules.length === 0) {
5574
+ throw new Error("Module schema lineage 配置没有待交接的业务模块");
5575
+ }
5576
+ return parsed;
5577
+ }
5578
+ function resolveProjectPath(projectDirectory, path2) {
5579
+ if (isAbsolute(path2))
5580
+ throw new Error(`路径必须相对项目根目录:${path2}`);
5581
+ const resolved = resolve6(projectDirectory, path2);
5582
+ const projectRelative = relative3(projectDirectory, resolved);
5583
+ if (projectRelative.startsWith("..") || isAbsolute(projectRelative)) {
5584
+ throw new Error(`路径越出项目根目录:${path2}`);
5585
+ }
5586
+ return resolved;
5587
+ }
5588
+ function isConfig(value) {
5589
+ if (!value || typeof value !== "object")
5590
+ return false;
5591
+ const config = value;
5592
+ return config.schemaVersion === 1 && isPlatform(config.platform) && Array.isArray(config.modules) && config.modules.every(isModule);
5593
+ }
5594
+ function isPlatform(value) {
5595
+ if (!value || typeof value !== "object")
5596
+ return false;
5597
+ const platform = value;
5598
+ return isStringList(platform.schema) && typeof platform.out === "string";
5599
+ }
5600
+ function isModule(value) {
5601
+ if (!value || typeof value !== "object")
5602
+ return false;
5603
+ const module3 = value;
5604
+ return typeof module3.key === "string" && /^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$/.test(module3.key) && isStringList(module3.schema) && typeof module3.out === "string" && isStringList(module3.legacyPlatformTables) && module3.legacyPlatformTables.every(isTableName);
5605
+ }
5606
+ function isStringList(value) {
5607
+ return Array.isArray(value) && value.length > 0 && value.every((item) => typeof item === "string" && item.length > 0);
5608
+ }
5609
+ function isTableName(value) {
5610
+ return /^[a-z_][a-z0-9_]*(?:\.[a-z_][a-z0-9_]*)?$/.test(value);
5611
+ }
5612
+ function validatePaths(projectDirectory, config) {
5613
+ for (const path2 of [
5614
+ ...config.platform.schema,
5615
+ config.platform.out,
5616
+ ...config.modules.flatMap(({ schema, out }) => [...schema, out])
5617
+ ]) {
5618
+ resolveProjectPath(projectDirectory, path2);
5619
+ }
5620
+ const platformOut = resolveProjectPath(projectDirectory, config.platform.out);
5621
+ for (const module3 of config.modules) {
5622
+ const moduleOut = resolveProjectPath(projectDirectory, module3.out);
5623
+ if (moduleOut === platformOut || moduleOut.startsWith(`${platformOut}/`) || platformOut.startsWith(`${moduleOut}/`)) {
5624
+ throw new Error(`${module3.key} 的 lineage 输出不能与平台目录重叠`);
5625
+ }
5626
+ }
5627
+ }
5628
+ function validateUniqueness(config) {
5629
+ const keys = new Set;
5630
+ const outputs = new Set;
5631
+ const tables = new Set;
5632
+ for (const module3 of config.modules) {
5633
+ if (keys.has(module3.key))
5634
+ throw new Error(`Module key 重复:${module3.key}`);
5635
+ if (outputs.has(module3.out))
5636
+ throw new Error(`Module lineage 输出重复:${module3.out}`);
5637
+ keys.add(module3.key);
5638
+ outputs.add(module3.out);
5639
+ for (const table of module3.legacyPlatformTables) {
5640
+ if (tables.has(table))
5641
+ throw new Error(`旧平台表重复声明:${table}`);
5642
+ tables.add(table);
5643
+ }
5644
+ }
5645
+ }
5646
+
5647
+ // src/module-schema/sql.ts
5648
+ function validatePlatformHandoffSql(sql, allowedTables) {
5649
+ const allowed = new Set(allowedTables);
5650
+ const dropped = new Set;
5651
+ const statements = sql.replaceAll("--> statement-breakpoint", "").split(";").map((statement) => statement.trim()).filter(Boolean);
5652
+ for (const statement of statements) {
5653
+ const match = statement.match(/^DROP TABLE(?: IF EXISTS)? (?:(?:"([^"]+)")\.)?"([^"]+)"(?: CASCADE)?$/i);
5654
+ if (!match) {
5655
+ throw new Error(`交接差异包含非 DROP TABLE DDL:${statement}`);
5656
+ }
5657
+ const table = normalizeTableName(match[1], match[2]);
5658
+ if (!allowed.has(table)) {
5659
+ throw new Error(`交接差异试图删除未声明的表:${table}`);
5660
+ }
5661
+ dropped.add(table);
5662
+ }
5663
+ const missing = [...allowed].filter((table) => !dropped.has(table));
5664
+ if (missing.length > 0) {
5665
+ throw new Error(`平台 snapshot 未包含声明的旧表:${missing.join("、")}`);
5666
+ }
5667
+ }
5668
+ function metadataOnlySql(kind) {
5669
+ const owner = kind === "platform" ? "platform lineage release" : "module lineage baseline";
5670
+ return `-- Windy ${owner}.
5671
+ -- Metadata-only migration: intentionally no executable DDL.
5672
+ `;
5673
+ }
5674
+ function normalizeTableName(schema, table) {
5675
+ return schema && schema !== "public" ? `${schema}.${table}` : table;
5676
+ }
5677
+
5678
+ // src/module-schema/handoff.ts
5679
+ async function handoffModuleSchemaLineages(options) {
5680
+ const project = options.projectDirectory;
5681
+ await assertCleanGit(project);
5682
+ const config = await readModuleSchemaLineageConfig(project, options.configPath || "module-schema.lineages.json");
5683
+ await validateSchemaSources(project, config);
5684
+ const temporaryRoot = await createTemporaryRoot(project);
5685
+ const execute = options.execute || executeDrizzleGenerate;
5686
+ try {
5687
+ const stagedModules = await stageModuleLineages(project, temporaryRoot, config, execute);
5688
+ const stagedPlatform = await stagePlatformLineage(project, temporaryRoot, config, execute);
5689
+ await installStagedLineages(project, temporaryRoot, config, stagedModules, stagedPlatform);
5690
+ } finally {
5691
+ await rm9(temporaryRoot, { recursive: true, force: true });
5692
+ }
5693
+ return {
5694
+ moduleCount: config.modules.length,
5695
+ tableCount: config.modules.reduce((total, module3) => total + module3.legacyPlatformTables.length, 0)
5696
+ };
5697
+ }
5698
+ async function stageModuleLineages(project, temporaryRoot, config, execute) {
5699
+ const outputs = [];
5700
+ for (const [index, module3] of config.modules.entries()) {
5701
+ const output = join18(temporaryRoot, `module-${index}`);
5702
+ await mkdir8(output, { recursive: true });
5703
+ const generatedConfig = join18(temporaryRoot, `module-${index}.config.ts`);
5704
+ await writeDrizzleConfig(generatedConfig, project, module3.schema, relative4(temporaryRoot, output));
5705
+ await execute(generatedConfig, temporaryRoot);
5706
+ const sqlFiles = await listSqlFiles(output);
5707
+ if (sqlFiles.length !== 1) {
5708
+ throw new Error(`${module3.key} baseline 必须恰好生成一个 migration`);
5709
+ }
5710
+ await writeFile13(join18(output, sqlFiles[0]), metadataOnlySql("module"));
5711
+ await writeFile13(join18(output, "lineage.json"), `${JSON.stringify({
5712
+ schemaVersion: 1,
5713
+ moduleKey: module3.key,
5714
+ schema: module3.schema,
5715
+ legacyPlatformTables: module3.legacyPlatformTables,
5716
+ productionExecutor: "module-migration-bundle"
5717
+ }, null, 2)}
5718
+ `);
5719
+ outputs.push(output);
5720
+ }
5721
+ return outputs;
5722
+ }
5723
+ async function stagePlatformLineage(project, temporaryRoot, config, execute) {
5724
+ const source = resolveProjectPath(project, config.platform.out);
5725
+ const output = join18(temporaryRoot, "platform");
5726
+ await cp(source, output, { recursive: true });
5727
+ const before = new Set(await listSqlFiles(output));
5728
+ const generatedConfig = join18(temporaryRoot, "platform.config.ts");
5729
+ await writeDrizzleConfig(generatedConfig, project, config.platform.schema, relative4(temporaryRoot, output));
5730
+ await execute(generatedConfig, temporaryRoot);
5731
+ const created = (await listSqlFiles(output)).filter((path2) => !before.has(path2));
5732
+ if (created.length !== 1) {
5733
+ throw new Error("平台 lineage handoff 必须恰好生成一个 migration");
5734
+ }
5735
+ const sqlPath = join18(output, created[0]);
5736
+ const allowed = config.modules.flatMap(({ legacyPlatformTables }) => legacyPlatformTables);
5737
+ validatePlatformHandoffSql(await readFile18(sqlPath, "utf8"), allowed);
5738
+ await writeFile13(sqlPath, metadataOnlySql("platform"));
5739
+ return output;
5740
+ }
5741
+ async function installStagedLineages(project, temporaryRoot, config, stagedModules, stagedPlatform) {
5742
+ const platform = resolveProjectPath(project, config.platform.out);
5743
+ const backup = join18(temporaryRoot, "platform-backup");
5744
+ const installed = [];
5745
+ await rename(platform, backup);
5746
+ try {
5747
+ await rename(stagedPlatform, platform);
5748
+ for (const [index, module3] of config.modules.entries()) {
5749
+ const output = resolveProjectPath(project, module3.out);
5750
+ if (await exists2(output)) {
5751
+ throw new Error(`${module3.key} lineage 输出已存在,禁止覆盖`);
5752
+ }
5753
+ await mkdir8(dirname5(output), { recursive: true });
5754
+ await rename(stagedModules[index], output);
5755
+ installed.push(output);
5756
+ }
5757
+ } catch (error) {
5758
+ await rm9(platform, { recursive: true, force: true });
5759
+ await rename(backup, platform);
5760
+ for (const output of installed)
5761
+ await rm9(output, { recursive: true, force: true });
5762
+ throw error;
5763
+ }
5764
+ }
5765
+ async function writeDrizzleConfig(path2, project, schemas, out) {
5766
+ const absoluteSchemas = schemas.map((schema) => resolveProjectPath(project, schema));
5767
+ await writeFile13(path2, `import { defineConfig } from "drizzle-kit";
5768
+ export default defineConfig(${JSON.stringify({
5769
+ dialect: "postgresql",
5770
+ schema: absoluteSchemas,
5771
+ out,
5772
+ casing: "snake_case"
5773
+ })});
5774
+ `);
5775
+ }
5776
+ async function validateSchemaSources(project, config) {
5777
+ for (const schema of [
5778
+ ...config.platform.schema,
5779
+ ...config.modules.flatMap(({ schema: schema2 }) => schema2)
5780
+ ]) {
5781
+ await access4(resolveProjectPath(project, schema));
5782
+ }
5783
+ for (const module3 of config.modules) {
5784
+ if (await exists2(resolveProjectPath(project, module3.out))) {
5785
+ throw new Error(`${module3.key} lineage 输出已存在,禁止覆盖`);
5786
+ }
5787
+ }
5788
+ }
5789
+ async function createTemporaryRoot(project) {
5790
+ const windy = join18(project, ".windy");
5791
+ await mkdir8(windy, { recursive: true });
5792
+ return await mkdtemp3(join18(windy, "module-schema-handoff-"));
5793
+ }
5794
+ async function listSqlFiles(root) {
5795
+ return (await readdir7(root)).filter((path2) => path2.endsWith(".sql")).sort();
5796
+ }
5797
+ async function executeDrizzleGenerate(configPath, cwd) {
5798
+ await assertProcess("bunx", ["--bun", "drizzle-kit", "generate", "--config", configPath], cwd, true);
5799
+ }
5800
+ async function assertCleanGit(project) {
5801
+ const result2 = await captureProcess("git", ["status", "--porcelain"], project);
5802
+ if (result2.code !== 0 || result2.stdout.trim()) {
5803
+ throw new Error("schema lineage handoff 前必须保持 Git 工作区干净");
5804
+ }
5805
+ }
5806
+ async function exists2(path2) {
5807
+ try {
5808
+ await access4(path2);
5809
+ return true;
5810
+ } catch {
5811
+ return false;
5812
+ }
5813
+ }
5814
+
5815
+ // src/module-schema/cli.ts
5816
+ async function runModuleSchemaCommand(args) {
5817
+ if (args[0] !== "schema")
5818
+ return false;
5819
+ if (args[1] !== "handoff") {
5820
+ throw new Error("当前 schema 命令只支持 handoff");
5821
+ }
5822
+ const projectDirectory = resolve7(readValue3(args, "--cwd") || process.cwd());
5823
+ const result2 = await handoffModuleSchemaLineages({
5824
+ projectDirectory,
5825
+ configPath: readValue3(args, "--config")
5826
+ });
5827
+ console.log(`Module schema lineage 交接完成:${result2.moduleCount} 个模块、${result2.tableCount} 张旧平台 snapshot 表`);
5828
+ return true;
5829
+ }
5830
+ function readValue3(args, flag) {
5831
+ const index = args.indexOf(flag);
5832
+ return index >= 0 ? args[index + 1] : undefined;
5833
+ }
5834
+
5461
5835
  // src/cli.ts
5462
5836
  async function main() {
5463
5837
  const args = process.argv.slice(2);
5838
+ if (await runModuleSchemaCommand(args))
5839
+ return;
5464
5840
  if (await runLifecycleCommand(args))
5465
5841
  return;
5466
5842
  const options = parseArguments(args);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-windy",
3
- "version": "0.2.29",
3
+ "version": "0.2.31",
4
4
  "description": "创建单组织、单租户、私有部署优先的 Windy 企业 Dashboard Starter",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "templateCommit": "fc15ecb10194385be79afcd4738b71c7966a9272",
4
- "generatorVersion": "0.2.29",
3
+ "templateCommit": "5d781a968ffc243b9c29c1ee31382cd61a5f4ab7",
4
+ "generatorVersion": "0.2.31",
5
5
  "modules": [
6
6
  {
7
7
  "name": "system",
@@ -107,6 +107,9 @@ create-windy 在本机 `.env` 中随机生成的 `WINDY_BOOTSTRAP_ADMIN_PASSWORD
107
107
  | `system.data-governance` | 未包含 | H2 | 敏感字段、明文访问和治理导出。 |
108
108
  | `work-order` | 未包含 | H2 | 可替换的业务模块接入示例。 |
109
109
 
110
+ 项目自有业务模块以 `apps/server/src/installed-business-modules.ts` 装配的 Module Manifest
111
+ 为准;shared-scaffold README 不复制项目业务 Key,避免平台升级固化产品命名。
112
+
110
113
  精确生成来源和选择记录在 `.windy/generation.json`。当前版本已提供可回滚的
111
114
  `windy update`;通用 `module add/remove` 尚未交付,不要把 Feature Flag 关闭当成
112
115
  模块已经卸载。
@@ -30,12 +30,14 @@ export AGENT_SERVER_INTERNAL_TOKEN='<random-token>'
30
30
  export QWEN_API_URL='https://dashscope-intl.aliyuncs.com/compatible-mode/v1'
31
31
  export QWEN_API_KEY='<server-secret>'
32
32
  export QWEN_MODEL='qwen-plus'
33
+ export AI_PROVIDER_HTTPS_HOST_ALLOWLIST='dashscope-intl.aliyuncs.com'
33
34
  export AGENT_RUN_STORE_PATH='.data/agent-runs.sqlite3'
34
35
  uv run uvicorn southwind_agent_server.app:app
35
36
  ```
36
37
 
37
- 公网 URL 必须使用 HTTPS,本机、容器服务名和内网地址允许 HTTP。Provider 请求包含连接
38
- 和总超时、响应大小限制、错误脱敏与 `/models` readiness。缺少 URL/Model 时回退
38
+ 公网 URL 必须使用 HTTPS Host 精确命中 allowlist;本机、容器服务名和内网地址允许
39
+ HTTP。Provider 请求包含连接和总超时、响应大小限制、错误脱敏与 `/models`
40
+ readiness。缺少 URL/Model 时回退
39
41
  `AdkEngineAdapter` 并返回 unavailable,不调用模型。测试另使用
40
42
  `DeterministicEngineAdapter`。
41
43
 
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import ipaddress
4
4
  import os
5
+ import re
5
6
  from collections.abc import Mapping
6
7
  from dataclasses import dataclass
7
8
  from urllib.parse import urlsplit, urlunsplit
@@ -35,6 +36,13 @@ class OpenAiCompatibleConfig:
35
36
  request_timeout_seconds: float = 60.0
36
37
  max_response_bytes: int = 2 * 1024 * 1024
37
38
  max_tool_rounds: int = 8
39
+ https_host_allowlist: tuple[str, ...] = ()
40
+ deployment_key: str = "ai.openai-compatible"
41
+
42
+ def __post_init__(self) -> None:
43
+ normalized = normalize_base_url(self.base_url)
44
+ object.__setattr__(self, "base_url", normalized)
45
+ require_allowed_https_host(normalized, self.https_host_allowlist)
38
46
 
39
47
  @classmethod
40
48
  def from_environment(
@@ -55,6 +63,9 @@ class OpenAiCompatibleConfig:
55
63
  if environment.get("QWEN_API_KEY", "").strip()
56
64
  else None
57
65
  ),
66
+ https_host_allowlist=normalize_https_host_allowlist(
67
+ environment.get("AI_PROVIDER_HTTPS_HOST_ALLOWLIST", "")
68
+ ),
58
69
  connect_timeout_seconds=read_milliseconds(
59
70
  environment,
60
71
  "AI_PROVIDER_CONNECT_TIMEOUT_MS",
@@ -100,6 +111,43 @@ def normalize_base_url(raw: str) -> str:
100
111
  return urlunsplit((parsed.scheme, parsed.netloc, path, "", ""))
101
112
 
102
113
 
114
+ def normalize_https_host_allowlist(raw: str) -> tuple[str, ...]:
115
+ hosts: list[str] = []
116
+ for item in raw.split(","):
117
+ candidate = item.strip().lower().rstrip(".")
118
+ if not candidate:
119
+ continue
120
+ if candidate in hosts or not valid_hostname(candidate):
121
+ raise ValueError("Provider HTTPS Host allowlist 无效")
122
+ hosts.append(candidate)
123
+ return tuple(hosts)
124
+
125
+
126
+ def require_allowed_https_host(
127
+ base_url: str,
128
+ allowed_hosts: tuple[str, ...],
129
+ ) -> None:
130
+ endpoint = urlsplit(base_url)
131
+ hostname = (endpoint.hostname or "").lower().rstrip(".")
132
+ if endpoint.scheme == "https" and hostname not in allowed_hosts:
133
+ raise ValueError("OpenAI-compatible Endpoint 未命中 HTTPS Host allowlist")
134
+
135
+
136
+ def valid_hostname(value: str) -> bool:
137
+ try:
138
+ ipaddress.ip_address(value)
139
+ return True
140
+ except ValueError:
141
+ return bool(
142
+ len(value) <= 253
143
+ and re.fullmatch(
144
+ r"(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)*"
145
+ r"[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?",
146
+ value,
147
+ )
148
+ )
149
+
150
+
103
151
  def private_hostname(hostname: str) -> bool:
104
152
  if (
105
153
  hostname == "localhost"