listpage_cli 0.0.295 → 0.0.296

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 (29) hide show
  1. package/bin/adapters/cli-interaction.js +4 -15
  2. package/bin/adapters/dockerode-client.js +295 -0
  3. package/bin/app/parse-args.js +86 -0
  4. package/bin/cli.js +5 -1
  5. package/bin/commands/deploy-project-command.js +17 -2
  6. package/bin/commands/release-project-command.js +24 -0
  7. package/bin/ports/release-project-command.js +2 -0
  8. package/bin/services/{artifact-validator.js → build-artifact-validator.js} +2 -2
  9. package/bin/services/config-value-utils.js +44 -0
  10. package/bin/services/deploy-project-service.js +303 -150
  11. package/bin/services/filesystem-capability-service.js +0 -5
  12. package/bin/services/init-service.js +0 -7
  13. package/bin/services/release-project-service.js +239 -0
  14. package/bin/types/deploy-config.js +2 -0
  15. package/package.json +6 -4
  16. package/templates/backend-template/package.json.tmpl +1 -1
  17. package/templates/backend-template/tsconfig.build.json +12 -2
  18. package/templates/frontend-template/package.json.tmpl +2 -2
  19. package/templates/rush-template/listpage.config.json.tmpl +89 -0
  20. package/bin/copy.js +0 -40
  21. package/bin/prompts.js +0 -170
  22. package/templates/package-app-template/.gitignore.tmpl +0 -28
  23. package/templates/package-app-template/README.md +0 -33
  24. package/templates/package-app-template/package.json +0 -27
  25. package/templates/package-app-template/src/build.ts +0 -6
  26. package/templates/package-app-template/src/config.ts.tmpl +0 -45
  27. package/templates/package-app-template/src/package.ts +0 -5
  28. package/templates/package-app-template/src/publish.ts +0 -6
  29. package/templates/package-app-template/tsconfig.json +0 -25
@@ -7,7 +7,6 @@ exports.runPrompt = runPrompt;
7
7
  exports.askRushQuestions = askRushQuestions;
8
8
  exports.askProjectPath = askProjectPath;
9
9
  exports.askOverwrite = askOverwrite;
10
- exports.askInstallDeployScript = askInstallDeployScript;
11
10
  exports.printHelp = printHelp;
12
11
  exports.printVersion = printVersion;
13
12
  const enquirer_1 = require("enquirer");
@@ -135,18 +134,6 @@ async function askOverwrite() {
135
134
  }
136
135
  return (0, interaction_result_1.interactionValue)(result.value.ok);
137
136
  }
