weaver-e10-builder 1.6.5 → 1.6.6

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 (37) hide show
  1. package/dist/commands/actionflow.d.ts.map +1 -1
  2. package/dist/commands/actionflow.js +16 -1
  3. package/dist/commands/actionflow.js.map +1 -1
  4. package/dist/commands/menu-permission.d.ts +11 -0
  5. package/dist/commands/menu-permission.d.ts.map +1 -1
  6. package/dist/commands/menu-permission.js +113 -26
  7. package/dist/commands/menu-permission.js.map +1 -1
  8. package/dist/commands/menu.d.ts +16 -5
  9. package/dist/commands/menu.d.ts.map +1 -1
  10. package/dist/commands/menu.js +153 -42
  11. package/dist/commands/menu.js.map +1 -1
  12. package/dist/services/actionflow/context_discovery.d.ts +16 -0
  13. package/dist/services/actionflow/context_discovery.d.ts.map +1 -0
  14. package/dist/services/actionflow/context_discovery.js +73 -0
  15. package/dist/services/actionflow/context_discovery.js.map +1 -0
  16. package/dist/services/actionflow/context_preflight.d.ts +0 -9
  17. package/dist/services/actionflow/context_preflight.d.ts.map +1 -1
  18. package/dist/services/actionflow/context_preflight.js +0 -59
  19. package/dist/services/actionflow/context_preflight.js.map +1 -1
  20. package/dist/services/actionflow/execution_setting.d.ts +11 -0
  21. package/dist/services/actionflow/execution_setting.d.ts.map +1 -0
  22. package/dist/services/actionflow/execution_setting.js +205 -0
  23. package/dist/services/actionflow/execution_setting.js.map +1 -0
  24. package/dist/services/actionflow/preflight_service.d.ts.map +1 -1
  25. package/dist/services/actionflow/preflight_service.js +107 -7
  26. package/dist/services/actionflow/preflight_service.js.map +1 -1
  27. package/dist/services/menu-catalog.d.ts +19 -0
  28. package/dist/services/menu-catalog.d.ts.map +1 -1
  29. package/dist/services/menu-catalog.js +31 -1
  30. package/dist/services/menu-catalog.js.map +1 -1
  31. package/package.json +1 -1
  32. package/skill-docs/SKILL.md +162 -163
  33. package/skill-docs/_catalog.md +1 -1
  34. package/skill-docs/actionflows/SKILL.md +8 -2
  35. package/skill-docs/actionflows/references/trigger_runtime_notes.md +6 -6
  36. package/skill-docs/references/common/ref-cli-commands.md +12 -11
  37. package/skill-docs/references/menu/system-menus.md +44 -1
@@ -1,20 +1,20 @@
1
1
  import { AuthSession } from '../core/auth.js';
2
2
  import { ApiClient } from '../core/client.js';
3
- import { allShareMap, buildAddNavigation, buildMoveNavigation, buildOrderNavigation, buildRenameNavigation, diffMenuGuard, findMenus, isMenuInSubtree, isSystemMenu, nestedCount, pickMenuBucket, pickParentId, resolveMenuShowOrder, showOrderOf, siblingIndex, siblingMenus, similarMenus, } from '../services/menu-catalog.js';
3
+ import { allShareMap, buildAddNavigation, buildMoveNavigation, buildOpenModeNavigation, buildOrderNavigation, buildRenameNavigation, diffMenuGuard, findMenus, isMenuInSubtree, isSystemMenu, nestedCount, openModeOf, pickMenuBucket, pickParentId, resolveMenuShowOrder, resolveOpenMode, showOrderOf, terminalTypeOf, siblingIndex, siblingMenus, similarMenus, } from '../services/menu-catalog.js';
4
4
  /**
5
5
  * 拉全量菜单树并展开。**必须是全量**:分页或只取顶层会把嵌套菜单漏掉,
6
6
  * 调用方据此说「这个应用没有这个菜单」就是假否定。
7
7
  */
