i18-fe-automator-beta 2.1.5 → 2.1.7

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.
@@ -1807,6 +1807,8 @@ const CACHE_KEY = "zentao"; // .cache/zentao: { account, password, zentaosid, pr
1807
1807
 
1808
1808
  // 需求列表展示与pull并发的上限
1809
1809
  const PLAN_STORY_TABLE_LIMIT = 50;
1810
+ // 需求列表表格中标题的最大展示长度(console.table单元格不换行, 超长截断)
1811
+ const STORY_TITLE_LIMIT = 30;
1810
1812
  const PULL_CONCURRENCY = 5;
1811
1813
 
1812
1814
  // 任务状态中文标签(close 命令展示用)
@@ -2215,7 +2217,7 @@ async function selectProduct(zentaosid, cliOpts) {
2215
2217
  name: `#${p.id} ${p.name}`,
2216
2218
  value: p.id,
2217
2219
  })),
2218
- { name: "输入其他产品ID...", value: "__INPUT__" },
2220
+ { name: "输入其他产品-计划ID(如: https://zentao.hongxinshop.com/zentao/productplan-browse-29.html 中的29)", value: "__INPUT__" },
2219
2221
  ],
2220
2222
  },
2221
2223
  ]);
@@ -2226,11 +2228,11 @@ async function selectProduct(zentaosid, cliOpts) {
2226
2228
  }
2227
2229
  const { id } = await inquirer.prompt([
2228
2230
  {
2229
- message: "请输入产品ID",
2231
+ message: "请输入产品计划ID(如: https://zentao.hongxinshop.com/zentao/productplan-browse-29.html 中的29)",
2230
2232
  name: "id",
2231
2233
  type: "input",
2232
2234
  validate: (val) =>
2233
- /^\d+$/.test(String(val).trim()) || "请输入数字产品ID",
2235
+ /^\d+$/.test(String(val).trim()) || "请输入数字产品计划ID",
2234
2236
  },
2235
2237
  ]);
2236
2238
  return resolveProduct(zentaosid, id);
