weaver-work-cli 0.1.20 → 0.1.22

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 (77) hide show
  1. package/README.md +19 -11
  2. package/dist/core/refs.js +39 -36
  3. package/dist/internal/e10/auth/commands.js +33 -18
  4. package/dist/internal/e10/auth/session.js +60 -2
  5. package/dist/internal/e10/auth/xiaoe.js +253 -53
  6. package/dist/internal/e10/context.js +18 -6
  7. package/dist/internal/e10/request-runtime.js +3 -1
  8. package/dist/shortcuts/caizhilian/operations/confirm.js +1 -1
  9. package/dist/shortcuts/caizhilian/operations/handlers/file-collection.js +901 -20
  10. package/dist/shortcuts/caizhilian/operations/handlers/folder.js +55 -0
  11. package/dist/shortcuts/caizhilian/operations/handlers/setting-attach.js +4 -4
  12. package/dist/shortcuts/caizhilian/operations/specs.js +75 -0
  13. package/dist/shortcuts/hrm/manifest.js +16 -9
  14. package/dist/shortcuts/hrm/operations/registry.js +2 -0
  15. package/dist/shortcuts/hrm/operations/resolve.js +449 -0
  16. package/dist/shortcuts/invoice/host.js +5 -4
  17. package/dist/shortcuts/meeting/operations/eb/association.js +151 -0
  18. package/dist/shortcuts/meeting/operations/eb/attendance-summary.js +153 -0
  19. package/dist/shortcuts/meeting/operations/eb/catalog.js +185 -13
  20. package/dist/shortcuts/meeting/operations/eb/conflict.js +17 -11
  21. package/dist/shortcuts/meeting/operations/eb/decision.js +385 -0
  22. package/dist/shortcuts/meeting/operations/eb/factory.js +222 -35
  23. package/dist/shortcuts/meeting/operations/eb/index.js +18 -1
  24. package/dist/shortcuts/meeting/operations/eb/link.js +212 -0
  25. package/dist/shortcuts/meeting/operations/eb/task-progress.js +123 -0
  26. package/dist/shortcuts/meeting/operations/env.js +78 -61
  27. package/dist/shortcuts/meeting/operations/link.js +125 -0
  28. package/dist/shortcuts/meeting/operations/registry.js +2 -0
  29. package/dist/shortcuts/meeting/operations/shared.js +13 -0
  30. package/dist/shortcuts/workflow/host.js +5 -1
  31. package/dist/shortcuts/workflow/operations/form.js +95 -11
  32. package/dist/shortcuts/workflow/operations/read.js +785 -26
  33. package/docs/e10-auth.md +8 -2
  34. package/docs/hrm.md +34 -2
  35. package/docs/invoice.md +3 -0
  36. package/package.json +2 -2
  37. package/scripts/xiaoe_env.py +118 -0
  38. package/skill-template/domains/hrm.md +2 -1
  39. package/skill-template/skill-template.md +1 -1
  40. package/skills/weaver-e10-blog/SKILL.md +1 -1
  41. package/skills/weaver-e10-caizhilian/SKILL.md +5 -4
  42. package/skills/weaver-e10-caizhilian/references/file-collection.md +37 -0
  43. package/skills/weaver-e10-caizhilian/references/folder.md +5 -1
  44. package/skills/weaver-e10-caizhilian/references/setting-attach.md +35 -5
  45. package/skills/weaver-e10-caizhilian/references/source-manifest.json +620 -5
  46. package/skills/weaver-e10-calendar/SKILL.md +1 -1
  47. package/skills/weaver-e10-ebuilder-form/SKILL.md +1 -1
  48. package/skills/weaver-e10-esb/SKILL.md +1 -1
  49. package/skills/weaver-e10-hrm/SKILL.md +52 -5
  50. package/skills/weaver-e10-hrm/references/org.md +107 -1
  51. package/skills/weaver-e10-jinchengda/SKILL.md +2 -1
  52. package/skills/weaver-e10-jiuchuanhui/SKILL.md +1 -1
  53. package/skills/weaver-e10-jucailin/SKILL.md +1 -1
  54. package/skills/weaver-e10-mail/SKILL.md +1 -1
  55. package/skills/weaver-e10-meeting/SKILL.md +27 -11
  56. package/skills/weaver-e10-meeting/references/meeting-eb.md +162 -7
  57. package/skills/weaver-e10-meeting/references/meeting-verification.md +271 -0
  58. package/skills/weaver-e10-meeting.zip +0 -0
  59. package/skills/weaver-e10-okr/SKILL.md +1 -1
  60. package/skills/weaver-e10-okr/references/source-manifest.json +3 -3
  61. package/skills/weaver-e10-plan/SKILL.md +1 -1
  62. package/skills/weaver-e10-qiyecheng/SKILL.md +1 -1
  63. package/skills/weaver-e10-shared/SKILL.md +10 -13
  64. package/skills/weaver-e10-shared/references/e10-auth-and-session.md +11 -17
  65. package/skills/weaver-e10-shared/references/non-interactive-environment.md +21 -14
  66. package/skills/weaver-e10-shijingran/SKILL.md +1 -1
  67. package/skills/weaver-e10-skill-maker/SKILL.md +1 -1
  68. package/skills/weaver-e10-skill-maker/references/weaver-skill-style.md +1 -1
  69. package/skills/weaver-e10-wenshuding/SKILL.md +1 -1
  70. package/skills/weaver-e10-workflow/SKILL.md +19 -19
  71. package/skills/weaver-e10-workflow/references/source-manifest.json +13 -13
  72. package/skills/weaver-e10-workflow/references/workflow-create.md +11 -7
  73. package/skills/weaver-e10-workflow/references/workflow-query.md +9 -14
  74. package/skills/weaver-e10-yepiaotong/SKILL.md +1 -2
  75. package/skills/weaver-e10-yimiaoban/SKILL.md +1 -1
  76. package/skills/weaver-e10-yimiaoban/references/source-manifest.json +3 -3
  77. package/skills/weaver-e10-ziguanjia/SKILL.md +1 -1