8
- export async function fetchMenuTree(client, baseUrl, appId) {
9
- const url = `${baseUrl}/api/cusapp/app/allMenus?appId=${appId}&terminalType=PC&isFront=0&domain=${encodeURIComponent(baseUrl)}`;
8
+ export async function fetchMenuTree(client, baseUrl, appId, terminal = 'PC') {
9
+ const url = `${baseUrl}/api/cusapp/app/allMenus?appId=${appId}&terminalType=${terminal}&isFront=0&domain=${encodeURIComponent(baseUrl)}`;
10
10
  const data = await client.get(url, { ebbusinessid: appId });
11
11
  if (data?.code !== 200)
12
12
  return { ok: false, msg: data?.msg || '未知错误' };
13
13
  const { nodes, bucket } = pickMenuBucket(data?.data || {});
14
14
  return { ok: true, nodes, bucket };
15
15
  }
16
- export async function confirmMenuCreated(client, baseUrl, appId, match) {
17
- const tree = await fetchMenuTree(client, baseUrl, appId);
16
+ export async function confirmMenuCreated(client, baseUrl, appId, match, terminal = 'PC') {
17
+ const tree = await fetchMenuTree(client, baseUrl, appId, terminal);
18
18
  if (!tree.ok)
19
19
  return { state: 'unreadable', msg: tree.msg };
20
20
  const hit = tree.nodes.filter(match);
@@ -41,12 +41,12 @@ function requireMenuConfirmed(confirm, label) {
41
41
  * `--parent` 既收父菜单 ID 也收名称(名称在同一应用内重复时报错并列 ID,与 `menu rename` 同口径)。
42
42
  * `0` = 顶级菜单。需要算「同级最前面」时必须读菜单树;纯顶级、没给排序选项时不读。
43
43
  */
44
- export async function resolveMenuPlacement(client, baseUrl, appId, parentSpec, order) {
44
+ export async function resolveMenuPlacement(client, baseUrl, appId, parentSpec, order, terminal = 'PC') {
45
45
  const raw = String(parentSpec ?? '').trim();
46
46
  const needTree = (raw !== '' && raw !== '0') || order.first === true || order.showOrder !== undefined;
47
47
  if (!needTree)
48
48
  return { parentId: '0', nodes: [] };
49
- const tree = await fetchMenuTree(client, baseUrl, appId);
49
+ const tree = await fetchMenuTree(client, baseUrl, appId, terminal);
50
50
  if (tree.ok === false)
51
51
  throw new Error(`解析父菜单失败: ${tree.msg}`);
52
52
  const picked = pickParentId(tree.nodes, raw || '0');
@@ -75,6 +75,20 @@ export function requireMenuOrder(node, nodes, placement, label) {
75
75
  process.exit(1);
76
76
  }
77
77
  }
78
+ /**
79
+ * 回读校验打开方式。读侧(`allMenus`)不一定带 `openMode`,所以分三态:
80
+ * 一致 / 不一致(硬失败)/ 读不到(提示无法核对,不冒充成功)。
81
+ */
82
+ export function requireMenuOpenMode(node, expected, label) {
83
+ const raw = node.raw?.openMode;
84
+ if (raw === undefined || raw === null || String(raw) === '')
85
+ return 'unreadable';
86
+ if (String(raw) !== String(expected)) {
87
+ console.error(`已提交,但回读显示 ${label} 的打开方式是 ${raw},不是请求的 ${expected} —— 打开方式没生效。`);
88
+ process.exit(1);
89
+ }
90
+ return 'verified';
91
+ }
78
92
  /**
79
93
  * 回读确认菜单落在**指定的父菜单下**。只确认「这条菜单存在」是不够的 ——
80
94
  * `pid` 没生效时菜单照样建得出来,只是跑到了顶级,用户看到的正是「子菜单建不出来」。
@@ -91,7 +105,7 @@ export function requireMenuUnderParent(node, parentId, label) {
91
105
  * (the VIEWPORT pageId) via getViewlist, then savePublishMenu with pageType=VIEWPORT.
92
106
  * Returns the new menu id, or null on failure.
93
107
  */
94
- export async function addFormTableViewMenu(client, baseUrl, appId, objId, menuName, parentId = '0', showOrder) {
108
+ export async function addFormTableViewMenu(client, baseUrl, appId, objId, menuName, parentId = '0', showOrder, extra = {}) {
95
109
  // 1. getViewlist → tableView id (the viewport pageId)
96
110
  const vl = await client.get(`${baseUrl}/api/bs/ebuilder/form/viewlist/getViewlist/${objId}?apid=${appId}&tpaid=${objId}&objId=${objId}`, { ebbusinessid: appId });
97
111
  const tableViews = vl?.data?.tableView?.detail || [];
@@ -101,7 +115,7 @@ export async function addFormTableViewMenu(client, baseUrl, appId, objId, menuNa
101
115
  const name = menuName || tv.name || '台账';
102
116
  // 2. build VIEWPORT navigation (matches browser payload)
103
117
  const navigation = {
104
- content: name, id: '', openMode: '1', pid: parentId, type: '0',
118
+ content: name, id: '', openMode: resolveOpenMode(extra.openMode), pid: parentId, type: '0',
105
119
  filterCondition: {}, menuType: '1', params: [], showType: '1',
106
120
  defaultMenuSelect: false, defaultFrontEnable: '1',
107
121
  showOrder: showOrder === undefined ? 1 : Number(showOrder),
@@ -123,7 +137,7 @@ export async function addFormTableViewMenu(client, baseUrl, appId, objId, menuNa
123
137
  appId,
124
138
  navigation: JSON.stringify(navigation),
125
139
  isThirdParty: 'true',
126
- terminalType: 'PC',
140
+ terminalType: extra.terminal ?? 'PC',
127
141
  });
128
142
  const res = await client.post(`${baseUrl}/api/cusapp/app/savePublishMenu`, body, {
129
143
  'content-type': `multipart/form-data; boundary=${boundary}`,
@@ -139,10 +153,10 @@ export async function addFormTableViewMenu(client, baseUrl, appId, objId, menuNa
139
153
  * the form's create page (pageType=LAYOUT, pageId=1_<formObjId>), which triggers the
140
154
  * bound workflow when the form has one. Matches browser savePublishMenu payload.
141
155
  */
142
- export async function addFormStartMenu(client, baseUrl, appId, formObjId, menuName, parentId = '0', showOrder) {
156
+ export async function addFormStartMenu(client, baseUrl, appId, formObjId, menuName, parentId = '0', showOrder, extra = {}) {
143
157
  const name = menuName || '发起';
144
158
  const navigation = {
145
- content: name, id: '', openMode: '1', pid: parentId, type: '0',
159
+ content: name, id: '', openMode: resolveOpenMode(extra.openMode), pid: parentId, type: '0',
146
160
  filterCondition: {}, menuType: '1', params: [], showType: '1',
147
161
  defaultMenuSelect: false, defaultFrontEnable: '1',
148
162
  showOrder: showOrder === undefined ? 1 : Number(showOrder),
@@ -166,7 +180,7 @@ export async function addFormStartMenu(client, baseUrl, appId, formObjId, menuNa
166
180
  appId,
167
181
  navigation: JSON.stringify(navigation),
168
182
  isThirdParty: 'true',
169
- terminalType: 'PC',
183
+ terminalType: extra.terminal ?? 'PC',
170
184
  });
171
185
  const res = await client.post(`${baseUrl}/api/cusapp/app/savePublishMenu`, body, {
172
186
  'content-type': `multipart/form-data; boundary=${boundary}`,
@@ -183,13 +197,14 @@ export function registerMenuCommands(program) {
183
197
  .description('列出应用菜单(--raw 可打印原始报文,用于核查某一项到底存了什么)')
184
198
  .argument('<appId>', '应用 ID')
185
199
  .option('--raw [menuId]', '打印菜单的原始报文;给了菜单 ID 就只打印那一条')
200
+ .option('--terminal <t>', '终端: pc(缺省)/ mobile —— PC 与移动端是两套独立菜单')
186
201
  .action(async (appId, cmdOpts) => {
187
202
  const opts = program.opts();
188
203
  const auth = await AuthSession.fromOptions(opts);
189
204
  if (!auth)
190
205
  process.exit(1);
191
206
  const client = new ApiClient(auth);
192
- const tree = await fetchMenuTree(client, auth.getBaseUrl(), appId);
207
+ const tree = await fetchMenuTree(client, auth.getBaseUrl(), appId, terminalTypeOf(cmdOpts.terminal));
193
208
  if (!tree.ok) {
194
209
  console.error(`查询失败: ${tree.msg}`);
195
210
  process.exit(1);
@@ -237,14 +252,16 @@ export function registerMenuCommands(program) {
237
252
  .argument('<appId>', '应用 ID')
238
253
  .argument('<menu>', '菜单 ID 或名称(名称在同一应用内重复时必须改用 ID)')
239
254
  .argument('<newName>', '新名称')
240
- .action(async (appId, menuRef, newName) => {
255
+ .option('--terminal <t>', '终端: pc(缺省)/ mobile')
256
+ .action(async (appId, menuRef, newName, cmdOpts) => {
241
257
  const opts = program.opts();
242
258
  const auth = await AuthSession.fromOptions(opts);
243
259
  if (!auth)
244
260
  process.exit(1);
245
261
  const client = new ApiClient(auth);
246
262
  const baseUrl = auth.getBaseUrl();
247
- const tree = await fetchMenuTree(client, baseUrl, appId);
263
+ const terminal = terminalTypeOf(cmdOpts.terminal);
264
+ const tree = await fetchMenuTree(client, baseUrl, appId, terminal);
248
265
  if (!tree.ok) {
249
266
  console.error(`查询失败: ${tree.msg}`);
250
267
  process.exit(1);
@@ -286,7 +303,7 @@ export function registerMenuCommands(program) {
286
303
  appId,
287
304
  navigation: JSON.stringify(navigation),
288
305
  isThirdParty: 'true',
289
- terminalType: 'PC',
306
+ terminalType: terminal,
290
307
  });
291
308
  const res = await client.post(`${baseUrl}/api/cusapp/app/savePublishMenu`, body, {
292
309
  'content-type': `multipart/form-data; boundary=${boundary}`,
@@ -299,7 +316,7 @@ export function registerMenuCommands(program) {
299
316
  }
300
317
  // 回读校验:**名字变了**且**别的字段一个都没动**,两条都成立才算成功。
301
318
  // 只认「返回 200」曾经放过写坏菜单的报文(LINK 被写成 EMPTY)。
302
- const after = await fetchMenuTree(client, baseUrl, appId);
319
+ const after = await fetchMenuTree(client, baseUrl, appId, terminal);
303
320
  if (!after.ok) {
304
321
  console.error('改名已提交,但回读失败,无法确认结果 —— 请在页面上核对菜单。');
305
322
  process.exit(1);
@@ -332,26 +349,30 @@ export function registerMenuCommands(program) {
332
349
  .option('--parent <pid>', '父菜单 ID 或名称(默认 0=顶级菜单)', '0')
333
350
  .option('--first', '排在同级菜单最前面')
334
351
  .option('--show-order <n>', '同级排序号(整数,越小越靠前;缺省 1)')
352
+ .option('--open-mode <mode>', '打开方式: default(默认窗口,缺省)/ new-window(新窗口)')
353
+ .option('--terminal <t>', '终端: pc(缺省)/ mobile')
335
354
  .action(async (name, pagePath, appId, cmdOpts) => {
336
355
  const opts = program.opts();
337
356
  const auth = await AuthSession.fromOptions(opts);
338
357
  if (!auth)
339
358
  process.exit(1);
359
+ const terminal = terminalTypeOf(cmdOpts.terminal);
340
360
  // Fix MSYS path mangling: Git Bash converts /sp/custom/... to D:/Program Files/Git/sp/...
341
361
  const msysMatch = pagePath.match(/^[A-Za-z]:[/\\].+?(\/sp\/.+)$/);
342
362
  if (msysMatch)
343
363
  pagePath = msysMatch[1];
344
364
  pagePath = '/' + pagePath.replace(/^\/+/, '');
345
365
  const client = new ApiClient(auth);
346
- const placement = await resolveMenuPlacement(client, auth.getBaseUrl(), appId, cmdOpts.parent, cmdOpts);
366
+ const placement = await resolveMenuPlacement(client, auth.getBaseUrl(), appId, cmdOpts.parent, cmdOpts, terminal);
347
367
  const navigation = buildAddNavigation({
348
368
  name, appId, parentId: placement.parentId, pagePath, showOrder: placement.showOrder,
369
+ openMode: cmdOpts.openMode,
349
370
  });
350
371
  const { body, boundary } = ApiClient.buildMultipart({
351
372
  appId,
352
373
  navigation: JSON.stringify(navigation),
353
374
  isThirdParty: 'true',
354
- terminalType: 'PC',
375
+ terminalType: terminal,
355
376
  });
356
377
  const url = `${auth.getBaseUrl()}/api/cusapp/app/savePublishMenu`;
357
378
  const data = await client.post(url, body, {
@@ -363,19 +384,23 @@ export function registerMenuCommands(program) {
363
384
  console.error(`添加失败: ${data?.msg || '未知错误'}`);
364
385
  process.exit(1);
365
386
  }
366
- const confirm = await confirmMenuCreated(client, auth.getBaseUrl(), appId, (n) => n.name === name);
387
+ const confirm = await confirmMenuCreated(client, auth.getBaseUrl(), appId, (n) => n.name === name, terminal);
367
388
  const n = requireMenuConfirmed(confirm, `菜单「${name}」`);
368
389
  requireMenuUnderParent(n, placement.parentId, `菜单「${n.name}」`);
369
390
  if (confirm.state === 'found') {
370
391
  requireMenuOrder(n, confirm.nodes, { ...placement, first: cmdOpts.first }, `菜单「${n.name}」`);
371
392
  }
393
+ const openMode = cmdOpts.openMode === undefined ? undefined : resolveOpenMode(cmdOpts.openMode);
394
+ const openModeState = openMode === undefined ? undefined : requireMenuOpenMode(n, openMode, `菜单「${n.name}」`);
372
395
  const orderText = placement.showOrder === undefined ? '' : `, showOrder=${showOrderOf(n)}`;
396
+ const openModeText = openMode === undefined ? '' : `, openMode=${openMode}${openModeState === 'unreadable' ? '(读侧未返回,未核对)' : ''}`;
373
397
  console.log(opts.json
374
398
  ? JSON.stringify({
375
399
  success: true, name, path: pagePath, menuId: n.id, parentId: n.parentId, depth: n.depth,
376
- showOrder: placement.showOrder === undefined ? undefined : showOrderOf(n),
400
+ terminal, showOrder: placement.showOrder === undefined ? undefined : showOrderOf(n),
401
+ openMode,
377
402
  })
378
- : `菜单「${name}」已添加 → ${pagePath} (id=${n.id}${n.depth > 0 ? `, 位于第 ${n.depth} 层` : ''}${orderText})`);
403
+ : `菜单「${name}」已添加 → ${pagePath} (id=${n.id}${n.depth > 0 ? `, 位于第 ${n.depth} 层` : ''}${orderText}${openModeText}${terminal === 'H5' ? ', 移动端' : ''})`);
379
404
  });
380
405
  menu.command('add-dir')
381
406
  .description('添加上级目录菜单 —— 空链接的纯分类节点,只用来给子菜单分组,点它不跳页面')
@@ -384,14 +409,16 @@ export function registerMenuCommands(program) {
384
409
  .option('--parent <pid>', '父菜单 ID 或名称(默认 0=顶级菜单)', '0')
385
410
  .option('--first', '排在同级菜单最前面')
386
411
  .option('--show-order <n>', '同级排序号(整数,越小越靠前;缺省 1)')
412
+ .option('--terminal <t>', '终端: pc(缺省)/ mobile')
387
413
  .action(async (appId, name, cmdOpts) => {
388
414
  const opts = program.opts();
389
415
  const auth = await AuthSession.fromOptions(opts);
390
416
  if (!auth)
391
417
  process.exit(1);
392
418
  const client = new ApiClient(auth);
419
+ const terminal = terminalTypeOf(cmdOpts.terminal);
393
420
  try {
394
- const placement = await resolveMenuPlacement(client, auth.getBaseUrl(), appId, cmdOpts.parent, cmdOpts);
421
+ const placement = await resolveMenuPlacement(client, auth.getBaseUrl(), appId, cmdOpts.parent, cmdOpts, terminal);
395
422
  const navigation = buildAddNavigation({
396
423
  name, appId, parentId: placement.parentId, linkless: true, showOrder: placement.showOrder,
397
424
  });
@@ -399,7 +426,7 @@ export function registerMenuCommands(program) {
399
426
  appId,
400
427
  navigation: JSON.stringify(navigation),
401
428
  isThirdParty: 'true',
402
- terminalType: 'PC',
429
+ terminalType: terminal,
403
430
  });
404
431
  const data = await client.post(`${auth.getBaseUrl()}/api/cusapp/app/savePublishMenu`, body, {
405
432
  'content-type': `multipart/form-data; boundary=${boundary}`,
@@ -410,7 +437,7 @@ export function registerMenuCommands(program) {
410
437
  console.error(`添加失败: ${data?.msg || '未知错误'}`);
411
438
  process.exit(1);
412
439
  }
413
- const confirm = await confirmMenuCreated(client, auth.getBaseUrl(), appId, (n) => n.name === name);
440
+ const confirm = await confirmMenuCreated(client, auth.getBaseUrl(), appId, (n) => n.name === name, terminal);
414
441
  const n = requireMenuConfirmed(confirm, `上级目录「${name}」`);
415
442
  requireMenuUnderParent(n, placement.parentId, `上级目录「${n.name}」`);
416
443
  if (confirm.state === 'found') {
@@ -439,6 +466,7 @@ export function registerMenuCommands(program) {
439
466
  .argument('<menu>', '菜单 ID 或名称(名称在同一应用内重复时必须改用 ID)')
440
467
  .option('--first', '排到同级最前面')
441
468
  .option('--show-order <n>', '设置同级排序号(整数,越小越靠前)')
469
+ .option('--terminal <t>', '终端: pc(缺省)/ mobile')
442
470
  .action(async (appId, menuRef, cmdOpts) => {
443
471
  const opts = program.opts();
444
472
  const auth = await AuthSession.fromOptions(opts);
@@ -446,11 +474,12 @@ export function registerMenuCommands(program) {
446
474
  process.exit(1);
447
475
  const client = new ApiClient(auth);
448
476
  const baseUrl = auth.getBaseUrl();
477
+ const terminal = terminalTypeOf(cmdOpts.terminal);
449
478
  try {
450
479
  if (cmdOpts.first !== true && cmdOpts.showOrder === undefined) {
451
480
  throw new Error('需要 --first 或 --show-order <n>');
452
481
  }
453
- const tree = await fetchMenuTree(client, baseUrl, appId);
482
+ const tree = await fetchMenuTree(client, baseUrl, appId, terminal);
454
483
  if (!tree.ok)
455
484
  throw new Error(`读取菜单失败: ${tree.msg}`);
456
485
  const hits = findMenus(tree.nodes, menuRef);
@@ -474,7 +503,7 @@ export function registerMenuCommands(program) {
474
503
  throw new Error('需要 --first 或 --show-order <n>');
475
504
  const navigation = buildOrderNavigation(target, newOrder, appId);
476
505
  const { body, boundary } = ApiClient.buildMultipart({
477
- appId, navigation: JSON.stringify(navigation), isThirdParty: 'true', terminalType: 'PC',
506
+ appId, navigation: JSON.stringify(navigation), isThirdParty: 'true', terminalType: terminal,
478
507
  });
479
508
  const res = await client.post(`${baseUrl}/api/cusapp/app/savePublishMenu`, body, {
480
509
  'content-type': `multipart/form-data; boundary=${boundary}`,
@@ -483,7 +512,7 @@ export function registerMenuCommands(program) {
483
512
  });
484
513
  if (res?.code !== 200)
485
514
  throw new Error(`保存排序失败: ${res?.msg || '未知错误'}`);
486
- const after = await fetchMenuTree(client, baseUrl, appId);
515
+ const after = await fetchMenuTree(client, baseUrl, appId, terminal);
487
516
  if (!after.ok)
488
517
  throw new Error(`排序已提交,但回读失败(${after.msg}),请在设计器里核对`);
489
518
  const node = after.nodes.find((n) => n.id === target.id);
@@ -513,6 +542,76 @@ export function registerMenuCommands(program) {
513
542
  process.exit(1);
514
543
  }
515
544
  });
545
+ menu.command('open-mode')
546
+ .description('设置菜单打开方式:default=默认窗口 / new-window=新窗口')
547
+ .argument('<appId>', '应用 ID')
548
+ .argument('<menu>', '菜单 ID 或名称(名称在同一应用内重复时必须改用 ID)')
549
+ .argument('<mode>', 'default=默认窗口 / new-window=新窗口')
550
+ .option('--terminal <t>', '终端: pc(缺省)/ mobile')
551
+ .action(async (appId, menuRef, mode, cmdOpts) => {
552
+ const opts = program.opts();
553
+ const auth = await AuthSession.fromOptions(opts);
554
+ if (!auth)
555
+ process.exit(1);
556
+ const client = new ApiClient(auth);
557
+ const baseUrl = auth.getBaseUrl();
558
+ const terminal = terminalTypeOf(cmdOpts.terminal);
559
+ try {
560
+ const expected = resolveOpenMode(mode);
561
+ const tree = await fetchMenuTree(client, baseUrl, appId, terminal);
562
+ if (!tree.ok)
563
+ throw new Error(`读取菜单失败: ${tree.msg}`);
564
+ const hits = findMenus(tree.nodes, menuRef);
565
+ if (!hits.length)
566
+ throw new Error(`应用 ${appId} 下找不到菜单「${menuRef}」`);
567
+ if (hits.length > 1) {
568
+ throw new Error(`名称「${menuRef}」在同一应用下有 ${hits.length} 条,请改用菜单 ID:${hits.map((n) => n.id).join(' / ')}`);
569
+ }
570
+ const target = hits[0];
571
+ if (isSystemMenu(target))
572
+ throw new Error(`「${target.name}」是平台系统菜单(pageType=${target.pageType}),不支持调整打开方式`);
573
+ if (!target.pageType || target.pageType === 'EMPTY') {
574
+ throw new Error(`菜单「${target.name}」没有绑定页面(上级目录),打开方式没有意义`);
575
+ }
576
+ if (openModeOf(target) === expected) {
577
+ console.log(opts.json
578
+ ? JSON.stringify({ success: true, unchanged: true, menuId: target.id, openMode: expected })
579
+ : `菜单「${target.name}」的打开方式本来就是 ${expected === '2' ? '新窗口' : '默认窗口'},无需改动。`);
580
+ return;
581
+ }
582
+ const navigation = buildOpenModeNavigation(target, expected, appId);
583
+ const { body, boundary } = ApiClient.buildMultipart({
584
+ appId, navigation: JSON.stringify(navigation), isThirdParty: 'true', terminalType: terminal,
585
+ });
586
+ const res = await client.post(`${baseUrl}/api/cusapp/app/savePublishMenu`, body, {
587
+ 'content-type': `multipart/form-data; boundary=${boundary}`,
588
+ ebbusinessid: appId,
589
+ referer: `${baseUrl}/sp/ebdapp/build/${appId}`,
590
+ });
591
+ if (res?.code !== 200)
592
+ throw new Error(`保存打开方式失败: ${res?.msg || '未知错误'}`);
593
+ const after = await fetchMenuTree(client, baseUrl, appId, terminal);
594
+ if (!after.ok)
595
+ throw new Error(`已提交,但回读失败(${after.msg}),请在设计器里核对`);
596
+ const node = after.nodes.find((n) => n.id === target.id);
597
+ if (!node)
598
+ throw new Error('已提交,但回读时该菜单不见了');
599
+ const state = requireMenuOpenMode(node, expected, `菜单「${node.name}」`);
600
+ const broken = diffMenuGuard(target, node, ['openMode']);
601
+ if (broken.length) {
602
+ throw new Error(`打开方式生效了,但以下字段被服务端改动(可能影响菜单可用性):${broken.join(';')}`);
603
+ }
604
+ const label = expected === '2' ? '新窗口' : '默认窗口';
605
+ console.log(opts.json
606
+ ? JSON.stringify({ success: true, menuId: node.id, openMode: expected, verified: state === 'verified' })
607
+ : `菜单「${node.name}」打开方式已改为「${label}」(openMode=${expected})`
608
+ + (state === 'unreadable' ? '\n 注意: 菜单列表未返回 openMode,无法回读核对,请在设计器里确认一次。' : '\n 回读校验: 打开方式已生效,其余字段无变化'));
609
+ }
610
+ catch (e) {
611
+ console.error(`设置打开方式失败: ${e.message}`);
612
+ process.exit(1);
613
+ }
614
+ });
516
615
  menu.command('move')
517
616
  .description('移动已有菜单到另一个父级下(可同时调整同级排序)')
518
617
  .argument('<appId>', '应用 ID')
@@ -520,6 +619,7 @@ export function registerMenuCommands(program) {
520
619
  .requiredOption('--parent <pid>', '目标父菜单 ID 或名称(0=顶级菜单)')
521
620
  .option('--first', '移动到同级最前面')
522
621
  .option('--show-order <n>', '目标同级排序号(整数,越小越靠前)')
622
+ .option('--terminal <t>', '终端: pc(缺省)/ mobile')
523
623
  .action(async (appId, menuRef, cmdOpts) => {
524
624
  const opts = program.opts();
525
625
  const auth = await AuthSession.fromOptions(opts);
@@ -527,8 +627,9 @@ export function registerMenuCommands(program) {
527
627
  process.exit(1);
528
628
  const client = new ApiClient(auth);
529
629
  const baseUrl = auth.getBaseUrl();
630
+ const terminal = terminalTypeOf(cmdOpts.terminal);
530
631
  try {
531
- const tree = await fetchMenuTree(client, baseUrl, appId);
632
+ const tree = await fetchMenuTree(client, baseUrl, appId, terminal);
532
633
  if (!tree.ok)
533
634
  throw new Error(`读取菜单失败: ${tree.msg}`);
534
635
  const hits = findMenus(tree.nodes, menuRef);
@@ -574,7 +675,7 @@ export function registerMenuCommands(program) {
574
675
  }
575
676
  const navigation = buildMoveNavigation(target, parentId, appId, newOrder);
576
677
  const { body, boundary } = ApiClient.buildMultipart({
577
- appId, navigation: JSON.stringify(navigation), isThirdParty: 'true', terminalType: 'PC',
678
+ appId, navigation: JSON.stringify(navigation), isThirdParty: 'true', terminalType: terminal,
578
679
  });
579
680
  const res = await client.post(`${baseUrl}/api/cusapp/app/savePublishMenu`, body, {
580
681
  'content-type': `multipart/form-data; boundary=${boundary}`,
@@ -583,7 +684,7 @@ export function registerMenuCommands(program) {
583
684
  });
584
685
  if (res?.code !== 200)
585
686
  throw new Error(`保存移动失败: ${res?.msg || '未知错误'}`);
586
- const after = await fetchMenuTree(client, baseUrl, appId);
687
+ const after = await fetchMenuTree(client, baseUrl, appId, terminal);
587
688
  if (!after.ok)
588
689
  throw new Error(`移动已提交,但回读失败(${after.msg}),请在设计器里核对`);
589
690
  const node = after.nodes.find((n) => n.id === target.id);
@@ -642,31 +743,36 @@ export function registerMenuCommands(program) {
642
743
  .option('--parent <pid>', '父菜单 ID 或名称(默认 0=顶级菜单)', '0')
643
744
  .option('--first', '排在同级菜单最前面')
644
745
  .option('--show-order <n>', '同级排序号(整数,越小越靠前;缺省 1)')
746
+ .option('--open-mode <mode>', '打开方式: default(默认窗口,缺省)/ new-window(新窗口)')
747
+ .option('--terminal <t>', '终端: pc(缺省)/ mobile')
645
748
  .action(async (appId, objId, name, cmdOpts) => {
646
749
  const opts = program.opts();
647
750
  const auth = await AuthSession.fromOptions(opts);
648
751
  if (!auth)
649
752
  process.exit(1);
650
753
  const client = new ApiClient(auth);
754
+ const terminal = terminalTypeOf(cmdOpts.terminal);
651
755
  try {
652
- const placement = await resolveMenuPlacement(client, auth.getBaseUrl(), appId, cmdOpts.parent, cmdOpts);
653
- const menuId = await addFormTableViewMenu(client, auth.getBaseUrl(), appId, objId, name, placement.parentId, placement.showOrder);
756
+ const placement = await resolveMenuPlacement(client, auth.getBaseUrl(), appId, cmdOpts.parent, cmdOpts, terminal);
757
+ const menuId = await addFormTableViewMenu(client, auth.getBaseUrl(), appId, objId, name, placement.parentId, placement.showOrder, { openMode: cmdOpts.openMode, terminal });
654
758
  if (!menuId) {
655
759
  console.error('已提交,但服务端没有返回菜单 ID,无法回读确认 —— 请在设计器里核对台账菜单是否加上了。');
656
760
  process.exit(1);
657
761
  }
658
- const confirm = await confirmMenuCreated(client, auth.getBaseUrl(), appId, (x) => x.id === String(menuId));
762
+ const confirm = await confirmMenuCreated(client, auth.getBaseUrl(), appId, (x) => x.id === String(menuId), terminal);
659
763
  const n = requireMenuConfirmed(confirm, `台账菜单 (id=${menuId})`);
660
764
  requireMenuUnderParent(n, placement.parentId, `台账菜单「${n.name}」`);
661
765
  if (confirm.state === 'found') {
662
766
  requireMenuOrder(n, confirm.nodes, { ...placement, first: cmdOpts.first }, `台账菜单「${n.name}」`);
663
767
  }
768
+ const openMode = cmdOpts.openMode === undefined ? undefined : resolveOpenMode(cmdOpts.openMode);
769
+ const openModeState = openMode === undefined ? undefined : requireMenuOpenMode(n, openMode, `台账菜单「${n.name}」`);
664
770
  console.log(opts.json
665
771
  ? JSON.stringify({
666
772
  success: true, menuId: n.id, appId, objId, name: n.name, parentId: n.parentId, depth: n.depth,
667
- showOrder: placement.showOrder === undefined ? undefined : showOrderOf(n),
773
+ terminal, showOrder: placement.showOrder === undefined ? undefined : showOrderOf(n), openMode,
668
774
  })
669
- : `台账菜单已添加: ${n.name} (id=${n.id}${n.depth > 0 ? `, 位于第 ${n.depth} 层` : ''}${placement.showOrder === undefined ? '' : `, showOrder=${showOrderOf(n)}`})`);
775
+ : `台账菜单已添加: ${n.name} (id=${n.id}${n.depth > 0 ? `, 位于第 ${n.depth} 层` : ''}${placement.showOrder === undefined ? '' : `, showOrder=${showOrderOf(n)}`}${openMode === undefined ? '' : `, openMode=${openMode}${openModeState === 'unreadable' ? '(读侧未返回,未核对)' : ''}`}${terminal === 'H5' ? ', 移动端' : ''})`);
670
776
  }
671
777
  catch (e) {
672
778
  console.error(`添加失败: ${e.message}`);
@@ -681,31 +787,36 @@ export function registerMenuCommands(program) {
681
787
  .option('--parent <pid>', '父菜单 ID 或名称(默认 0=顶级菜单)', '0')
682
788
  .option('--first', '排在同级菜单最前面')
683
789
  .option('--show-order <n>', '同级排序号(整数,越小越靠前;缺省 1)')
790
+ .option('--open-mode <mode>', '打开方式: default(默认窗口,缺省)/ new-window(新窗口)')
791
+ .option('--terminal <t>', '终端: pc(缺省)/ mobile')
684
792
  .action(async (appId, formObjId, name, cmdOpts) => {
685
793
  const opts = program.opts();
686
794
  const auth = await AuthSession.fromOptions(opts);
687
795
  if (!auth)
688
796
  process.exit(1);
689
797
  const client = new ApiClient(auth);
798
+ const terminal = terminalTypeOf(cmdOpts.terminal);
690
799
  try {
691
- const placement = await resolveMenuPlacement(client, auth.getBaseUrl(), appId, cmdOpts.parent, cmdOpts);
692
- const menuId = await addFormStartMenu(client, auth.getBaseUrl(), appId, formObjId, name, placement.parentId, placement.showOrder);
800
+ const placement = await resolveMenuPlacement(client, auth.getBaseUrl(), appId, cmdOpts.parent, cmdOpts, terminal);
801
+ const menuId = await addFormStartMenu(client, auth.getBaseUrl(), appId, formObjId, name, placement.parentId, placement.showOrder, { openMode: cmdOpts.openMode, terminal });
693
802
  if (!menuId) {
694
803
  console.error('已提交,但服务端没有返回菜单 ID,无法回读确认 —— 请在设计器里核对发起菜单是否加上了。');
695
804
  process.exit(1);
696
805
  }
697
- const confirm = await confirmMenuCreated(client, auth.getBaseUrl(), appId, (x) => x.id === String(menuId));
806
+ const confirm = await confirmMenuCreated(client, auth.getBaseUrl(), appId, (x) => x.id === String(menuId), terminal);
698
807
  const n = requireMenuConfirmed(confirm, `发起菜单 (id=${menuId})`);
699
808
  requireMenuUnderParent(n, placement.parentId, `发起菜单「${n.name}」`);
700
809
  if (confirm.state === 'found') {
701
810
  requireMenuOrder(n, confirm.nodes, { ...placement, first: cmdOpts.first }, `发起菜单「${n.name}」`);
702
811
  }
812
+ const openMode = cmdOpts.openMode === undefined ? undefined : resolveOpenMode(cmdOpts.openMode);
813
+ const openModeState = openMode === undefined ? undefined : requireMenuOpenMode(n, openMode, `发起菜单「${n.name}」`);
703
814
  console.log(opts.json
704
815
  ? JSON.stringify({
705
816
  success: true, menuId: n.id, appId, formObjId, name: n.name, parentId: n.parentId, depth: n.depth,
706
- showOrder: placement.showOrder === undefined ? undefined : showOrderOf(n),
817
+ terminal, showOrder: placement.showOrder === undefined ? undefined : showOrderOf(n), openMode,
707
818
  })
708
- : `发起菜单已添加: ${n.name} (id=${n.id}${n.depth > 0 ? `, 位于第 ${n.depth} 层` : ''}${placement.showOrder === undefined ? '' : `, showOrder=${showOrderOf(n)}`})`);
819
+ : `发起菜单已添加: ${n.name} (id=${n.id}${n.depth > 0 ? `, 位于第 ${n.depth} 层` : ''}${placement.showOrder === undefined ? '' : `, showOrder=${showOrderOf(n)}`}${openMode === undefined ? '' : `, openMode=${openMode}${openModeState === 'unreadable' ? '(读侧未返回,未核对)' : ''}`}${terminal === 'H5' ? ', 移动端' : ''})`);
709
820
  }
710
821
  catch (e) {
711
822
  console.error(`添加失败: ${e.message}`);