138
- async function askInstallDeployScript() {
139
- const result = await runPrompt({
140
- type: "confirm",
141
- name: "ok",
142
- message: "是否添加部署脚本?这个脚本允许你可以帮你快速构建docker镜像,并发布到阿里云等环境,但是需要本机有docker环境。",
143
- initial: false,
144
- });
145
- if (result.status !== "value") {
146
- return result;
147
- }
148
- return (0, interaction_result_1.interactionValue)(result.value.ok);
149
- }
150
137
  function printHelp() {
151
138
  const h = [
152
139
  "用法: listpage_cli init",
@@ -155,8 +142,10 @@ function printHelp() {
155
142
  "说明: 安装技能到 Cursor。默认 skillName 为 test,安装到当前命令执行目录的 .cursor/skills/",
156
143
  "用法: listpage_cli build-project",
157
144
  "说明: 非交互校验当前目录是否为有效项目根(需存在 listpage.config.json)",
158
- "用法: listpage_cli deploy-project",
159
- "说明: 先校验 .listpage/output 产物,再按 login/build/tag/push 执行 Docker 部署",
145
+ "用法: listpage_cli release-project [tag] [--profile dev] [--platform linux/amd64] [--env KEY=VALUE]",
146
+ "说明: 先校验 .listpage/output 产物,再按 login/build/tag/push 执行 Docker 发布;参数优先级: CLI > profile > base",
147
+ "用法: listpage_cli deploy-project [tag] [--profile dev] [--platform linux/amd64] [--env KEY=VALUE]",
148
+ "说明: 使用 docker.remote + docker.container 执行部署,支持 ports[] 与 envFile/env 合并;参数优先级: CLI > profile > base",
160
149
  ].join("\n");
161
150
  console.log(h);
162
151
  }
@@ -0,0 +1,295 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createDockerodeClient = void 0;
7
+ exports.toDockerConnectionOptions = toDockerConnectionOptions;
8
+ exports.parsePorts = parsePorts;
9
+ exports.loadEnvFromFile = loadEnvFromFile;
10
+ exports.mergeEnv = mergeEnv;
11
+ exports.toRuntimeContainerInput = toRuntimeContainerInput;
12
+ const dockerode_1 = __importDefault(require("dockerode"));
13
+ const node_fs_1 = require("node:fs");
14
+ const createDockerodeClient = (config) => {
15
+ const options = toDockerConnectionOptions(config);
16
+ const client = new dockerode_1.default(options);
17
+ return {
18
+ ping: async () => {
19
+ await client.ping();
20
+ },
21
+ imageExists: async (image) => {
22
+ try {
23
+ await client.getImage(image).inspect();
24
+ return true;
25
+ }
26
+ catch {
27
+ return false;
28
+ }
29
+ },
30
+ removeImage: async (image) => {
31
+ await client.getImage(image).remove({ force: true });
32
+ },
33
+ pullImage: async (image, auth) => {
34
+ const stream = await new Promise((resolve, reject) => {
35
+ const pullOptions = auth ? { authconfig: auth } : undefined;
36
+ client.pull(image, pullOptions, (err, output) => {
37
+ if (err || !output) {
38
+ reject(err ?? new Error("docker pull 返回空输出"));
39
+ return;
40
+ }
41
+ resolve(output);
42
+ });
43
+ });
44
+ await new Promise((resolve, reject) => {
45
+ client.modem.followProgress(stream, (err) => {
46
+ if (err) {
47
+ reject(err);
48
+ return;
49
+ }
50
+ resolve();
51
+ }, () => undefined);
52
+ });
53
+ },
54
+ findContainerIdByName: async (name) => {
55
+ const containers = await client.listContainers({ all: true });
56
+ const matched = containers.find((item) => (item.Names ?? []).some((n) => n === `/${name}`));
57
+ return matched?.Id;
58
+ },
59
+ stopContainer: async (id) => {
60
+ const container = client.getContainer(id);
61
+ try {
62
+ await container.stop();
63
+ }
64
+ catch {
65
+ // Container may already be stopped.
66
+ }
67
+ },
68
+ removeContainer: async (id) => {
69
+ const container = client.getContainer(id);
70
+ await container.remove({ force: true });
71
+ },
72
+ createContainer: async (input) => {
73
+ const payload = toContainerCreateOptions(input);
74
+ const container = await client.createContainer(payload);
75
+ return container.id;
76
+ },
77
+ startContainer: async (id) => {
78
+ const container = client.getContainer(id);
79
+ await container.start();
80
+ },
81
+ inspectContainer: async (id) => {
82
+ const container = client.getContainer(id);
83
+ const inspectInfo = await container.inspect();
84
+ const state = inspectInfo?.State;
85
+ const portMappings = formatPortMappings(inspectInfo);
86
+ return {
87
+ running: state?.Running,
88
+ status: state?.Status,
89
+ exitCode: state?.ExitCode,
90
+ error: state?.Error,
91
+ startedAt: state?.StartedAt,
92
+ finishedAt: state?.FinishedAt,
93
+ health: state?.Health?.Status,
94
+ portMappings,
95
+ };
96
+ },
97
+ getContainerLogs: async (id, tail = 100) => {
98
+ const container = client.getContainer(id);
99
+ const logs = await container.logs({
100
+ stdout: true,
101
+ stderr: true,
102
+ timestamps: true,
103
+ follow: false,
104
+ tail,
105
+ });
106
+ if (typeof logs === "string") {
107
+ return logs;
108
+ }
109
+ return logs.toString("utf8");
110
+ },
111
+ };
112
+ };
113
+ exports.createDockerodeClient = createDockerodeClient;
114
+ function toDockerConnectionOptions(config) {
115
+ const protocol = (config.protocol ?? "https").toLowerCase();
116
+ const isHttps = protocol === "https";
117
+ const options = {
118
+ host: config.host ?? "127.0.0.1",
119
+ port: config.port ?? 2376,
120
+ protocol,
121
+ };
122
+ const socketPath = (config.socketPath ?? "").trim();
123
+ if (socketPath !== "") {
124
+ options.socketPath = socketPath;
125
+ }
126
+ if (isHttps) {
127
+ const tlsOptions = {
128
+ ca: asOptionalTlsBuffer(config.ca),
129
+ cert: asOptionalTlsBuffer(config.cert),
130
+ key: asOptionalTlsBuffer(config.key),
131
+ };
132
+ if (tlsOptions.ca) {
133
+ options.ca = tlsOptions.ca;
134
+ }
135
+ if (tlsOptions.cert) {
136
+ options.cert = tlsOptions.cert;
137
+ }
138
+ if (tlsOptions.key) {
139
+ options.key = tlsOptions.key;
140
+ }
141
+ options.checkServerIdentity = () => undefined;
142
+ }
143
+ return options;
144
+ }
145
+ function parsePorts(ports) {
146
+ if (!ports || ports.length === 0) {
147
+ return {};
148
+ }
149
+ const exposedPorts = {};
150
+ const portBindings = {};
151
+ for (const rawPort of ports) {
152
+ const normalized = rawPort.trim();
153
+ if (normalized === "") {
154
+ continue;
155
+ }
156
+ const [hostAndContainer, protocolPart] = normalized.split("/");
157
+ const protocol = protocolPart?.trim() === "udp" ? "udp" : "tcp";
158
+ const segments = hostAndContainer.split(":").map((part) => part.trim());
159
+ if (segments.length !== 2 || !segments[0] || !segments[1]) {
160
+ throw new Error(`container.ports 配置无效: ${rawPort}`);
161
+ }
162
+ const hostPort = segments[0];
163
+ const containerPort = segments[1];
164
+ const key = `${containerPort}/${protocol}`;
165
+ exposedPorts[key] = {};
166
+ portBindings[key] = [{ HostPort: hostPort }];
167
+ }
168
+ return {
169
+ exposedPorts: Object.keys(exposedPorts).length > 0 ? exposedPorts : undefined,
170
+ portBindings: Object.keys(portBindings).length > 0 ? portBindings : undefined,
171
+ };
172
+ }
173
+ function loadEnvFromFile(input) {
174
+ if (!input.envFilePath) {
175
+ return {};
176
+ }
177
+ const targetPath = input.resolve(input.projectRoot, input.envFilePath);
178
+ const raw = input.readText(targetPath);
179
+ const result = {};
180
+ for (const line of raw.split(/\r?\n/)) {
181
+ const normalized = line.trim();
182
+ if (normalized === "" || normalized.startsWith("#")) {
183
+ continue;
184
+ }
185
+ const eqIndex = normalized.indexOf("=");
186
+ if (eqIndex <= 0) {
187
+ continue;
188
+ }
189
+ const key = normalized.slice(0, eqIndex).trim();
190
+ const value = normalized.slice(eqIndex + 1);
191
+ if (key !== "") {
192
+ result[key] = value;
193
+ }
194
+ }
195
+ return result;
196
+ }
197
+ function mergeEnv(...inputs) {
198
+ const merged = {};
199
+ for (const input of inputs) {
200
+ if (!input) {
201
+ continue;
202
+ }
203
+ for (const [key, value] of Object.entries(input)) {
204
+ if (key.trim() === "") {
205
+ continue;
206
+ }
207
+ merged[key] = value;
208
+ }
209
+ }
210
+ return Object.entries(merged).map(([key, value]) => `${key}=${value}`);
211
+ }
212
+ function toRuntimeContainerInput(input) {
213
+ const envFromFile = loadEnvFromFile({
214
+ projectRoot: input.runtime.projectRoot,
215
+ envFilePath: input.runtime.envFilePath,
216
+ resolve: input.resolve,
217
+ readText: input.readText,
218
+ });
219
+ const env = mergeEnv(envFromFile, input.runtime.env, input.runtime.cliEnv);
220
+ const ports = parsePorts(input.runtime.ports);
221
+ return {
222
+ name: input.runtime.name,
223
+ image: input.runtime.image,
224
+ env,
225
+ exposedPorts: ports.exposedPorts,
226
+ portBindings: ports.portBindings,
227
+ binds: input.runtime.binds,
228
+ nanoCpus: input.runtime.nanoCpus,
229
+ memory: input.runtime.memory,
230
+ };
231
+ }
232
+ function toContainerCreateOptions(input) {
233
+ const hostConfig = {};
234
+ let hasHostConfig = false;
235
+ if (input.portBindings) {
236
+ hostConfig.PortBindings = input.portBindings;
237
+ hasHostConfig = true;
238
+ }
239
+ if (input.binds) {
240
+ hostConfig.Binds = input.binds;
241
+ hasHostConfig = true;
242
+ }
243
+ if (typeof input.nanoCpus === "number") {
244
+ hostConfig.NanoCpus = input.nanoCpus;
245
+ hasHostConfig = true;
246
+ }
247
+ if (typeof input.memory === "number") {
248
+ hostConfig.Memory = Number(input.memory) * 1024 * 1024;
249
+ hasHostConfig = true;
250
+ }
251
+ const payload = {
252
+ name: input.name,
253
+ Image: input.image,
254
+ Env: input.env,
255
+ };
256
+ if (input.exposedPorts) {
257
+ payload.ExposedPorts = input.exposedPorts;
258
+ }
259
+ if (hasHostConfig) {
260
+ payload.HostConfig = hostConfig;
261
+ }
262
+ return payload;
263
+ }
264
+ function asOptionalTlsBuffer(value) {
265
+ if (typeof value !== "string") {
266
+ return undefined;
267
+ }
268
+ const normalized = value.trim();
269
+ if (normalized === "") {
270
+ return undefined;
271
+ }
272
+ if ((0, node_fs_1.existsSync)(normalized)) {
273
+ return (0, node_fs_1.readFileSync)(normalized);
274
+ }
275
+ return Buffer.from(normalized);
276
+ }
277
+ function formatPortMappings(inspectInfo) {
278
+ const ports = inspectInfo?.NetworkSettings?.Ports;
279
+ if (!ports || typeof ports !== "object") {
280
+ return undefined;
281
+ }
282
+ const mappings = [];
283
+ for (const [containerPort, hostBindings] of Object.entries(ports)) {
284
+ if (!hostBindings || hostBindings.length === 0) {
285
+ mappings.push(`${containerPort} -> <not-published>`);
286
+ continue;
287
+ }
288
+ for (const binding of hostBindings) {
289
+ const hostIp = binding.HostIp || "0.0.0.0";
290
+ const hostPort = binding.HostPort || "<unknown>";
291
+ mappings.push(`${containerPort} -> ${hostIp}:${hostPort}`);
292
+ }
293
+ }
294
+ return mappings.length > 0 ? mappings : undefined;
295
+ }
@@ -1,10 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.parseArgs = parseArgs;
4
+ exports.parseCommandOptions = parseCommandOptions;
5
+ exports.parseCommandPositionals = parseCommandPositionals;
4
6
  const KNOWN_COMMANDS = new Set([
5
7
  "init",
6
8
  "install-skill",
7
9
  "build-project",
10
+ "release-project",
8
11
  "deploy-project",
9
12
  ]);
10
13
  function parseArgs(argv) {
@@ -37,3 +40,86 @@ function parseArgs(argv) {
37
40
  positionals,
38
41
  };
39
42
  }
43
+ function parseCommandOptions(rawArgs) {
44
+ const args = rawArgs.slice(1);
45
+ const profile = readSingleOption(args, "profile");
46
+ const platform = readSingleOption(args, "platform");
47
+ const envEntries = readMultiOption(args, "env");
48
+ const env = parseEnvEntries(envEntries);
49
+ return {
50
+ profile,
51
+ platform,
52
+ env: Object.keys(env).length > 0 ? env : undefined,
53
+ };
54
+ }
55
+ function parseCommandPositionals(rawArgs) {
56
+ const args = rawArgs.slice(1);
57
+ const positionals = [];
58
+ const optionsWithValue = new Set(["--profile", "--platform", "--env"]);
59
+ for (let index = 0; index < args.length; index += 1) {
60
+ const token = args[index];
61
+ if (token.startsWith("--")) {
62
+ if (token.includes("=")) {
63
+ continue;
64
+ }
65
+ if (optionsWithValue.has(token)) {
66
+ index += 1;
67
+ }
68
+ continue;
69
+ }
70
+ if (token.startsWith("-")) {
71
+ continue;
72
+ }
73
+ positionals.push(token);
74
+ }
75
+ return positionals;
76
+ }
77
+ function readSingleOption(args, optionName) {
78
+ const values = readMultiOption(args, optionName);
79
+ const last = values.at(-1);
80
+ if (typeof last !== "string") {
81
+ return undefined;
82
+ }
83
+ const normalized = last.trim();
84
+ return normalized === "" ? undefined : normalized;
85
+ }
86
+ function readMultiOption(args, optionName) {
87
+ const values = [];
88
+ const longFlag = `--${optionName}`;
89
+ const prefix = `${longFlag}=`;
90
+ for (let index = 0; index < args.length; index += 1) {
91
+ const token = args[index];
92
+ if (token === longFlag) {
93
+ const next = args[index + 1];
94
+ if (typeof next === "string" && !next.startsWith("-")) {
95
+ values.push(next);
96
+ index += 1;
97
+ }
98
+ continue;
99
+ }
100
+ if (token.startsWith(prefix)) {
101
+ values.push(token.slice(prefix.length));
102
+ }
103
+ }
104
+ return values;
105
+ }
106
+ function parseEnvEntries(entries) {
107
+ const result = {};
108
+ for (const entry of entries) {
109
+ const normalized = entry.trim();
110
+ if (normalized === "") {
111
+ continue;
112
+ }
113
+ const eqIndex = normalized.indexOf("=");
114
+ if (eqIndex <= 0) {
115
+ continue;
116
+ }
117
+ const key = normalized.slice(0, eqIndex).trim();
118
+ const value = normalized.slice(eqIndex + 1);
119
+ if (key === "") {
120
+ continue;
121
+ }
122
+ result[key] = value;
123
+ }
124
+ return result;
125
+ }
package/bin/cli.js CHANGED
@@ -7,6 +7,7 @@ const command_result_1 = require("./domain/command-result");
7
7
  const init_command_1 = require("./commands/init-command");
8
8
  const install_skill_command_1 = require("./commands/install-skill-command");
9
9
  const build_project_command_1 = require("./commands/build-project-command");
10
+ const release_project_command_1 = require("./commands/release-project-command");
10
11
  const deploy_project_command_1 = require("./commands/deploy-project-command");
11
12
  const node_fs_adapter_1 = require("./adapters/node-fs-adapter");
12
13
  const filesystem_capability_service_1 = require("./services/filesystem-capability-service");
@@ -19,7 +20,6 @@ const initCommandHandler = (0, init_command_1.createInitCommandHandler)({
19
20
  askProjectPath: cli_interaction_1.askProjectPath,
20
21
  askOverwrite: cli_interaction_1.askOverwrite,
21
22
  askRushQuestions: cli_interaction_1.askRushQuestions,
22
- askInstallDeployScript: cli_interaction_1.askInstallDeployScript,
23
23
  },
24
24
  fs: fsAdapter,
25
25
  files: filesystemCapability,
@@ -35,6 +35,9 @@ const installSkillCommandHandler = (0, install_skill_command_1.createInstallSkil
35
35
  const buildProjectCommandHandler = (0, build_project_command_1.createBuildProjectCommandHandler)({
36
36
  fs: fsAdapter,
37
37
  });
38
+ const releaseProjectCommandHandler = (0, release_project_command_1.createReleaseProjectCommandHandler)({
39
+ fs: fsAdapter,
40
+ });
38
41
  const deployProjectCommandHandler = (0, deploy_project_command_1.createDeployProjectCommandHandler)({
39
42
  fs: fsAdapter,
40
43
  });
@@ -46,6 +49,7 @@ async function main() {
46
49
  init: initCommandHandler,
47
50
  "install-skill": installSkillCommandHandler,
48
51
  "build-project": buildProjectCommandHandler,
52
+ "release-project": releaseProjectCommandHandler,
49
53
  "deploy-project": deployProjectCommandHandler,
50
54
  },
51
55
  });
@@ -1,9 +1,24 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createDeployProjectCommandHandler = createDeployProjectCommandHandler;
4
+ const parse_args_1 = require("../app/parse-args");
4
5
  const deploy_project_service_1 = require("../services/deploy-project-service");
5
6
  function createDeployProjectCommandHandler(deps) {
6
- return async (_input) => {
7
- return (0, deploy_project_service_1.runDeployProjectFlow)(deps);
7
+ return async (input) => {
8
+ const firstPositional = (0, parse_args_1.parseCommandPositionals)(input.rawArgs)[0];
9
+ const tag = typeof firstPositional === "string" && firstPositional.trim() !== ""
10
+ ? firstPositional.trim()
11
+ : undefined;
12
+ const options = (0, parse_args_1.parseCommandOptions)(input.rawArgs);
13
+ const cliOverrides = {
14
+ tag,
15
+ profile: options.profile,
16
+ platform: options.platform,
17
+ env: options.env,
18
+ };
19
+ return (0, deploy_project_service_1.runDeployProjectFlow)({
20
+ ...deps,
21
+ cliOverrides,
22
+ });
8
23
  };
9
24
  }
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createReleaseProjectCommandHandler = createReleaseProjectCommandHandler;
4
+ const parse_args_1 = require("../app/parse-args");
5
+ const release_project_service_1 = require("../services/release-project-service");
6
+ function createReleaseProjectCommandHandler(deps) {
7
+ return async (input) => {
8
+ const firstPositional = (0, parse_args_1.parseCommandPositionals)(input.rawArgs)[0];
9
+ const tag = typeof firstPositional === "string" && firstPositional.trim() !== ""
10
+ ? firstPositional.trim()
11
+ : undefined;
12
+ const options = (0, parse_args_1.parseCommandOptions)(input.rawArgs);
13
+ const cliOverrides = {
14
+ tag,
15
+ profile: options.profile,
16
+ platform: options.platform,
17
+ env: options.env,
18
+ };
19
+ return (0, release_project_service_1.runReleaseProjectFlow)({
20
+ ...deps,
21
+ cliOverrides,
22
+ });
23
+ };
24
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -1,8 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.createArtifactValidator = createArtifactValidator;
3
+ exports.createBuildArtifactValidator = createBuildArtifactValidator;
4
4
  const ARTIFACT_STAGE = "artifact";
5
- function createArtifactValidator(fs) {
5
+ function createBuildArtifactValidator(fs) {
6
6
  return (expectation) => {
7
7
  const issues = [];
8
8
  if (!fs.exists(expectation.outputDir)) {
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.asObject = asObject;
4
+ exports.asOptionalObject = asOptionalObject;
5
+ exports.asRequiredString = asRequiredString;
6
+ exports.asRequiredNumber = asRequiredNumber;
7
+ exports.asOptionalString = asOptionalString;
8
+ function asObject(value, fieldName) {
9
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
10
+ throw new Error(`缺少对象字段: ${fieldName}`);
11
+ }
12
+ return value;
13
+ }
14
+ function asOptionalObject(value) {
15
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
16
+ return undefined;
17
+ }
18
+ return value;
19
+ }
20
+ function asRequiredString(value, fieldName) {
21
+ if (typeof value === "string" && value.trim() !== "") {
22
+ return value.trim();
23
+ }
24
+ throw new Error(`缺少字段: ${fieldName}`);
25
+ }
26
+ function asRequiredNumber(value, fieldName) {
27
+ if (typeof value === "number" && Number.isFinite(value)) {
28
+ return value;
29
+ }
30
+ if (typeof value === "string" && value.trim() !== "") {
31
+ const parsed = Number(value);
32
+ if (Number.isFinite(parsed)) {
33
+ return parsed;
34
+ }
35
+ }
36
+ throw new Error(`字段类型无效: ${fieldName} (需要 number)`);
37
+ }
38
+ function asOptionalString(value) {
39
+ if (typeof value !== "string") {
40
+ return undefined;
41
+ }
42
+ const normalized = value.trim();
43
+ return normalized === "" ? undefined : normalized;
44
+ }