@@ -1,16 +1,19 @@
1
1
  /**
2
2
  * caizhilian.file-collection — 文件采集(FTP/SFTP)
3
3
  *
4
- * 覆盖文件采集的生命周期:
5
- * - 任务列表/详情/删除
6
- * - 扫描/运行/取消
7
- * - 进度查询/结果查询
4
+ * 覆盖文件采集的完整生命周期:
5
+ * - 任务列表/详情/保存/删除/另存为/开关切换
6
+ * - 全宗 ID 更新(归档分支)
7
+ * - 连接器查询/创建、FTP 资源创建
8
+ * - 动作流搜索/检测/创建/启用/绑定
9
+ * - 扫描/运行/取消、等待完成、结果归一化
8
10
  *
9
11
  * 接口:
10
12
  * - 列任务:GET /api/file/coll/taskTable
11
13
  * - 读表单:GET /api/file/coll/taskForm
12
14
  * - 保存任务:POST /api/file/coll/save
13
15
  * - 另存为:POST /api/file/coll/saveAs
16
+ * - 切换开关:POST /api/file/coll/switch
14
17
  * - 检查存储:GET /api/file/coll/checkStorageConnect
15
18
  * - 扫描:POST /api/file/coll/scan
16
19
  * - 扫描进度:GET /api/file/coll/scan/progress
@@ -19,10 +22,23 @@
19
22
  * - 详情:GET /api/file/coll/detailTable
20
23
  * - 取消:POST /api/file/coll/cancel
21
24
  * - 删除:POST /api/file/coll/delete
25
+ * - 连接器列表:GET /api/bs/esb/setting/connector/getConnectorListPage
26
+ * - 资源列表:GET /api/bs/esb/setting/resource/getResourceListPage
27
+ * - 创建连接器:POST /api/bs/esb/setting/systemPool/save
28
+ * - 创建 FTP 资源:POST /api/bs/esb/setting/resource/ftp/save
29
+ * - 动作流列表:POST /api/bs/esb/setting/application/getApplicationListPage
30
+ * - 动作流详情:GET /api/bs/esb/setting/application/getAppById/{id}
31
+ * - 动作流版本:GET /api/bs/esb/setting/application/version/versionList/{id}
32
+ * - 动作流设计:GET /api/bs/esb/setting/flow/design/getDesignBySceneId/{id}
33
+ * - 创建动作流:POST /api/bs/esb/setting/application/save
34
+ * - 注册变量:POST /api/bs/esb/setting/application/variable/addVariable
35
+ * - 校验设计:POST /api/bs/esb/setting/flow/design/check
36
+ * - 保存设计:POST /api/bs/esb/setting/flow/design/saveDesign
37
+ * - 启用动作流:GET /api/bs/esb/setting/application/startUseApp/{id}
22
38
  */
23
39
  import { CaizhilianCliError } from '../../errors.js';
24
40
  import { caizhilianSuccess } from '../types.js';
25
- import { requireId } from '../shared.js';
41
+ import { requireId, requireNonEmpty } from '../shared.js';
26
42
  const ACTION = 'caizhilian.file-collection';
27
43
  // ── 接口路径 ──────────────────────────────────────────────
28
44
  const LIST_TASKS_PATH = '/api/file/coll/taskTable';
@@ -37,6 +53,21 @@ const RUN_PROGRESS_PATH = '/api/file/coll/run/progress';
37
53
  const DETAIL_TABLE_PATH = '/api/file/coll/detailTable';
38
54
  const CANCEL_PATH = '/api/file/coll/cancel';
39
55
  const DELETE_PATH = '/api/file/coll/delete';
56
+ const SWITCH_PATH = '/api/file/coll/switch';
57
+ const SAVE_AS_PATH = '/api/file/coll/saveAs';
58
+ const CONNECTOR_LIST_PATH = '/api/bs/esb/setting/connector/getConnectorListPage';
59
+ const RESOURCE_LIST_PATH = '/api/bs/esb/setting/resource/getResourceListPage';
60
+ const SYSTEM_POOL_SAVE_PATH = '/api/bs/esb/setting/systemPool/save';
61
+ const FTP_RESOURCE_SAVE_PATH = '/api/bs/esb/setting/resource/ftp/save';
62
+ const APP_LIST_PATH = '/api/bs/esb/setting/application/getApplicationListPage';
63
+ const APP_DETAIL_PATH = '/api/bs/esb/setting/application/getAppById';
64
+ const APP_VERSION_PATH = '/api/bs/esb/setting/application/version/versionList';
65
+ const DESIGN_BY_SCENE_PATH = '/api/bs/esb/setting/flow/design/getDesignBySceneId';
66
+ const APP_SAVE_PATH = '/api/bs/esb/setting/application/save';
67
+ const APP_VARIABLE_PATH = '/api/bs/esb/setting/application/variable/addVariable';
68
+ const DESIGN_CHECK_PATH = '/api/bs/esb/setting/flow/design/check';
69
+ const DESIGN_SAVE_PATH = '/api/bs/esb/setting/flow/design/saveDesign';
70
+ const APP_ENABLE_PATH = '/api/bs/esb/setting/application/startUseApp';
40
71
  // ── 辅助函数 ──────────────────────────────────────────────