@@ -2752,8 +2754,8 @@ function writeCodeWorkspace(zentaoDir, planTitle, frontProjects, backProjects) {
2752
2754
  /**
2753
2755
  * plan --export 产出需求目录 .zentao-<计划名>/(产出契约见 .trae/documents/story-command-zentao.md 第4节):
2754
2756
  * tasks/manifest.json(对象结构: 顶部projects汇总全部涉及项目 + stories数组合并去重, 同storyID覆盖title/taskDir/projects, 按storyID数值排序)
2755
- * + 每条需求 tasks/<taskDir>/task.md(同名taskDir已由 exportPlanStories 先删除, 此处全新写入)
2756
- * plan.md 与 codex-*.md 是 workflow 的产物(重拉同名taskDir会连带删除, 确认关卡后不要再重导)
2757
+ * + 每条需求 tasks/<taskDir>/task.md(同名taskDir的task.md与images/已由 exportPlanStories 先删除, 此处全新写入)
2758
+ * plan.md 与 codex-*.md 是 workflow 的产物(重导时手术式刷新保留不覆盖, 但需求内容变了需AI自行判断是否重排)
2757
2759
  */
2758
2760
  function writeZentaoTasks({ plan, scope, stories, storyDetails, products, failed, storyImages, zentaoDir }) {
2759
2761
  const tasksDir = path.join(zentaoDir, "tasks");
@@ -2854,7 +2856,7 @@ function writeZentaoTasks({ plan, scope, stories, storyDetails, products, failed
2854
2856
  }
2855
2857
  }
2856
2858
 
2857
- // 计划选取交互: 关键词过滤 > 命中1条自动选中 > rawlist选取
2859
+ // 计划选取交互: 关键词过滤(标题) > 命中1条自动选中 > rawlist选取
2858
2860
  async function selectPlan(plans, keyword) {
2859
2861
  if (keyword && String(keyword).trim()) {
2860
2862
  const kw = String(keyword).trim().toLowerCase();
@@ -2899,10 +2901,15 @@ async function loadPlanList(zentaosid, productID) {
2899
2901
  return plans;
2900
2902
  }
2901
2903
 
2902
- // plan动作: 选中计划后展示计划信息 + 关联需求列表(ID供detail用)
2904
+ // plan动作: 选中计划后展示计划信息 + 关联需求列表(ID供detail用); 返回关联需求(供调用方判断空计划)
2903
2905
  async function showPlanDetail(zentaosid, selected) {
2904
2906
  Loading$1.start("拉取计划详情中...");
2905
2907
  const { plan, stories } = await fetchPlanDetail(zentaosid, selected.id);
2908
+ // 0条需求时详情/工时统计均无意义, 一句话结束
2909
+ if (!stories.length) {
2910
+ Loading$1.succeed(`计划 #${plan.id} ${plan.title} 无关联需求`);
2911
+ return stories;
2912
+ }
2906
2913
  Loading$1.succeed("拉取计划详情成功");
2907
2914
  console.log(chalk.bold("\n计划信息"));
2908
2915
  console.table([
@@ -2945,7 +2952,11 @@ async function showPlanDetail(zentaosid, selected) {
2945
2952
  );
2946
2953
  return {
2947
2954
  ID: s.id,
2948
- 标题: s.title,
2955
+ // console.table单元格不换行, 超长标题截断(完整标题可用 story detail 查看)
2956
+ 标题:
2957
+ s.title.length > STORY_TITLE_LIMIT
2958
+ ? `${s.title.slice(0, STORY_TITLE_LIMIT)}...`
2959
+ : s.title,
2949
2960
  前端工时: isFront ? s.webestimate : "-",
2950
2961
  后端工时: Number(s.estimate) > 0 ? s.estimate : "-",
2951
2962
  poollist: s.poollist || "-",
@@ -2957,6 +2968,7 @@ async function showPlanDetail(zentaosid, selected) {
2957
2968
  console.log(
2958
2969
  chalk.blue("提示: ID 供 fe-it-beta story detail <需求ID> 使用(也可传需求url)")
2959
2970
  );
2971
+ return stories;
2960
2972
  }
2961
2973
 
2962
2974
  // 导出动作(plan收尾交互使用): 按范围过滤需求, 拉详情并产出 .zentao-<计划名>/ + 工作区
@@ -3012,16 +3024,23 @@ async function exportPlanStories(zentaosid, selected, scope) {
3012
3024
  Loading$1.succeed(`拉取需求详情完成(${done}/${stories.length})`);
3013
3025
  // 需求目录: .zentao-<计划名>(多计划互不混目录)
3014
3026
  const zentaoDir = path.join(process.cwd(), zentaoDirNameOf(plan));
3015
- // 同名taskDir直接删除重导(重拉=全新快照, 连带清掉旧images与旧plan/codex报告; 失败条目保留旧数据)
3027
+ // 同名taskDir手术式刷新: 只删task.md与images/重写, 保留plan.md/codex报告等AI产物; 失败条目保留旧数据
3016
3028
  stories.forEach((story) => {
3017
3029
  if (failed.has(String(story.id))) return;
3018
3030
  const dir = path.join(zentaoDir, "tasks", taskDirOf(story));
3019
3031
  if (isExistPath(dir)) {
3020
- fs.rmSync(dir, { recursive: true, force: true });
3021
- console.log(chalk.yellow(`同名任务目录已删除重导: ${taskDirOf(story)}`));
3032
+ const taskMd = path.join(dir, "task.md");
3033
+ if (isExistPath(taskMd)) fs.rmSync(taskMd, { force: true });
3034
+ const imagesDir = path.join(dir, "images");
3035
+ if (isExistPath(imagesDir)) {
3036
+ fs.rmSync(imagesDir, { recursive: true, force: true });
3037
+ }
3038
+ console.log(
3039
+ chalk.yellow(`同名任务目录已刷新(保留plan/报告): ${taskDirOf(story)}`)
3040
+ );
3022
3041
  }
3023
3042
  });
3024
- // 图片本地化到 tasks/<taskDir>/images/(目录已删全量重下, 失败黄字不阻塞, 引用降级远程url)
3043
+ // 图片本地化到 tasks/<taskDir>/images/(images/已删全量重下, 失败黄字不阻塞, 引用降级远程url)
3025
3044
  const storyImages = await downloadStoryImages({
3026
3045
  stories,
3027
3046
  storyDetails,
@@ -3047,11 +3066,13 @@ async function exportPlanStories(zentaosid, selected, scope) {
3047
3066
  scope === "back" || scope === "all"
3048
3067
  ? collectPoolProjects(stories, "back")
3049
3068
  : [];
3050
- if (frontProjects.length || backProjects.length) {
3051
- writeCodeWorkspace(zentaoDir, plan.title, frontProjects, backProjects);
3052
- } else {
3053
- console.log(chalk.yellow("本范围无关联项目, 跳过工作区生成"));
3069
+ // 无pool时仍生成单目录工作区(只含需求目录自身, 打开仍能看manifest与task.md), 黄字提示补维护
3070
+ if (!frontProjects.length && !backProjects.length) {
3071
+ console.log(
3072
+ chalk.yellow("本范围需求均未维护poollist, 工作区仅含需求目录, 维护后重跑可补齐项目")
3073
+ );
3054
3074
  }
3075
+ writeCodeWorkspace(zentaoDir, plan.title, frontProjects, backProjects);
3055
3076
  }
3056
3077
 
3057
3078
  /**
@@ -3125,7 +3146,7 @@ async function storyMain(action, cliOpts) {
3125
3146
  if (!match) {
3126
3147
  console.log(
3127
3148
  chalk.red(
3128
- "请传入项目ID(从禅道项目需求页url复制), 如: fe-it-beta story task 2915 或 fe-it-beta story task https://zentao.hongxinshop.com/zentao/project-story-2915.html"
3149
+ "请传入项目-需求url的ID, 如: fe-it-beta story task 2915 或 fe-it-beta story task https://zentao.hongxinshop.com/zentao/project-story-2915.html"
3129
3150
  )
3130
3151
  );
3131
3152
  process.exit(1);
@@ -3725,7 +3746,7 @@ async function storyMain(action, cliOpts) {
3725
3746
  if (!match) {
3726
3747
  console.log(
3727
3748
  chalk.red(
3728
- "请传入项目ID(从禅道任务列表url复制), 如: fe-it-beta story close 2915 或 fe-it-beta story close https://zentao.hongxinshop.com/zentao/project-task-2915-myinvolved.html"
3749
+ "请传入项目-任务-由我参与url的ID, 如: fe-it-beta story close 2915 或 fe-it-beta story close https://zentao.hongxinshop.com/zentao/project-task-2915-myinvolved.html"
3729
3750
  )
3730
3751
  );
3731
3752
  process.exit(1);
@@ -3997,10 +4018,26 @@ async function storyMain(action, cliOpts) {
3997
4018
  // plan: 产品选择 -> 计划选取 -> 展示 -> 导出(--export直通一键场景, 不传则收尾交互)
3998
4019
  const session = await ensureZentaoSession(cliOpts);
3999
4020
  await session.request(async (zentaosid) => {
4000
- const product = await selectProduct(zentaosid, cliOpts);
4021
+ // 位置参数纯数字 = 产品计划ID(productplan-browse-<ID>.html 中的ID), 等同 --product 直通
4022
+ const nameArg = String(cliOpts.name || "").trim();
4023
+ const productIdArg = /^\d+$/.test(nameArg) ? nameArg : "";
4024
+ const product = await selectProduct(zentaosid, {
4025
+ ...cliOpts,
4026
+ product: cliOpts.product || productIdArg,
4027
+ });
4001
4028
  const plans = await loadPlanList(zentaosid, product.id);
4002
- const selected = await selectPlan(plans, cliOpts.name);
4003
- await showPlanDetail(zentaosid, selected);
4029
+ // 该产品无计划时后续选取/详情/导出均无意义, 提前结束
4030
+ if (!plans.length) {
4031
+ console.log(chalk.yellow(`产品 #${product.id} ${product.name} 下无计划, 结束`));
4032
+ return;
4033
+ }
4034
+ // 纯数字已用作产品ID, 非数字关键词仍按计划标题过滤
4035
+ const selected = await selectPlan(plans, productIdArg ? "" : cliOpts.name);
4036
+ const stories = await showPlanDetail(zentaosid, selected);
4037
+ // 空计划提示已在showPlanDetail输出, 直接结束不进导出询问
4038
+ if (!stories.length) {
4039
+ return;
4040
+ }
4004
4041
  // 导出范围: --export参数直通(一键场景) > 收尾交互(默认前端)
4005
4042
  let exportChoice;
4006
4043
  if (cliOpts.export !== undefined) {
@@ -4009,14 +4046,14 @@ async function storyMain(action, cliOpts) {
4009
4046
  } else {
4010
4047
  ({ exportChoice } = await inquirer.prompt([
4011
4048
  {
4012
- message: "是否导出需求文件(按计划生成 .zentao-<计划名>目录)?",
4049
+ message: `是否导出需求文件(按计划生成 .zentao-<计划名>目录, ${chalk.red("目前导出功能只适用于claude code批量建任务")})?`,
4013
4050
  name: "exportChoice",
4014
4051
  type: "list",
4015
4052
  choices: [
4016
- { name: "导出: 前端需求", value: "front" }, // 默认第一项
4053
+ { name: "", value: "none" }, // 默认第一项
4054
+ { name: "导出: 前端需求", value: "front" },
4017
4055
  { name: "导出: 后端需求", value: "back" },
4018
4056
  { name: "导出: 全部需求", value: "all" },
4019
- { name: "否", value: "none" },
4020
4057
  ],
4021
4058
  },
4022
4059
  ]));
@@ -4042,7 +4079,7 @@ function registerStoryCommand(program, { markHandled, addHelpExample }) {
4042
4079
  .option("--username <name>", "禅道账号(story命令用)")
4043
4080
  .option("--password <pwd>", "禅道密码(story命令用, 日常建议用--cache)")
4044
4081
  .option("--cache", "凭据静默走缓存")
4045
- .option("--product <id>", "禅道产品ID(plan用, 不传则交互选择)")
4082
+ .option("--product <id>", "禅道产品计划ID(plan用, 即productplan-browse-<ID>.html中的ID, 不传则交互选择)")
4046
4083
  .option("--export <scope>", "plan导出范围: front|back|all(传了跳过收尾询问直接导出)")
4047
4084
  .option("--type <t>", "task任务类型: front|back(传了跳过类型询问与确认, 一键创建)")
4048
4085
  .option("--split", "task时创建父任务并自动拆分子任务(>8h, 每个8h末个补余); subtask时跳过确认直接拆")
@@ -4061,6 +4098,7 @@ function registerStoryCommand(program, { markHandled, addHelpExample }) {
4061
4098
  // help示例经 helpers 注入 index.js 的 helpExamples, 自动参与列对齐
4062
4099
  addHelpExample("fe-it-beta story login", "禅道登录, 缓存zentaosid");
4063
4100
  addHelpExample("fe-it-beta story plan --product 87", "选计划看列表, 交互选导出");
4101
+ addHelpExample("fe-it-beta story plan 29", "29=产品计划ID(productplan-browse-29.html), 拉计划列表再选取");
4064
4102
  addHelpExample("fe-it-beta story detail 需求ID", "按需求ID查看详情");
4065
4103
  addHelpExample("fe-it-beta story task 项目ID", "选项目勾选需求, 交互建禅道任务");
4066
4104
  addHelpExample(
@@ -1785,6 +1785,8 @@ const CACHE_KEY = "zentao"; // .cache/zentao: { account, password, zentaosid, pr
1785
1785
 
1786
1786
  // 需求列表展示与pull并发的上限
1787
1787
  const PLAN_STORY_TABLE_LIMIT = 50;
1788
+ // 需求列表表格中标题的最大展示长度(console.table单元格不换行, 超长截断)
1789
+ const STORY_TITLE_LIMIT = 30;
1788
1790
  const PULL_CONCURRENCY = 5;
1789
1791
 
1790
1792
  // 任务状态中文标签(close 命令展示用)
@@ -2193,7 +2195,7 @@ async function selectProduct(zentaosid, cliOpts) {
2193
2195
  name: `#${p.id} ${p.name}`,
2194
2196
  value: p.id,
2195
2197
  })),
2196
- { name: "输入其他产品ID...", value: "__INPUT__" },
2198
+ { name: "输入其他产品-计划ID(如: https://zentao.hongxinshop.com/zentao/productplan-browse-29.html 中的29)", value: "__INPUT__" },
2197
2199
  ],
2198
2200
  },
2199
2201
  ]);
@@ -2204,11 +2206,11 @@ async function selectProduct(zentaosid, cliOpts) {
2204
2206
  }
2205
2207
  const { id } = await inquirer.prompt([
2206
2208
  {
2207
- message: "请输入产品ID",
2209
+ message: "请输入产品计划ID(如: https://zentao.hongxinshop.com/zentao/productplan-browse-29.html 中的29)",
2208
2210
  name: "id",
2209
2211
  type: "input",
2210
2212
  validate: (val) =>
2211
- /^\d+$/.test(String(val).trim()) || "请输入数字产品ID",
2213
+ /^\d+$/.test(String(val).trim()) || "请输入数字产品计划ID",
2212
2214
  },
2213
2215
  ]);
2214
2216
  return resolveProduct(zentaosid, id);
@@ -2730,8 +2732,8 @@ function writeCodeWorkspace(zentaoDir, planTitle, frontProjects, backProjects) {
2730
2732
  /**
2731
2733
  * plan --export 产出需求目录 .zentao-<计划名>/(产出契约见 .trae/documents/story-command-zentao.md 第4节):
2732
2734
  * tasks/manifest.json(对象结构: 顶部projects汇总全部涉及项目 + stories数组合并去重, 同storyID覆盖title/taskDir/projects, 按storyID数值排序)
2733
- * + 每条需求 tasks/<taskDir>/task.md(同名taskDir已由 exportPlanStories 先删除, 此处全新写入)
2734
- * plan.md 与 codex-*.md 是 workflow 的产物(重拉同名taskDir会连带删除, 确认关卡后不要再重导)
2735
+ * + 每条需求 tasks/<taskDir>/task.md(同名taskDir的task.md与images/已由 exportPlanStories 先删除, 此处全新写入)
2736
+ * plan.md 与 codex-*.md 是 workflow 的产物(重导时手术式刷新保留不覆盖, 但需求内容变了需AI自行判断是否重排)
2735
2737
  */
2736
2738
  function writeZentaoTasks({ plan, scope, stories, storyDetails, products, failed, storyImages, zentaoDir }) {
2737
2739
  const tasksDir = path.join(zentaoDir, "tasks");
@@ -2832,7 +2834,7 @@ function writeZentaoTasks({ plan, scope, stories, storyDetails, products, failed
2832
2834
  }
2833
2835
  }
2834
2836
 
2835
- // 计划选取交互: 关键词过滤 > 命中1条自动选中 > rawlist选取
2837
+ // 计划选取交互: 关键词过滤(标题) > 命中1条自动选中 > rawlist选取
2836
2838
  async function selectPlan(plans, keyword) {
2837
2839
  if (keyword && String(keyword).trim()) {
2838
2840
  const kw = String(keyword).trim().toLowerCase();
@@ -2877,10 +2879,15 @@ async function loadPlanList(zentaosid, productID) {
2877
2879
  return plans;
2878
2880
  }
2879
2881
 
2880
- // plan动作: 选中计划后展示计划信息 + 关联需求列表(ID供detail用)
2882
+ // plan动作: 选中计划后展示计划信息 + 关联需求列表(ID供detail用); 返回关联需求(供调用方判断空计划)
2881
2883
  async function showPlanDetail(zentaosid, selected) {
2882
2884
  Loading.start("拉取计划详情中...");
2883
2885
  const { plan, stories } = await fetchPlanDetail(zentaosid, selected.id);
2886
+ // 0条需求时详情/工时统计均无意义, 一句话结束
2887
+ if (!stories.length) {
2888
+ Loading.succeed(`计划 #${plan.id} ${plan.title} 无关联需求`);
2889
+ return stories;
2890
+ }
2884
2891
  Loading.succeed("拉取计划详情成功");
2885
2892
  console.log(chalk.bold("\n计划信息"));
2886
2893
  console.table([
@@ -2923,7 +2930,11 @@ async function showPlanDetail(zentaosid, selected) {
2923
2930
  );
2924
2931
  return {
2925
2932
  ID: s.id,
2926
- 标题: s.title,
2933
+ // console.table单元格不换行, 超长标题截断(完整标题可用 story detail 查看)
2934
+ 标题:
2935
+ s.title.length > STORY_TITLE_LIMIT
2936
+ ? `${s.title.slice(0, STORY_TITLE_LIMIT)}...`
2937
+ : s.title,
2927
2938
  前端工时: isFront ? s.webestimate : "-",
2928
2939
  后端工时: Number(s.estimate) > 0 ? s.estimate : "-",
2929
2940
  poollist: s.poollist || "-",
@@ -2935,6 +2946,7 @@ async function showPlanDetail(zentaosid, selected) {
2935
2946
  console.log(
2936
2947
  chalk.blue("提示: ID 供 fe-it-beta story detail <需求ID> 使用(也可传需求url)")
2937
2948
  );
2949
+ return stories;
2938
2950
  }
2939
2951
 
2940
2952
  // 导出动作(plan收尾交互使用): 按范围过滤需求, 拉详情并产出 .zentao-<计划名>/ + 工作区
@@ -2990,16 +3002,23 @@ async function exportPlanStories(zentaosid, selected, scope) {
2990
3002
  Loading.succeed(`拉取需求详情完成(${done}/${stories.length})`);
2991
3003
  // 需求目录: .zentao-<计划名>(多计划互不混目录)
2992
3004
  const zentaoDir = path.join(process.cwd(), zentaoDirNameOf(plan));
2993
- // 同名taskDir直接删除重导(重拉=全新快照, 连带清掉旧images与旧plan/codex报告; 失败条目保留旧数据)
3005
+ // 同名taskDir手术式刷新: 只删task.md与images/重写, 保留plan.md/codex报告等AI产物; 失败条目保留旧数据
2994
3006
  stories.forEach((story) => {
2995
3007
  if (failed.has(String(story.id))) return;
2996
3008
  const dir = path.join(zentaoDir, "tasks", taskDirOf(story));
2997
3009
  if (isExistPath(dir)) {
2998
- fs.rmSync(dir, { recursive: true, force: true });
2999
- console.log(chalk.yellow(`同名任务目录已删除重导: ${taskDirOf(story)}`));
3010
+ const taskMd = path.join(dir, "task.md");
3011
+ if (isExistPath(taskMd)) fs.rmSync(taskMd, { force: true });
3012
+ const imagesDir = path.join(dir, "images");
3013
+ if (isExistPath(imagesDir)) {
3014
+ fs.rmSync(imagesDir, { recursive: true, force: true });
3015
+ }
3016
+ console.log(
3017
+ chalk.yellow(`同名任务目录已刷新(保留plan/报告): ${taskDirOf(story)}`)
3018
+ );
3000
3019
  }
3001
3020
  });
3002
- // 图片本地化到 tasks/<taskDir>/images/(目录已删全量重下, 失败黄字不阻塞, 引用降级远程url)
3021
+ // 图片本地化到 tasks/<taskDir>/images/(images/已删全量重下, 失败黄字不阻塞, 引用降级远程url)
3003
3022
  const storyImages = await downloadStoryImages({
3004
3023
  stories,
3005
3024
  storyDetails,
@@ -3025,11 +3044,13 @@ async function exportPlanStories(zentaosid, selected, scope) {
3025
3044
  scope === "back" || scope === "all"
3026
3045
  ? collectPoolProjects(stories, "back")
3027
3046
  : [];
3028
- if (frontProjects.length || backProjects.length) {
3029
- writeCodeWorkspace(zentaoDir, plan.title, frontProjects, backProjects);
3030
- } else {
3031
- console.log(chalk.yellow("本范围无关联项目, 跳过工作区生成"));
3047
+ // 无pool时仍生成单目录工作区(只含需求目录自身, 打开仍能看manifest与task.md), 黄字提示补维护
3048
+ if (!frontProjects.length && !backProjects.length) {
3049
+ console.log(
3050
+ chalk.yellow("本范围需求均未维护poollist, 工作区仅含需求目录, 维护后重跑可补齐项目")
3051
+ );
3032
3052
  }
3053
+ writeCodeWorkspace(zentaoDir, plan.title, frontProjects, backProjects);
3033
3054
  }
3034
3055
 
3035
3056
  /**
@@ -3103,7 +3124,7 @@ async function storyMain(action, cliOpts) {
3103
3124
  if (!match) {
3104
3125
  console.log(
3105
3126
  chalk.red(
3106
- "请传入项目ID(从禅道项目需求页url复制), 如: fe-it-beta story task 2915 或 fe-it-beta story task https://zentao.hongxinshop.com/zentao/project-story-2915.html"
3127
+ "请传入项目-需求url的ID, 如: fe-it-beta story task 2915 或 fe-it-beta story task https://zentao.hongxinshop.com/zentao/project-story-2915.html"
3107
3128
  )
3108
3129
  );
3109
3130
  process.exit(1);
@@ -3703,7 +3724,7 @@ async function storyMain(action, cliOpts) {
3703
3724
  if (!match) {
3704
3725
  console.log(
3705
3726
  chalk.red(
3706
- "请传入项目ID(从禅道任务列表url复制), 如: fe-it-beta story close 2915 或 fe-it-beta story close https://zentao.hongxinshop.com/zentao/project-task-2915-myinvolved.html"
3727
+ "请传入项目-任务-由我参与url的ID, 如: fe-it-beta story close 2915 或 fe-it-beta story close https://zentao.hongxinshop.com/zentao/project-task-2915-myinvolved.html"
3707
3728
  )
3708
3729
  );
3709
3730
  process.exit(1);
@@ -3975,10 +3996,26 @@ async function storyMain(action, cliOpts) {
3975
3996
  // plan: 产品选择 -> 计划选取 -> 展示 -> 导出(--export直通一键场景, 不传则收尾交互)
3976
3997
  const session = await ensureZentaoSession(cliOpts);
3977
3998
  await session.request(async (zentaosid) => {
3978
- const product = await selectProduct(zentaosid, cliOpts);
3999
+ // 位置参数纯数字 = 产品计划ID(productplan-browse-<ID>.html 中的ID), 等同 --product 直通
4000
+ const nameArg = String(cliOpts.name || "").trim();
4001
+ const productIdArg = /^\d+$/.test(nameArg) ? nameArg : "";
4002
+ const product = await selectProduct(zentaosid, {
4003
+ ...cliOpts,
4004
+ product: cliOpts.product || productIdArg,
4005
+ });
3979
4006
  const plans = await loadPlanList(zentaosid, product.id);
3980
- const selected = await selectPlan(plans, cliOpts.name);
3981
- await showPlanDetail(zentaosid, selected);
4007
+ // 该产品无计划时后续选取/详情/导出均无意义, 提前结束
4008
+ if (!plans.length) {
4009
+ console.log(chalk.yellow(`产品 #${product.id} ${product.name} 下无计划, 结束`));
4010
+ return;
4011
+ }
4012
+ // 纯数字已用作产品ID, 非数字关键词仍按计划标题过滤
4013
+ const selected = await selectPlan(plans, productIdArg ? "" : cliOpts.name);
4014
+ const stories = await showPlanDetail(zentaosid, selected);
4015
+ // 空计划提示已在showPlanDetail输出, 直接结束不进导出询问
4016
+ if (!stories.length) {
4017
+ return;
4018
+ }
3982
4019
  // 导出范围: --export参数直通(一键场景) > 收尾交互(默认前端)
3983
4020
  let exportChoice;
3984
4021
  if (cliOpts.export !== undefined) {
@@ -3987,14 +4024,14 @@ async function storyMain(action, cliOpts) {
3987
4024
  } else {
3988
4025
  ({ exportChoice } = await inquirer.prompt([
3989
4026
  {
3990
- message: "是否导出需求文件(按计划生成 .zentao-<计划名>目录)?",
4027
+ message: `是否导出需求文件(按计划生成 .zentao-<计划名>目录, ${chalk.red("目前导出功能只适用于claude code批量建任务")})?`,
3991
4028
  name: "exportChoice",
3992
4029
  type: "list",
3993
4030
  choices: [
3994
- { name: "导出: 前端需求", value: "front" }, // 默认第一项
4031
+ { name: "", value: "none" }, // 默认第一项
4032
+ { name: "导出: 前端需求", value: "front" },
3995
4033
  { name: "导出: 后端需求", value: "back" },
3996
4034
  { name: "导出: 全部需求", value: "all" },
3997
- { name: "否", value: "none" },
3998
4035
  ],
3999
4036
  },
4000
4037
  ]));
@@ -4020,7 +4057,7 @@ function registerStoryCommand(program, { markHandled, addHelpExample }) {
4020
4057
  .option("--username <name>", "禅道账号(story命令用)")
4021
4058
  .option("--password <pwd>", "禅道密码(story命令用, 日常建议用--cache)")
4022
4059
  .option("--cache", "凭据静默走缓存")
4023
- .option("--product <id>", "禅道产品ID(plan用, 不传则交互选择)")
4060
+ .option("--product <id>", "禅道产品计划ID(plan用, 即productplan-browse-<ID>.html中的ID, 不传则交互选择)")
4024
4061
  .option("--export <scope>", "plan导出范围: front|back|all(传了跳过收尾询问直接导出)")
4025
4062
  .option("--type <t>", "task任务类型: front|back(传了跳过类型询问与确认, 一键创建)")
4026
4063
  .option("--split", "task时创建父任务并自动拆分子任务(>8h, 每个8h末个补余); subtask时跳过确认直接拆")
@@ -4039,6 +4076,7 @@ function registerStoryCommand(program, { markHandled, addHelpExample }) {
4039
4076
  // help示例经 helpers 注入 index.js 的 helpExamples, 自动参与列对齐
4040
4077
  addHelpExample("fe-it-beta story login", "禅道登录, 缓存zentaosid");
4041
4078
  addHelpExample("fe-it-beta story plan --product 87", "选计划看列表, 交互选导出");
4079
+ addHelpExample("fe-it-beta story plan 29", "29=产品计划ID(productplan-browse-29.html), 拉计划列表再选取");
4042
4080
  addHelpExample("fe-it-beta story detail 需求ID", "按需求ID查看详情");
4043
4081
  addHelpExample("fe-it-beta story task 项目ID", "选项目勾选需求, 交互建禅道任务");
4044
4082
  addHelpExample(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i18-fe-automator-beta",
3
- "version": "2.1.5",
3
+ "version": "2.1.7",
4
4
  "description": "i18-fe-automator-beta内测版本,只用于key: ''替换、检测中文规则等",
5
5
  "type": "module",
6
6
  "bin": {