41
72
  function asRecord(value) {
42
73
  return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
@@ -113,11 +144,11 @@ async function checkStorage(args, env) {
113
144
  async function scanPrepare(args, env) {
114
145
  const taskId = requireId(args.task_id, 'task_id');
115
146
  const fullColl = args.full_coll ?? false;
116
- // 签发 continuation
147
+ // 签发 continuation(键名与 targetKeys 保持一致,均为 snake_case)
117
148
  const continuation = await env.continuation.issue({
118
149
  operation: 'caizhilian.file-collection.scan.apply',
119
- taskId,
120
- fullColl,
150
+ task_id: taskId,
151
+ full_coll: fullColl,
121
152
  });
122
153
  return caizhilianSuccess('caizhilian.file-collection.scan.prepare', {
123
154
  task_id: taskId,
@@ -126,8 +157,8 @@ async function scanPrepare(args, env) {
126
157
  });
127
158
  }
128
159
  async function scanApply(args, env, lease) {
129
- const taskId = lease.payload?.taskId;
130
- const fullColl = lease.payload?.fullColl;
160
+ const taskId = lease.task_id;
161
+ const fullColl = lease.full_coll;
131
162
  const response = await env.host.request({
132
163
  method: 'POST',
133
164
  path: SCAN_PATH,
@@ -172,10 +203,10 @@ async function scanProgress(args, env) {
172
203
  async function runPrepare(args, env) {
173
204
  const taskId = requireId(args.task_id, 'task_id');
174
205
  const action = args.action || 'run';
175
- // 签发 continuation
206
+ // 签发 continuation(键名与 targetKeys 保持一致,均为 snake_case)
176
207
  const continuation = await env.continuation.issue({
177
208
  operation: 'caizhilian.file-collection.run.apply',
178
- taskId,
209
+ task_id: taskId,
179
210
  action,
180
211
  });
181
212
  return caizhilianSuccess('caizhilian.file-collection.run.prepare', {
@@ -185,8 +216,8 @@ async function runPrepare(args, env) {
185
216
  });
186
217
  }
187
218
  async function runApply(args, env, lease) {
188
- const taskId = lease.payload?.taskId;
189
- const action = lease.payload?.action;
219
+ const taskId = lease.task_id;
220
+ const action = lease.action;
190
221
  const response = await env.host.request({
191
222
  method: 'POST',
192
223
  path: RUN_PATH,
@@ -250,10 +281,10 @@ async function detailTable(args, env) {
250
281
  // ── 取消采集(prepare/apply)──────────────────────────────────────
251
282
  async function cancelPrepare(args, env) {
252
283
  const taskId = requireId(args.task_id, 'task_id');
253
- // 签发 continuation
284
+ // 签发 continuation(键名与 targetKeys 保持一致,均为 snake_case)
254
285
  const continuation = await env.continuation.issue({
255
286
  operation: 'caizhilian.file-collection.cancel.apply',
256
- taskId,
287
+ task_id: taskId,
257
288
  });
258
289
  return caizhilianSuccess('caizhilian.file-collection.cancel.prepare', {
259
290
  task_id: taskId,
@@ -261,7 +292,7 @@ async function cancelPrepare(args, env) {
261
292
  });
262
293
  }
263
294
  async function cancelApply(args, env, lease) {
264
- const taskId = lease.payload?.taskId;
295
+ const taskId = lease.task_id;
265
296
  const response = await env.host.request({
266
297
  method: 'POST',
267
298
  path: CANCEL_PATH,
@@ -284,10 +315,10 @@ async function cancelApply(args, env, lease) {
284
315
  // ── 删除采集任务(prepare/apply)──────────────────────────────────────
285
316
  async function deleteTaskPrepare(args, env) {
286
317
  const taskId = requireId(args.task_id, 'task_id');
287
- // 签发 continuation
318
+ // 签发 continuation(键名与 targetKeys 保持一致,均为 snake_case)
288
319
  const continuation = await env.continuation.issue({
289
320
  operation: 'caizhilian.file-collection.delete.apply',
290
- taskId,
321
+ task_id: taskId,
291
322
  });
292
323
  return caizhilianSuccess('caizhilian.file-collection.delete.prepare', {
293
324
  task_id: taskId,
@@ -295,7 +326,7 @@ async function deleteTaskPrepare(args, env) {
295
326
  });
296
327
  }
297
328
  async function deleteTaskApply(args, env, lease) {
298
- const taskId = lease.payload?.taskId;
329
+ const taskId = lease.task_id;
299
330
  const response = await env.host.request({
300
331
  method: 'POST',
301
332
  path: DELETE_PATH,
@@ -315,10 +346,813 @@ async function deleteTaskApply(args, env, lease) {
315
346
  task_id: taskId,
316
347
  });
317
348
  }
349
+ // ── 保存采集任务(prepare/apply)──────────────────────────────────────
350
+ async function saveTaskPrepare(args, env) {
351
+ const sourceId = requireNonEmpty(args.source_id, 'source_id');
352
+ const workflowId = requireNonEmpty(args.workflow_id, 'workflow_id');
353
+ const taskName = args.task_name || '';
354
+ const retainDirStruct = args.retain_dir_struct ?? true;
355
+ const taskFilePath = args.task_file_path || '/';
356
+ // 签发 continuation(键名与 targetKeys 保持一致,均为 snake_case)
357
+ const continuation = await env.continuation.issue({
358
+ operation: 'caizhilian.file-collection.task.save.apply',
359
+ source_id: sourceId,
360
+ workflow_id: workflowId,
361
+ task_name: taskName,
362
+ retain_dir_struct: retainDirStruct,
363
+ task_file_path: taskFilePath,
364
+ });
365
+ return caizhilianSuccess('caizhilian.file-collection.task.save.prepare', {
366
+ source_id: sourceId,
367
+ workflow_id: workflowId,
368
+ task_name: taskName,
369
+ retain_dir_struct: retainDirStruct,
370
+ task_file_path: taskFilePath,
371
+ continuation,
372
+ });
373
+ }
374
+ async function saveTaskApply(args, env, lease) {
375
+ const sourceId = lease.source_id;
376
+ const workflowId = lease.workflow_id;
377
+ const taskName = lease.task_name;
378
+ const retainDirStruct = lease.retain_dir_struct;
379
+ const taskFilePath = lease.task_file_path;
380
+ const payload = {
381
+ taskStatus: '0',
382
+ taskName: taskName || `文件采集(${new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }).replace(/[/:]/g, '').replace(' ', ' ').substring(0, 15)})`,
383
+ taskSourceId: sourceId,
384
+ taskSourceType: 'ftp',
385
+ taskWorkflowId: workflowId,
386
+ taskFilePath: taskFilePath || '/',
387
+ retainDirStruct: retainDirStruct,
388
+ taskMethod: '1',
389
+ deleteType: '0',
390
+ taskSource: sourceId,
391
+ };
392
+ const response = await env.host.request({
393
+ method: 'POST',
394
+ path: SAVE_TASK_PATH,
395
+ body: payload,
396
+ operation: 'caizhilian.file-collection.task.save.apply',
397
+ });
398
+ const data = asRecord(response);
399
+ if (!data.status && !data.success) {
400
+ throw new CaizhilianCliError('api', 'business_error', String(data.msg || data.message || '保存采集任务失败'), {
401
+ code: data.code ?? data.status,
402
+ details: { action: 'caizhilian.file-collection.task.save.apply', message: String(data.msg || data.message || '保存采集任务失败') },
403
+ exitCode: 1,
404
+ });
405
+ }
406
+ const result = asRecord(data.data);
407
+ return caizhilianSuccess('caizhilian.file-collection.task.save.apply', {
408
+ task_id: result.id || '',
409
+ task_name: taskName,
410
+ source_id: sourceId,
411
+ workflow_id: workflowId,
412
+ });
413
+ }
414
+ // ── 切换任务启用/禁用(prepare/apply)────────────────────────
415
+ async function switchTaskPrepare(args, env) {
416
+ const taskId = requireId(args.task_id, 'task_id');
417
+ const taskStatus = args.task_status;
418
+ const taskSwitch = args.task_switch;
419
+ const continuation = await env.continuation.issue({
420
+ operation: 'caizhilian.file-collection.task.switch.apply',
421
+ task_id: taskId, task_status: taskStatus, task_switch: taskSwitch,
422
+ });
423
+ return caizhilianSuccess('caizhilian.file-collection.task.switch.prepare', {
424
+ task_id: taskId, task_status: taskStatus, task_switch: taskSwitch, continuation,
425
+ });
426
+ }
427
+ async function switchTaskApply(args, env, lease) {
428
+ const taskId = lease.task_id;
429
+ const taskStatus = lease.task_status;
430
+ const taskSwitch = lease.task_switch;
431
+ const response = await env.host.request({
432
+ method: 'POST',
433
+ path: SWITCH_PATH,
434
+ body: { id: taskId, taskStatus, taskSwitch },
435
+ operation: 'caizhilian.file-collection.task.switch.apply',
436
+ });
437
+ const data = asRecord(response);
438
+ if (!data.status && !data.success) {
439
+ throw new CaizhilianCliError('api', 'business_error', String(data.msg || data.message || '切换任务状态失败'), {
440
+ code: data.code ?? data.status,
441
+ details: { action: 'caizhilian.file-collection.task.switch.apply', message: String(data.msg || data.message || '切换任务状态失败') },
442
+ exitCode: 1,
443
+ });
444
+ }
445
+ return caizhilianSuccess('caizhilian.file-collection.task.switch.apply', {
446
+ task_id: taskId, task_status: taskStatus, task_switch: taskSwitch,
447
+ });
448
+ }
449
+ // ── 另存为采集任务(prepare/apply)────────────────────────
450
+ async function saveAsTaskPrepare(args, env) {
451
+ const taskId = requireId(args.task_id, 'task_id');
452
+ const continuation = await env.continuation.issue({
453
+ operation: 'caizhilian.file-collection.task.save-as.apply',
454
+ task_id: taskId,
455
+ });
456
+ return caizhilianSuccess('caizhilian.file-collection.task.save-as.prepare', {
457
+ task_id: taskId, continuation,
458
+ });
459
+ }
460
+ async function saveAsTaskApply(args, env, lease) {
461
+ const taskId = lease.task_id;
462
+ const response = await env.host.request({
463
+ method: 'POST',
464
+ path: SAVE_AS_PATH,
465
+ body: { id: taskId },
466
+ operation: 'caizhilian.file-collection.task.save-as.apply',
467
+ });
468
+ const data = asRecord(response);
469
+ if (!data.status && !data.success) {
470
+ throw new CaizhilianCliError('api', 'business_error', String(data.msg || data.message || '另存为失败'), {
471
+ code: data.code ?? data.status,
472
+ details: { action: 'caizhilian.file-collection.task.save-as.apply', message: String(data.msg || data.message || '另存为失败') },
473
+ exitCode: 1,
474
+ });
475
+ }
476
+ const result = asRecord(data.data);
477
+ return caizhilianSuccess('caizhilian.file-collection.task.save-as.apply', {
478
+ new_task_id: result.id || '',
479
+ source_task_id: taskId,
480
+ });
481
+ }
482
+ // ── 更新任务全宗 ID(prepare/apply)────────────────────────
483
+ async function updateFondsPrepare(args, env) {
484
+ const taskId = requireId(args.task_id, 'task_id');
485
+ const fondsId = requireId(args.fonds_id, 'fonds_id');
486
+ const continuation = await env.continuation.issue({
487
+ operation: 'caizhilian.file-collection.task.update-fonds.apply',
488
+ task_id: taskId, fonds_id: fondsId,
489
+ });
490
+ return caizhilianSuccess('caizhilian.file-collection.task.update-fonds.prepare', {
491
+ task_id: taskId, fonds_id: fondsId, continuation,
492
+ });
493
+ }
494
+ async function updateFondsApply(args, env, lease) {
495
+ const taskId = lease.task_id;
496
+ const fondsId = lease.fonds_id;
497
+ // 先读取当前表单
498
+ const formResponse = await env.host.request({
499
+ method: 'GET',
500
+ path: `${GET_TASK_FORM_PATH}?id=${taskId}`,
501
+ operation: 'caizhilian.file-collection.task.update-fonds.apply',
502
+ });
503
+ const formData = asRecord(formResponse);
504
+ const form = asRecord(formData.data);
505
+ // 组装更新 payload:保留表单字段,覆盖 fondsId
506
+ const payload = { ...form };
507
+ payload.fondsId = fondsId;
508
+ delete payload.fontId;
509
+ payload.taskType = 3;
510
+ // 处理 extParam 中的 fondsId
511
+ if (payload.extParam) {
512
+ try {
513
+ const extParam = JSON.parse(payload.extParam);
514
+ extParam.fondsId = fondsId;
515
+ payload.extParam = JSON.stringify(extParam);
516
+ }
517
+ catch { /* extParam 非 JSON 时忽略 */ }
518
+ }
519
+ const response = await env.host.request({
520
+ method: 'POST',
521
+ path: SAVE_TASK_PATH,
522
+ body: payload,
523
+ operation: 'caizhilian.file-collection.task.update-fonds.apply',
524
+ });
525
+ const data = asRecord(response);
526
+ if (!data.status && !data.success) {
527
+ throw new CaizhilianCliError('api', 'business_error', String(data.msg || data.message || '更新全宗失败'), {
528
+ code: data.code ?? data.status,
529
+ details: { action: 'caizhilian.file-collection.task.update-fonds.apply', message: String(data.msg || data.message || '更新全宗失败') },
530
+ exitCode: 1,
531
+ });
532
+ }
533
+ return caizhilianSuccess('caizhilian.file-collection.task.update-fonds.apply', {
534
+ task_id: taskId, fonds_id: fondsId,
535
+ });
536
+ }
537
+ // ── 查询连接器和资源 ─────────────────────────────────────
538
+ async function queryConnectors(args, env) {
539
+ const nameFilter = args.name || '';
540
+ const protocolType = args.protocol_type || 'nas,ftp,sftp';
541
+ const pageSize = args.page_size ?? 20;
542
+ const pageNo = args.page_no ?? 1;
543
+ const maxPages = args.max_pages ?? 5;
544
+ const allConnectors = [];
545
+ for (let page = 1; page <= maxPages; page++) {
546
+ const response = await env.host.request({
547
+ method: 'GET',
548
+ path: `${CONNECTOR_LIST_PATH}?pageSize=${pageSize}&current=${page}&protocolType=${protocolType}${nameFilter ? `&name=${encodeURIComponent(nameFilter)}` : ''}`,
549
+ operation: ACTION,
550
+ });
551
+ const data = asRecord(response);
552
+ const result = asRecord(data.data);
553
+ const records = asList(result.data || result.records || result);
554
+ if (records.length === 0)
555
+ break;
556
+ allConnectors.push(...records);
557
+ if (records.length < pageSize)
558
+ break;
559
+ }
560
+ // 只保留 FTP 类型的连接器
561
+ const ftpConnectors = allConnectors.filter((c) => c.product === 'FTP');
562
+ // 查询每个连接器的资源
563
+ const enriched = [];
564
+ for (const connector of ftpConnectors) {
565
+ const resourceResponse = await env.host.request({
566
+ method: 'GET',
567
+ path: `${RESOURCE_LIST_PATH}?systemPk=${connector.id}&showDefalt=true&connectorType=ftp&current=1&pageSize=100`,
568
+ operation: ACTION,
569
+ });
570
+ const resourceData = asRecord(resourceResponse);
571
+ const resourceResult = asRecord(resourceData.data);
572
+ const resources = asList(resourceResult.data || resourceResult.records || resourceResult);
573
+ enriched.push({
574
+ id: connector.id,
575
+ name: connector.name || '',
576
+ product: connector.product || '',
577
+ resources: resources.map((r) => ({
578
+ id: r.id,
579
+ name: r.name || '',
580
+ type: r.type || '',
581
+ status: r.status ?? '',
582
+ link_address: r.linkAddressSpan || r.linkAddress || '',
583
+ })),
584
+ });
585
+ }
586
+ return caizhilianSuccess(ACTION, {
587
+ ftp_connector_total: enriched.length,
588
+ ftp_connectors: enriched,
589
+ });
590
+ }
591
+ // ── 创建连接器(prepare/apply)────────────────────────
592
+ async function createConnectorPrepare(args, env) {
593
+ const name = args.name || `文件采集FTP连接器(${new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }).replace(/[/:]/g, '').replace(' ', ' ').substring(0, 15)})`;
594
+ const company = args.company || '文件采集';
595
+ const continuation = await env.continuation.issue({
596
+ operation: 'caizhilian.file-collection.connector.create.apply',
597
+ name, company,
598
+ });
599
+ return caizhilianSuccess('caizhilian.file-collection.connector.create.prepare', {
600
+ name, company, continuation,
601
+ });
602
+ }
603
+ async function createConnectorApply(args, env, lease) {
604
+ const name = lease.name;
605
+ const company = lease.company;
606
+ const response = await env.host.request({
607
+ method: 'POST',
608
+ path: SYSTEM_POOL_SAVE_PATH,
609
+ body: {
610
+ name, company,
611
+ website: '', product: 'FTP', productWebsite: '', edition: '',
612
+ description: '', icon: '', id: '', connectorType: 'ftp',
613
+ },
614
+ operation: 'caizhilian.file-collection.connector.create.apply',
615
+ });
616
+ const data = asRecord(response);
617
+ if (!data.status && !data.success) {
618
+ throw new CaizhilianCliError('api', 'business_error', String(data.msg || data.message || '创建连接器失败'), {
619
+ code: data.code ?? data.status,
620
+ details: { action: 'caizhilian.file-collection.connector.create.apply', message: String(data.msg || data.message || '创建连接器失败') },
621
+ exitCode: 1,
622
+ });
623
+ }
624
+ const result = asRecord(data.data);
625
+ return caizhilianSuccess('caizhilian.file-collection.connector.create.apply', {
626
+ id: result.id || result.systemPk || '',
627
+ name,
628
+ });
629
+ }
630
+ // ── 创建 FTP/SFTP 资源(prepare/apply)────────────────────────
631
+ async function createResourcePrepare(args, env) {
632
+ const remoteHost = requireNonEmpty(args.remote_host, 'remote_host');
633
+ const remotePort = requireNonEmpty(args.remote_port, 'remote_port');
634
+ const remoteUsername = requireNonEmpty(args.remote_username, 'remote_username');
635
+ const remotePassword = requireNonEmpty(args.remote_password, 'remote_password');
636
+ const transferProtocol = requireNonEmpty(args.transfer_protocol, 'transfer_protocol');
637
+ const systemPk = args.system_pk || '';
638
+ const name = args.name || '文件采集FTP连接';
639
+ const continuation = await env.continuation.issue({
640
+ operation: 'caizhilian.file-collection.resource.create.apply',
641
+ system_pk: systemPk, name, remote_host: remoteHost, remote_port: remotePort,
642
+ remote_username: remoteUsername, remote_password: remotePassword, transfer_protocol: transferProtocol,
643
+ });
644
+ return caizhilianSuccess('caizhilian.file-collection.resource.create.prepare', {
645
+ system_pk: systemPk, name, remote_host: remoteHost, remote_port: remotePort,
646
+ remote_username: remoteUsername, transfer_protocol: transferProtocol, continuation,
647
+ });
648
+ }
649
+ async function createResourceApply(args, env, lease) {
650
+ let systemPk = lease.system_pk;
651
+ const name = lease.name;
652
+ const remoteHost = lease.remote_host;
653
+ const remotePort = lease.remote_port;
654
+ const remoteUsername = lease.remote_username;
655
+ const remotePassword = lease.remote_password;
656
+ const transferProtocol = lease.transfer_protocol;
657
+ let systemPoolCreated = false;
658
+ // 如果没有连接器 ID,先自动创建
659
+ if (!systemPk) {
660
+ const connResponse = await env.host.request({
661
+ method: 'POST',
662
+ path: SYSTEM_POOL_SAVE_PATH,
663
+ body: {
664
+ name: `文件采集FTP连接器(${new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }).replace(/[/:]/g, '').replace(' ', ' ').substring(0, 15)})`,
665
+ company: '文件采集',
666
+ website: '', product: 'FTP', productWebsite: '', edition: '',
667
+ description: '', icon: '', id: '', connectorType: 'ftp',
668
+ },
669
+ operation: 'caizhilian.file-collection.resource.create.apply',
670
+ });
671
+ const connData = asRecord(connResponse);
672
+ const connResult = asRecord(connData.data);
673
+ systemPk = connResult.id || connResult.systemPk || '';
674
+ systemPoolCreated = true;
675
+ }
676
+ const response = await env.host.request({
677
+ method: 'POST',
678
+ path: FTP_RESOURCE_SAVE_PATH,
679
+ body: {
680
+ encoding: 'UTF-8',
681
+ connectionMode: 2,
682
+ name,
683
+ remoteHost,
684
+ remotePort: String(remotePort),
685
+ remoteUsername,
686
+ remotePassword,
687
+ requestTimeout: '60',
688
+ responseTimeout: 60,
689
+ retryTime: '0',
690
+ systemPk,
691
+ transferMode: 2,
692
+ transferProtocol,
693
+ type: 'ftp',
694
+ },
695
+ operation: 'caizhilian.file-collection.resource.create.apply',
696
+ });
697
+ const data = asRecord(response);
698
+ if (!data.status && !data.success) {
699
+ throw new CaizhilianCliError('api', 'business_error', String(data.msg || data.message || '创建资源失败'), {
700
+ code: data.code ?? data.status,
701
+ details: { action: 'caizhilian.file-collection.resource.create.apply', message: String(data.msg || data.message || '创建资源失败') },
702
+ exitCode: 1,
703
+ });
704
+ }
705
+ const result = asRecord(data.data);
706
+ return caizhilianSuccess('caizhilian.file-collection.resource.create.apply', {
707
+ id: result.id || '',
708
+ name,
709
+ system_pk: systemPk,
710
+ system_pool_created: systemPoolCreated,
711
+ });
712
+ }
713
+ // ── 搜索动作流 ─────────────────────────────────────
714
+ async function searchWorkflows(args, env) {
715
+ const nameFilter = args.name_contains || '';
716
+ const pageSize = args.page_size ?? 50;
717
+ const pageNo = args.page_no ?? 1;
718
+ const response = await env.host.request({
719
+ method: 'POST',
720
+ path: APP_LIST_PATH,
721
+ body: {
722
+ current: pageNo, pageSize,
723
+ action: '', componentType: '', orgId: '',
724
+ createDates: [], creator: [], event: [],
725
+ type: [], updateDates: [], appIds: [],
726
+ skipId: '', thirdParty: '', appLevel: '', templateId: '',
727
+ name: '', status: '', moudle: 'esb',
728
+ isDelete: 0, orderType: '', orderStatus: '',
729
+ },
730
+ operation: ACTION,
731
+ });
732
+ const data = asRecord(response);
733
+ const result = asRecord(data.data);
734
+ let candidates = asList(result.data || result.records || result);
735
+ // 客户端过滤
736
+ if (nameFilter) {
737
+ const lower = nameFilter.toLowerCase();
738
+ candidates = candidates.filter((c) => (c.name || '').toLowerCase().includes(lower));
739
+ }
740
+ return caizhilianSuccess(ACTION, {
741
+ version: 'file_collection_action_flow_candidates.v1',
742
+ candidates: candidates.map((c, i) => ({
743
+ ordinal: i + 1,
744
+ id: c.id,
745
+ name: c.name || '',
746
+ status: c.status ?? '',
747
+ update_time: c.updateTime || '',
748
+ })),
749
+ candidate_count: candidates.length,
750
+ });
751
+ }
752
+ // ── 检测动作流兼容性 ─────────────────────────────────────
753
+ async function inspectWorkflow(args, env) {
754
+ const applicationId = requireId(args.application_id, 'application_id');
755
+ const archiveRequired = args.archive_required ?? false;
756
+ const retainDirStruct = args.retain_dir_struct ?? false;
757
+ // 1. 获取应用信息
758
+ const appResponse = await env.host.request({
759
+ method: 'GET',
760
+ path: `${APP_DETAIL_PATH}/${applicationId}`,
761
+ operation: ACTION,
762
+ });
763
+ const appData = asRecord(appResponse);
764
+ const app = asRecord(appData.data);
765
+ // 2. 获取版本列表
766
+ const versionResponse = await env.host.request({
767
+ method: 'GET',
768
+ path: `${APP_VERSION_PATH}/${applicationId}`,
769
+ operation: ACTION,
770
+ });
771
+ const versionData = asRecord(versionResponse);
772
+ const versions = asList(versionData.data);
773
+ // 找到活动版本
774
+ const activeVersion = versions.find((v) => String(v.activeVersion) === '1' || v.activeVersion === true) || versions[0];
775
+ if (!activeVersion) {
776
+ return caizhilianSuccess(ACTION, {
777
+ validation: { compatible: false, issues: ['未找到活动版本'] },
778
+ });
779
+ }
780
+ const sceneId = activeVersion.sceneId || '';
781
+ const secretKey = activeVersion.secretKey || '';
782
+ // 3. 获取设计物
783
+ let design = {};
784
+ if (sceneId) {
785
+ const designResponse = await env.host.request({
786
+ method: 'GET',
787
+ path: `${DESIGN_BY_SCENE_PATH}/${sceneId}`,
788
+ operation: ACTION,
789
+ });
790
+ const designData = asRecord(designResponse);
791
+ design = asRecord(designData.data);
792
+ }
793
+ // 4. 合规检测
794
+ const issues = [];
795
+ const triggers = asList(design.triggers || design.event);
796
+ const actions = asList(design.actions);
797
+ // 检测触发器
798
+ const customTrigger = triggers.find((t) => t.type === 'CustomTrigger' || t.componentType === 'CustomTrigger');
799
+ if (!customTrigger) {
800
+ issues.push('缺少 CustomTrigger 触发器');
801
+ }
802
+ else {
803
+ const inputs = asList(customTrigger.inputs || customTrigger.input);
804
+ const inputNames = inputs.map((i) => i.name || i.key || '');
805
+ if (!inputNames.includes('fileId'))
806
+ issues.push('触发器缺少 fileId 输入');
807
+ if (!inputNames.includes('filename'))
808
+ issues.push('触发器缺少 filename 输入');
809
+ if (!inputNames.includes('employeeId'))
810
+ issues.push('触发器缺少 employeeId 输入');
811
+ if (archiveRequired && !inputNames.includes('fondsId'))
812
+ issues.push('归档模式下触发器缺少 fondsId 输入');
813
+ if (retainDirStruct && !inputNames.includes('folderId'))
814
+ issues.push('维持目录结构时触发器缺少 folderId 输入');
815
+ }
816
+ // 检测动作节点
817
+ const docSave = actions.find((a) => a.type === 'DocumentSave' || a.componentType === 'DocumentSave');
818
+ if (!docSave) {
819
+ issues.push('缺少 DocumentSave 动作节点');
820
+ }
821
+ const outputNode = actions.find((a) => a.type === 'Output' || a.componentType === 'Output');
822
+ if (!outputNode) {
823
+ issues.push('缺少 Output 动作节点');
824
+ }
825
+ const compatible = issues.length === 0;
826
+ return caizhilianSuccess(ACTION, {
827
+ validation: {
828
+ compatible,
829
+ issues,
830
+ workflow: { id: app.id, name: app.name, status: app.status },
831
+ },
832
+ selection: compatible ? {
833
+ application_id: app.id,
834
+ scene_id: sceneId,
835
+ secret_key: secretKey,
836
+ version: 'file_collection_workflow_selection.v1',
837
+ } : undefined,
838
+ });
839
+ }
840
+ // ── 创建动作流(prepare/apply)────────────────────────
841
+ /** 标准文件采集动作流设计模板(CustomTrigger → DocumentSave → Output) */
842
+ function buildActionFlowDesign(opts) {
843
+ const { appId, sceneId, secretKey, retainDirStruct } = opts;
844
+ // 确定性 ID 生成(基于 SHA-256 前 19 位数字)
845
+ const hash = (s) => {
846
+ let h = 0;
847
+ for (let i = 0; i < s.length; i++)
848
+ h = ((h << 5) - h + s.charCodeAt(i)) | 0;
849
+ return Math.abs(h);
850
+ };
851
+ const triggerId = 1000000000000000000 + (hash(`${appId}-trigger`) % 900000000000000000);
852
+ const docSaveId = 1000000000000000000 + (hash(`${appId}-docsave`) % 900000000000000000);
853
+ const outputId = 1000000000000000000 + (hash(`${appId}-output`) % 900000000000000000);
854
+ const fileFolder = retainDirStruct
855
+ ? { formula: '${SelectParams.mainTable.folderId}', type: 'field' }
856
+ : { formula: '200', type: 'fixed' };
857
+ return {
858
+ saveAndStart: true,
859
+ appId, sceneId, secretKey,
860
+ appName: 'weaver-file-collection',
861
+ event: {
862
+ componentType: 'CustomTrigger',
863
+ id: String(triggerId),
864
+ inputs: [
865
+ { name: 'fileId', type: 'string' },
866
+ { name: 'filename', type: 'string' },
867
+ { name: 'employeeId', type: 'string' },
868
+ ...(retainDirStruct ? [{ name: 'folderId', type: 'string' }] : []),
869
+ ],
870
+ },
871
+ actions: [
872
+ {
873
+ componentType: 'DocumentSave',
874
+ id: String(docSaveId),
875
+ fileFolder,
876
+ nextActionId: String(outputId),
877
+ },
878
+ {
879
+ componentType: 'Output',
880
+ id: String(outputId),
881
+ customParamSetting: [
882
+ { name: 'docId', type: 'string', value: '${DocumentSave.Response.entry.docId}' },
883
+ ],
884
+ },
885
+ ],
886
+ addIds: [String(triggerId), String(docSaveId), String(outputId)],
887
+ };
888
+ }
889
+ async function createWorkflowPrepare(args, env) {
890
+ const name = args.name || `文件采集动作流(${new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }).replace(/[/:]/g, '').replace(' ', ' ').substring(0, 15)})`;
891
+ const archiveRequired = args.archive_required ?? false;
892
+ const retainDirStruct = args.retain_dir_struct ?? false;
893
+ const continuation = await env.continuation.issue({
894
+ operation: 'caizhilian.file-collection.workflow.create.apply',
895
+ name, archive_required: archiveRequired, retain_dir_struct: retainDirStruct,
896
+ });
897
+ return caizhilianSuccess('caizhilian.file-collection.workflow.create.prepare', {
898
+ name, archive_required: archiveRequired, retain_dir_struct: retainDirStruct, continuation,
899
+ });
900
+ }
901
+ async function createWorkflowApply(args, env, lease) {
902
+ const name = lease.name;
903
+ const archiveRequired = lease.archive_required;
904
+ const retainDirStruct = lease.retain_dir_struct;
905
+ const applyOp = 'caizhilian.file-collection.workflow.create.apply';
906
+ // 1. 创建应用
907
+ const saveResponse = await env.host.request({
908
+ method: 'POST',
909
+ path: APP_SAVE_PATH,
910
+ body: { name, type: 0, moudle: 'esb', description: 'weaver-file-collection dynamically generated action flow' },
911
+ operation: applyOp,
912
+ });
913
+ const saveData = asRecord(saveResponse);
914
+ const saveResult = asRecord(saveData.data);
915
+ const appId = String(saveResult.id || '');
916
+ if (!appId)
917
+ throw new CaizhilianCliError('api', 'business_error', '创建动作流应用失败:未返回应用 ID', { exitCode: 1 });
918
+ // 2. 获取版本列表
919
+ const versionResponse = await env.host.request({
920
+ method: 'GET',
921
+ path: `${APP_VERSION_PATH}/${appId}`,
922
+ operation: applyOp,
923
+ });
924
+ const versionData = asRecord(versionResponse);
925
+ const versions = asList(versionData.data);
926
+ const activeVersion = versions[0] || {};
927
+ const sceneId = activeVersion.sceneId || '';
928
+ const secretKey = activeVersion.secretKey || '';
929
+ // 3. 注册变量
930
+ const variables = ['fileId', 'filename', 'employeeId'];
931
+ if (archiveRequired)
932
+ variables.push('fondsId');
933
+ if (retainDirStruct)
934
+ variables.push('folderId');
935
+ for (const varName of variables) {
936
+ await env.host.request({
937
+ method: 'POST',
938
+ path: APP_VARIABLE_PATH,
939
+ body: { appId, sceneId, variableName: varName, variableType: 'string', variableValue: '' },
940
+ operation: applyOp,
941
+ });
942
+ }
943
+ // 4. 生成并保存设计
944
+ const design = buildActionFlowDesign({ appId, sceneId, secretKey, retainDirStruct });
945
+ // 5. 校验设计
946
+ await env.host.request({
947
+ method: 'POST',
948
+ path: DESIGN_CHECK_PATH,
949
+ body: design,
950
+ operation: applyOp,
951
+ });
952
+ // 6. 保存设计
953
+ await env.host.request({
954
+ method: 'POST',
955
+ path: DESIGN_SAVE_PATH,
956
+ body: design,
957
+ operation: applyOp,
958
+ });
959
+ // 7. 启用
960
+ await env.host.request({
961
+ method: 'GET',
962
+ path: `${APP_ENABLE_PATH}/${appId}`,
963
+ operation: applyOp,
964
+ });
965
+ return caizhilianSuccess(applyOp, {
966
+ id: appId,
967
+ scene_id: sceneId,
968
+ name,
969
+ registered_variables: variables,
970
+ });
971
+ }
972
+ // ── 启用动作流(prepare/apply)────────────────────────
973
+ async function enableWorkflowPrepare(args, env) {
974
+ const applicationId = requireId(args.application_id, 'application_id');
975
+ const continuation = await env.continuation.issue({
976
+ operation: 'caizhilian.file-collection.workflow.enable.apply',
977
+ application_id: applicationId,
978
+ });
979
+ return caizhilianSuccess('caizhilian.file-collection.workflow.enable.prepare', {
980
+ application_id: applicationId, continuation,
981
+ });
982
+ }
983
+ async function enableWorkflowApply(args, env, lease) {
984
+ const applicationId = lease.application_id;
985
+ const response = await env.host.request({
986
+ method: 'GET',
987
+ path: `${APP_ENABLE_PATH}/${applicationId}`,
988
+ operation: 'caizhilian.file-collection.workflow.enable.apply',
989
+ });
990
+ const data = asRecord(response);
991
+ if (!data.status && !data.success) {
992
+ throw new CaizhilianCliError('api', 'business_error', String(data.msg || data.message || '启用动作流失败'), {
993
+ code: data.code ?? data.status,
994
+ details: { action: 'caizhilian.file-collection.workflow.enable.apply', message: String(data.msg || data.message || '启用动作流失败') },
995
+ exitCode: 1,
996
+ });
997
+ }
998
+ return caizhilianSuccess('caizhilian.file-collection.workflow.enable.apply', {
999
+ application_id: applicationId, enabled: true,
1000
+ });
1001
+ }
1002
+ // ── 更新任务关联的动作流(prepare/apply)────────────────────────
1003
+ async function updateTaskWorkflowPrepare(args, env) {
1004
+ const taskId = requireId(args.task_id, 'task_id');
1005
+ const workflowId = requireId(args.workflow_id, 'workflow_id');
1006
+ const continuation = await env.continuation.issue({
1007
+ operation: 'caizhilian.file-collection.workflow.update-task.apply',
1008
+ task_id: taskId, workflow_id: workflowId,
1009
+ });
1010
+ return caizhilianSuccess('caizhilian.file-collection.workflow.update-task.prepare', {
1011
+ task_id: taskId, workflow_id: workflowId, continuation,
1012
+ });
1013
+ }
1014
+ async function updateTaskWorkflowApply(args, env, lease) {
1015
+ const taskId = lease.task_id;
1016
+ const workflowId = lease.workflow_id;
1017
+ const applyOp = 'caizhilian.file-collection.workflow.update-task.apply';
1018
+ // 读取当前表单
1019
+ const formResponse = await env.host.request({
1020
+ method: 'GET',
1021
+ path: `${GET_TASK_FORM_PATH}?id=${taskId}`,
1022
+ operation: applyOp,
1023
+ });
1024
+ const formData = asRecord(formResponse);
1025
+ const form = asRecord(formData.data);
1026
+ // 更新 workflowId
1027
+ const payload = { ...form, taskWorkflowId: workflowId };
1028
+ const response = await env.host.request({
1029
+ method: 'POST',
1030
+ path: SAVE_TASK_PATH,
1031
+ body: payload,
1032
+ operation: applyOp,
1033
+ });
1034
+ const data = asRecord(response);
1035
+ if (!data.status && !data.success) {
1036
+ throw new CaizhilianCliError('api', 'business_error', String(data.msg || data.message || '更新任务动作流失败'), {
1037
+ code: data.code ?? data.status,
1038
+ details: { action: applyOp, message: String(data.msg || data.message || '更新任务动作流失败') },
1039
+ exitCode: 1,
1040
+ });
1041
+ }
1042
+ return caizhilianSuccess(applyOp, {
1043
+ task_id: taskId, workflow_id: workflowId,
1044
+ });
1045
+ }
1046
+ // ── 等待扫描完成 ─────────────────────────────────────
1047
+ async function waitScanCompletion(args, env) {
1048
+ const runtimeId = requireNonEmpty(args.runtime_id, 'runtime_id');
1049
+ const timeoutSeconds = args.timeout_seconds ?? 300;
1050
+ const intervalSeconds = args.interval_seconds ?? 3;
1051
+ const deadline = Date.now() + timeoutSeconds * 1000;
1052
+ while (Date.now() < deadline) {
1053
+ const response = await env.host.request({
1054
+ method: 'GET',
1055
+ path: `${SCAN_PROGRESS_PATH}?runtimeId=${runtimeId}`,
1056
+ operation: ACTION,
1057
+ });
1058
+ const data = asRecord(response);
1059
+ const result = asRecord(data.data);
1060
+ if (result.finish === true || result.finish === 'true') {
1061
+ return caizhilianSuccess(ACTION, {
1062
+ runtime_id: runtimeId,
1063
+ finished: true,
1064
+ progress: result,
1065
+ });
1066
+ }
1067
+ await new Promise(resolve => setTimeout(resolve, intervalSeconds * 1000));
1068
+ }
1069
+ throw new CaizhilianCliError('api', 'timeout', `扫描等待超时(${timeoutSeconds}秒)`, {
1070
+ details: { runtime_id: runtimeId, timeout_seconds: timeoutSeconds },
1071
+ exitCode: 1,
1072
+ });
1073
+ }
1074
+ // ── 查询运行结果并归一化 ─────────────────────────────────────
1075
+ async function getRunResult(args, env) {
1076
+ const taskId = requireId(args.task_id, 'task_id');
1077
+ // 1. 查询运行进度
1078
+ const progressResponse = await env.host.request({
1079
+ method: 'GET',
1080
+ path: `${RUN_PROGRESS_PATH}?id=${taskId}`,
1081
+ operation: ACTION,
1082
+ });
1083
+ const progressData = asRecord(progressResponse);
1084
+ const progress = asRecord(progressData.data);
1085
+ if (!progress.finish && progress.finish !== 'true') {
1086
+ return caizhilianSuccess(ACTION, {
1087
+ version: 'file_collection_result.v1',
1088
+ status: 'collection_running',
1089
+ task_id: taskId,
1090
+ file_collection_ok: false,
1091
+ message: '采集任务仍在运行中',
1092
+ });
1093
+ }
1094
+ // 2. 查询成功和失败明细
1095
+ let successfulCount = 0;
1096
+ let failedCount = 0;
1097
+ try {
1098
+ const successResponse = await env.host.request({
1099
+ method: 'GET',
1100
+ path: `${DETAIL_TABLE_PATH}?fileCollTaskId=${taskId}&detailType=true`,
1101
+ operation: ACTION,
1102
+ });
1103
+ const successData = asRecord(successResponse);
1104
+ const successResult = asRecord(successData.data);
1105
+ successfulCount = asList(successResult.data || successResult).length;
1106
+ }
1107
+ catch { /* 查询失败时计数为 0 */ }
1108
+ try {
1109
+ const failedResponse = await env.host.request({
1110
+ method: 'GET',
1111
+ path: `${DETAIL_TABLE_PATH}?fileCollTaskId=${taskId}&detailType=false`,
1112
+ operation: ACTION,
1113
+ });
1114
+ const failedData = asRecord(failedResponse);
1115
+ const failedResult = asRecord(failedData.data);
1116
+ failedCount = asList(failedResult.data || failedResult).length;
1117
+ }
1118
+ catch { /* 查询失败时计数为 0 */ }
1119
+ // 3. 归一化结果
1120
+ let status;
1121
+ let fileCollectionOk;
1122
+ let message;
1123
+ if (failedCount > 0) {
1124
+ status = 'collection_completed_with_failures';
1125
+ fileCollectionOk = false;
1126
+ message = `采集完成,但有 ${failedCount} 个文件失败`;
1127
+ }
1128
+ else if (successfulCount > 0) {
1129
+ status = 'collection_succeeded';
1130
+ fileCollectionOk = true;
1131
+ message = `采集成功,共 ${successfulCount} 个文件`;
1132
+ }
1133
+ else {
1134
+ status = 'collection_completed_without_files';
1135
+ fileCollectionOk = false;
1136
+ message = '采集完成,但无文件记录';
1137
+ }
1138
+ return caizhilianSuccess(ACTION, {
1139
+ version: 'file_collection_result.v1',
1140
+ status,
1141
+ task_id: taskId,
1142
+ file_counts: { successful: successfulCount, failed: failedCount, total: successfulCount + failedCount },
1143
+ file_collection_ok: fileCollectionOk,
1144
+ message,
1145
+ });
1146
+ }
318
1147
  // ── 处理器汇总 ────────────────────────────────────────────
319
1148
  export const caizhilianFileCollectionHandlers = {
320
1149
  'caizhilian.file-collection.task.list': { execute: listTasks },
321
1150
  'caizhilian.file-collection.task.form': { execute: getTaskForm },
1151
+ 'caizhilian.file-collection.task.save': {
1152
+ prepare: saveTaskPrepare,
1153
+ apply: saveTaskApply,
1154
+ targetKeys: ['source_id', 'workflow_id'],
1155
+ },
322
1156
  'caizhilian.file-collection.storage.check': { execute: checkStorage },
323
1157
  'caizhilian.file-collection.scan': {
324
1158
  prepare: scanPrepare,
@@ -343,4 +1177,51 @@ export const caizhilianFileCollectionHandlers = {
343
1177
  apply: deleteTaskApply,
344
1178
  targetKeys: ['task_id'],
345
1179
  },
1180
+ // ── 任务管理补充 ──
1181
+ 'caizhilian.file-collection.task.switch': {
1182
+ prepare: switchTaskPrepare,
1183
+ apply: switchTaskApply,
1184
+ targetKeys: ['task_id'],
1185
+ },
1186
+ 'caizhilian.file-collection.task.save-as': {
1187
+ prepare: saveAsTaskPrepare,
1188
+ apply: saveAsTaskApply,
1189
+ targetKeys: ['task_id'],
1190
+ },
1191
+ 'caizhilian.file-collection.task.update-fonds': {
1192
+ prepare: updateFondsPrepare,
1193
+ apply: updateFondsApply,
1194
+ targetKeys: ['task_id'],
1195
+ },
1196
+ // ── 连接器/资源管理 ──
1197
+ 'caizhilian.file-collection.connector.query': { execute: queryConnectors },
1198
+ 'caizhilian.file-collection.connector.create': {
1199
+ prepare: createConnectorPrepare,
1200
+ apply: createConnectorApply,
1201
+ },
1202
+ 'caizhilian.file-collection.resource.create': {
1203
+ prepare: createResourcePrepare,
1204
+ apply: createResourceApply,
1205
+ targetKeys: ['remote_host'],
1206
+ },
1207
+ // ── 动作流管理 ──
1208
+ 'caizhilian.file-collection.workflow.search': { execute: searchWorkflows },
1209
+ 'caizhilian.file-collection.workflow.inspect': { execute: inspectWorkflow },
1210
+ 'caizhilian.file-collection.workflow.create': {
1211
+ prepare: createWorkflowPrepare,
1212
+ apply: createWorkflowApply,
1213
+ },
1214
+ 'caizhilian.file-collection.workflow.enable': {
1215
+ prepare: enableWorkflowPrepare,
1216
+ apply: enableWorkflowApply,
1217
+ targetKeys: ['application_id'],
1218
+ },
1219
+ 'caizhilian.file-collection.workflow.update-task': {
1220
+ prepare: updateTaskWorkflowPrepare,
1221
+ apply: updateTaskWorkflowApply,
1222
+ targetKeys: ['task_id'],
1223
+ },
1224
+ // ── 等待与结果验证 ──
1225
+ 'caizhilian.file-collection.scan.wait': { execute: waitScanCompletion },
1226
+ 'caizhilian.file-collection.run.result': { execute: getRunResult },
346
1227
  };