cloudcc-cli 2.3.8 → 2.4.0

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 (66) hide show
  1. package/.cursor/skills/{cloudcc-cli-dev → dev-guide}/SKILL.md +5 -1
  2. package/README.md +82 -1
  3. package/bin/cc.js +2 -1
  4. package/bin/index.js +1 -0
  5. package/cloudcc-dev-skill/SKILL.md +31 -8
  6. package/cloudcc-dev-skill/cloudcc-dev-html.md +42 -0
  7. package/cloudcc-dev-skill/config.json +2 -2
  8. package/mcp/index.js +27 -3
  9. package/mcp/tools/JSP Migrator/handler.js +51 -866
  10. package/mcp/tools/Object Creator/handler.js +14 -4
  11. package/mcp/tools/Object Fields Creator/handler.js +149 -3
  12. package/package.json +1 -1
  13. package/src/classes/docs/devguide.md +758 -364
  14. package/src/classes/docs/introduction.md +279 -143
  15. package/src/fields/buildFieldData.js +692 -0
  16. package/src/fields/create.js +10 -170
  17. package/src/fields/detail.js +37 -0
  18. package/src/fields/docs/devguide.md +168 -44
  19. package/src/fields/docs/introduction.md +2 -0
  20. package/src/fields/fields/A.js +3 -2
  21. package/src/fields/fields/AD.js +4 -2
  22. package/src/fields/fields/B.js +8 -5
  23. package/src/fields/fields/C.js +13 -5
  24. package/src/fields/fields/D.js +4 -4
  25. package/src/fields/fields/E.js +10 -5
  26. package/src/fields/fields/ENC.js +27 -8
  27. package/src/fields/fields/ENCD.js +27 -8
  28. package/src/fields/fields/F.js +4 -4
  29. package/src/fields/fields/FL.js +8 -4
  30. package/src/fields/fields/H.js +4 -4
  31. package/src/fields/fields/IMG.js +23 -5
  32. package/src/fields/fields/J.js +21 -6
  33. package/src/fields/fields/L.js +32 -8
  34. package/src/fields/fields/LT.js +23 -6
  35. package/src/fields/fields/M.js +2 -2
  36. package/src/fields/fields/MR.js +2 -2
  37. package/src/fields/fields/N.js +31 -8
  38. package/src/fields/fields/P.js +13 -5
  39. package/src/fields/fields/Q.js +42 -12
  40. package/src/fields/fields/S.js +19 -7
  41. package/src/fields/fields/SCORE.js +9 -4
  42. package/src/fields/fields/T.js +4 -4
  43. package/src/fields/fields/U.js +18 -5
  44. package/src/fields/fields/X.js +20 -6
  45. package/src/fields/fields/Y.js +17 -4
  46. package/src/fields/index.js +2 -0
  47. package/src/fields/update.js +148 -0
  48. package/src/jsp/analyze.js +17 -0
  49. package/src/jsp/doc.js +18 -0
  50. package/src/jsp/docs/devguide.md +111 -0
  51. package/src/jsp/docs/introduction.md +50 -0
  52. package/src/jsp/docs.js +21 -0
  53. package/src/jsp/index.js +14 -0
  54. package/src/jsp/migration.js +871 -0
  55. package/src/jsp/split.js +17 -0
  56. package/src/object/create.js +36 -10
  57. package/src/object/docs/devguide.md +6 -3
  58. package/src/project/docs/devguide.md +1 -1
  59. package/src/timer/docs/devguide.md +849 -400
  60. package/src/timer/docs/introduction.md +343 -231
  61. package/src/triggers/docs/devguide.md +929 -352
  62. package/src/triggers/docs/introduction.md +640 -369
  63. package/src/version/listModuleCommands.js +6 -0
  64. package/test/fields.cli.test.js +3 -1
  65. package/test/jsp.cli.test.js +70 -0
  66. package/test/object.cli.test.js +9 -1
@@ -1,888 +1,73 @@
1
- const fs = require('fs');
2
- const path = require('path');
1
+ const path = require("path")
2
+ const { runCcCommand, buildRunDetail } = require("../../cliRunner")
3
3
 
4
- function uniq(items) {
5
- return Array.from(new Set((items || []).filter(Boolean)));
4
+ function successText(run) {
5
+ return {
6
+ content: [{ type: "text", text: run.stdout || "" }],
7
+ }
6
8
  }
7
9
 
8
- function toPascalCase(value) {
9
- return String(value || '')
10
- .replace(/\.[^.]+$/, '')
11
- .split(/[^a-zA-Z0-9]+/)
12
- .filter(Boolean)
13
- .map(part => part.charAt(0).toUpperCase() + part.slice(1))
14
- .join('');
10
+ function failureText(prefix, run) {
11
+ return {
12
+ content: [{ type: "text", text: `${prefix}: ${buildRunDetail(run) || "cloudcc 命令执行失败"}` }],
13
+ }
15
14
  }
16
15
 
17
- function toKebabCase(value) {
18
- const base = String(value || '')
19
- .replace(/\.[^.]+$/, '')
20
- .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
21
- .replace(/[^a-zA-Z0-9]+/g, '-')
22
- .replace(/^-+|-+$/g, '')
23
- .toLowerCase();
24
- return base || 'jsp-migrated-component';
25
- }
26
-
27
- function lineNumberAt(source, index) {
28
- if (index <= 0) return 1;
29
- let count = 1;
30
- for (let i = 0; i < index && i < source.length; i += 1) {
31
- if (source[i] === '\n') count += 1;
32
- }
33
- return count;
34
- }
35
-
36
- function extractTagBlock(source, tag) {
37
- const start = source.indexOf(tag);
38
- if (start === -1) return null;
39
-
40
- const closeTag = '</cc>';
41
- const contentStart = start + tag.length;
42
- const closeStart = source.indexOf(closeTag, contentStart);
43
- if (closeStart === -1) return null;
44
-
45
- return {
46
- tag,
47
- start,
48
- end: closeStart + closeTag.length,
49
- innerStart: contentStart,
50
- innerEnd: closeStart,
51
- inner: source.slice(contentStart, closeStart)
52
- };
53
- }
54
-
55
- function extractBraceBlock(source, openBraceIndex) {
56
- let depth = 0;
57
- let inSingle = false;
58
- let inDouble = false;
59
- let inBacktick = false;
60
- let inLineComment = false;
61
- let inBlockComment = false;
62
- let escaped = false;
63
-
64
- for (let i = openBraceIndex; i < source.length; i += 1) {
65
- const ch = source[i];
66
- const next = source[i + 1];
67
-
68
- if (inLineComment) {
69
- if (ch === '\n') inLineComment = false;
70
- continue;
71
- }
72
-
73
- if (inBlockComment) {
74
- if (ch === '*' && next === '/') {
75
- inBlockComment = false;
76
- i += 1;
77
- }
78
- continue;
79
- }
80
-
81
- if (inSingle) {
82
- if (!escaped && ch === "'") inSingle = false;
83
- escaped = ch === '\\' && !escaped;
84
- continue;
85
- }
86
-
87
- if (inDouble) {
88
- if (!escaped && ch === '"') inDouble = false;
89
- escaped = ch === '\\' && !escaped;
90
- continue;
91
- }
92
-
93
- if (inBacktick) {
94
- if (!escaped && ch === '`') inBacktick = false;
95
- escaped = ch === '\\' && !escaped;
96
- continue;
97
- }
98
-
99
- if (ch === '/' && next === '/') {
100
- inLineComment = true;
101
- i += 1;
102
- continue;
103
- }
104
-
105
- if (ch === '/' && next === '*') {
106
- inBlockComment = true;
107
- i += 1;
108
- continue;
109
- }
110
-
111
- if (ch === "'") {
112
- inSingle = true;
113
- escaped = false;
114
- continue;
115
- }
116
-
117
- if (ch === '"') {
118
- inDouble = true;
119
- escaped = false;
120
- continue;
121
- }
122
-
123
- if (ch === '`') {
124
- inBacktick = true;
125
- escaped = false;
126
- continue;
127
- }
128
-
129
- if (ch === '{') {
130
- depth += 1;
131
- } else if (ch === '}') {
132
- depth -= 1;
133
- if (depth === 0) {
134
- return {
135
- start: openBraceIndex,
136
- end: i,
137
- body: source.slice(openBraceIndex + 1, i)
138
- };
139
- }
140
- }
141
- }
142
-
143
- return null;
144
- }
145
-
146
- function extractObjectApis(snippet) {
147
- const objectApis = [];
148
- const cqueryRegex = /cs\.(?:cquery|cqlQuery)\(\s*"([^"]+)"/g;
149
- let match = cqueryRegex.exec(snippet);
150
- while (match) {
151
- objectApis.push(match[1]);
152
- match = cqueryRegex.exec(snippet);
153
- }
154
- return uniq(objectApis);
155
- }
156
-
157
- function extractInvokers(snippet) {
158
- const invokers = [];
159
- const invokerRegex = /PageClsInvoker\(userInfo\)\.invoker\(\s*"([^"]+)"\s*,\s*"([^"]+)"/g;
160
- let match = invokerRegex.exec(snippet);
161
- while (match) {
162
- invokers.push(`${match[1]}.${match[2]}`);
163
- match = invokerRegex.exec(snippet);
164
- }
165
- return uniq(invokers);
166
- }
167
-
168
- function buildReasonBranches(source) {
169
- const branches = [];
170
- const reasonRegex = /if\s*\(\s*"([^"]+)"\.equals\(reason\)\s*\)\s*\{/g;
171
- let match = reasonRegex.exec(source);
172
- const methodNameCount = {};
173
-
174
- while (match) {
175
- const reason = match[1];
176
- const matchStart = match.index;
177
- const openBraceOffset = match[0].lastIndexOf('{');
178
- const openBraceIndex = matchStart + openBraceOffset;
179
- const block = extractBraceBlock(source, openBraceIndex);
180
- if (!block) {
181
- match = reasonRegex.exec(source);
182
- continue;
183
- }
184
-
185
- const methodBase = `handle${toPascalCase(reason)}`;
186
- methodNameCount[methodBase] = (methodNameCount[methodBase] || 0) + 1;
187
- const methodName = methodNameCount[methodBase] > 1
188
- ? `${methodBase}${methodNameCount[methodBase]}`
189
- : methodBase;
190
-
191
- const startLine = lineNumberAt(source, matchStart);
192
- const endLine = lineNumberAt(source, block.end);
193
-
194
- branches.push({
195
- reason,
196
- methodName,
197
- start: matchStart,
198
- end: block.end,
199
- startLine,
200
- endLine,
201
- snippet: source.slice(matchStart, block.end + 1),
202
- body: block.body,
203
- objectApis: extractObjectApis(block.body),
204
- invokers: extractInvokers(block.body)
205
- });
206
-
207
- match = reasonRegex.exec(source);
208
- }
209
-
210
- return branches;
211
- }
16
+ function extractRulesSection(devguideText) {
17
+ const startMarker = "## 6. JSP 迁移规则"
18
+ const start = devguideText.indexOf(startMarker)
19
+ if (start === -1) {
20
+ throw new Error("未在 jsp 开发文档中找到“JSP 迁移规则”章节")
21
+ }
212
22
 
213
- function buildClassSource({ className, jspFileName, branches, hasHelperBlock, sourcePath }) {
214
- const switchCases = branches.length > 0
215
- ? branches.map(branch => ` case "${branch.reason}":\n return ${branch.methodName}(oppid);`).join('\n')
216
- : ' default:\n return fail("未识别到 reason 分支,请手动补充迁移逻辑");';
23
+ const afterStart = devguideText.slice(start)
24
+ const nextSectionIndex = afterStart.slice(startMarker.length).search(/\n## \d+\. /)
25
+ const section =
26
+ nextSectionIndex === -1
27
+ ? afterStart
28
+ : afterStart.slice(0, startMarker.length + nextSectionIndex)
217
29
 
218
- const handlerMethods = branches.length > 0
219
- ? branches.map(branch => {
220
- const objectApiText = branch.objectApis.length > 0 ? branch.objectApis.join(', ') : '无';
221
- const invokerText = branch.invokers.length > 0 ? branch.invokers.join(', ') : '无';
222
- return `
223
- /**
224
- * TODO: 从 JSP 迁移 reason 分支: ${branch.reason}
225
- * 来源: ${jspFileName}:${branch.startLine}
226
- * 依赖对象: ${objectApiText}
227
- * 依赖调用: ${invokerText}
228
- */
229
- private Map<String, Object> ${branch.methodName}(String oppid) {
230
- Map<String, Object> result = new HashMap<>();
231
- result.put("success", false);
232
- result.put("oppid", oppid == null ? "" : oppid);
233
- result.put("reason", "${branch.reason}");
234
- result.put("message", "TODO: 请将 JSP 分支 '${branch.reason}' 迁移到该方法");
235
- return result;
236
- }
237
- `;
238
- }).join('\n')
239
- : `
240
- private Map<String, Object> handleUnclassified(String oppid) {
241
- Map<String, Object> result = new HashMap<>();
242
- result.put("success", false);
243
- result.put("oppid", oppid == null ? "" : oppid);
244
- result.put("message", "未识别到 reason 分支,请手动迁移");
245
- return result;
246
- }
247
- `;
248
-
249
- return `package classes.${className};
250
-
251
- import com.cloudcc.core.*;
252
- import java.util.*;
253
- // @SOURCE_CONTENT_START
254
- public class ${className} {
255
- private final UserInfo userInfo;
256
- private final CCService cs;
257
-
258
- public ${className}(UserInfo userInfo) {
259
- this(userInfo, new CCService(userInfo));
260
- }
261
-
262
- public ${className}(UserInfo userInfo, CCService cs) {
263
- this.userInfo = userInfo;
264
- this.cs = cs;
265
- }
266
-
267
- /**
268
- * 自动生成的迁移入口。
269
- * JSP来源: ${sourcePath}
270
- * helper块存在: ${hasHelperBlock ? '是' : '否'}
271
- */
272
- public Map<String, Object> execute(Map<String, Object> input) {
273
- String oppid = getString(input, "oppid");
274
- String reason = getString(input, "reason");
275
- return execute(oppid, reason);
276
- }
277
-
278
- public Map<String, Object> execute(String oppid, String reason) {
279
- if ("".equals(oppid)) {
280
- return fail("缺少参数 oppid");
281
- }
282
- if ("".equals(reason)) {
283
- return fail("缺少参数 reason");
284
- }
285
-
286
- switch (reason) {
287
- ${switchCases}
288
- default:
289
- return fail("不支持的 reason: " + reason);
290
- }
291
- }
292
-
293
- ${handlerMethods}
294
-
295
- private String getString(Map<String, Object> input, String key) {
296
- if (input == null || key == null) {
297
- return "";
298
- }
299
- Object value = input.get(key);
300
- return value == null ? "" : String.valueOf(value);
301
- }
302
-
303
- private Map<String, Object> fail(String message) {
304
- Map<String, Object> result = new HashMap<>();
305
- result.put("success", false);
306
- result.put("message", message);
307
- return result;
308
- }
309
- }
310
- // @SOURCE_CONTENT_END
311
- `;
312
- }
313
-
314
- function buildClassTestSource({ className, firstReason }) {
315
- const reason = firstReason || 'TODO_REASON';
316
- return `package classes.${className};
317
-
318
- import com.cloudcc.core.*;
319
- import java.util.*;
320
-
321
- public class ${className}Test {
322
- public static void main(String[] args) {
323
- ${className} service = new ${className}(new UserInfo());
324
- Map<String, Object> input = new HashMap<>();
325
- input.put("oppid", "demo_oppid");
326
- input.put("reason", "${reason}");
327
- Map<String, Object> result = service.execute(input);
328
- System.out.println(result);
329
- }
330
- }
331
- `;
332
- }
333
-
334
- function buildComponentSource({ componentName, className, jspFileName, reasons }) {
335
- const reasonList = JSON.stringify(reasons, null, 2);
336
- const defaultReason = reasons[0] || '';
337
-
338
- return `<template>
339
- <div class="jsp-migrated-wrapper">
340
- <header class="migrated-header">
341
- <h3>{{ componentInfo.compName }}</h3>
342
- <p>Legacy JSP: {{ legacyJsp }}</p>
343
- </header>
344
-
345
- <section class="migrated-panel">
346
- <label>Opportunity ID</label>
347
- <input v-model.trim="form.oppid" type="text" placeholder="请输入 oppid" />
348
-
349
- <label>Reason</label>
350
- <select v-model="form.reason">
351
- <option v-for="item in reasons" :key="item" :value="item">{{ item }}</option>
352
- </select>
353
-
354
- <button type="button" @click="handleExecute">执行迁移入口</button>
355
- <p class="hint">{{ hint }}</p>
356
- </section>
357
-
358
- <section class="migrated-result" v-if="lastPayload">
359
- <h4>最近触发参数</h4>
360
- <pre>{{ formattedPayload }}</pre>
361
- </section>
362
- </div>
363
- </template>
364
-
365
- <script>
366
- export default {
367
- data() {
368
- return {
369
- componentInfo: {
370
- component: "component-${componentName}",
371
- compName: "compName-${componentName}",
372
- compDesc: "Migrated from JSP ${jspFileName}",
373
- },
374
- legacyJsp: "${jspFileName}",
375
- targetClass: "${className}",
376
- reasons: ${reasonList},
377
- form: {
378
- oppid: "",
379
- reason: "${defaultReason}",
380
- },
381
- hint: "TODO: 在宿主中接入自定义类调用逻辑。",
382
- lastPayload: null,
383
- };
384
- },
385
- computed: {
386
- formattedPayload() {
387
- return this.lastPayload ? JSON.stringify(this.lastPayload, null, 2) : "";
388
- },
389
- },
390
- methods: {
391
- handleExecute() {
392
- const payload = {
393
- oppid: this.form.oppid,
394
- reason: this.form.reason,
395
- };
396
- this.lastPayload = payload;
397
-
398
- if (!payload.oppid || !payload.reason) {
399
- this.hint = "请先输入 oppid 并选择 reason。";
400
- return;
401
- }
402
-
403
- this.$emit("execute-migrated-action", {
404
- className: this.targetClass,
405
- payload,
406
- });
407
- this.hint = \`已触发事件 execute-migrated-action,请在宿主中调用 \${this.targetClass}.execute(payload)\`;
408
- },
409
- },
410
- };
411
- </script>
412
-
413
- <style lang="scss" scoped>
414
- .jsp-migrated-wrapper {
415
- padding: 16px;
416
- background: linear-gradient(160deg, #f2f6ff 0%, #ffffff 70%);
417
- border: 1px solid #dbe4ff;
418
- border-radius: 10px;
419
- color: #1d2b53;
30
+ return `# JSP 迁移处理规则(CloudCC)\n\n${section.trim()}\n`
420
31
  }
421
32
 
422
- .migrated-header h3 {
423
- margin: 0 0 6px;
424
- font-size: 18px;
425
- }
426
-
427
- .migrated-header p {
428
- margin: 0;
429
- font-size: 12px;
430
- color: #5b6b91;
431
- }
432
-
433
- .migrated-panel {
434
- margin-top: 14px;
435
- display: grid;
436
- gap: 8px;
437
- }
438
-
439
- .migrated-panel label {
440
- font-size: 12px;
441
- font-weight: 600;
442
- }
443
-
444
- .migrated-panel input,
445
- .migrated-panel select {
446
- height: 34px;
447
- border: 1px solid #c9d5ff;
448
- border-radius: 6px;
449
- padding: 0 10px;
450
- outline: none;
451
- }
452
-
453
- .migrated-panel button {
454
- margin-top: 4px;
455
- width: 160px;
456
- height: 34px;
457
- border: 0;
458
- border-radius: 6px;
459
- background: #3451ff;
460
- color: #fff;
461
- cursor: pointer;
462
- }
463
-
464
- .hint {
465
- margin: 0;
466
- font-size: 12px;
467
- color: #4f5f88;
468
- }
469
-
470
- .migrated-result {
471
- margin-top: 16px;
472
- background: #0f1530;
473
- border-radius: 8px;
474
- padding: 10px;
475
- }
476
-
477
- .migrated-result h4 {
478
- margin: 0 0 8px;
479
- font-size: 12px;
480
- color: #d7def7;
481
- }
482
-
483
- .migrated-result pre {
484
- margin: 0;
485
- white-space: pre-wrap;
486
- word-break: break-word;
487
- color: #b8c7ff;
488
- font-size: 12px;
489
- }
490
- </style>
491
- `;
492
- }
493
-
494
- function buildReport({
495
- sourcePath,
496
- helpDocPath,
497
- jspContent,
498
- helpDocContent,
499
- pageDirective,
500
- ccBlock,
501
- ccBangBlock,
502
- visualSegment,
503
- branches,
504
- objectApis,
505
- invokers,
506
- className,
507
- componentName,
508
- reportPath,
509
- mode
510
- }) {
511
- const lines = [];
512
- lines.push('# JSP Migration Report');
513
- lines.push('');
514
- lines.push(`- generatedAt: ${new Date().toISOString()}`);
515
- lines.push(`- mode: ${mode}`);
516
- lines.push(`- sourceJsp: ${sourcePath}`);
517
- lines.push(`- helpDoc: ${helpDocPath || '未提供'}`);
518
- lines.push(`- outputClass: classes/${className}`);
519
- lines.push(`- outputComponent: plugins/${componentName}`);
520
- lines.push(`- reportFile: ${reportPath}`);
521
- lines.push('');
522
-
523
- lines.push('## Summary');
524
- lines.push('');
525
- lines.push(`- jspLines: ${jspContent.split(/\r?\n/).length}`);
526
- lines.push(`- reasonBranches: ${branches.length}`);
527
- lines.push(`- helperBlockFound: ${ccBangBlock ? 'yes' : 'no'}`);
528
- lines.push(`- visualSegmentFound: ${visualSegment ? 'yes' : 'no'}`);
529
- lines.push(`- distinctObjectApis: ${objectApis.length}`);
530
- lines.push(`- distinctInvokers: ${invokers.length}`);
531
- lines.push('');
532
-
533
- lines.push('## Page Directive');
534
- lines.push('');
535
- lines.push(`- cc:page: ${pageDirective || '未检测到'}`);
536
- lines.push('');
537
-
538
- lines.push('## Legacy Blocks');
539
- lines.push('');
540
- lines.push(`- <cc>: ${ccBlock ? `${lineNumberAt(jspContent, ccBlock.start)}-${lineNumberAt(jspContent, ccBlock.end)}` : 'not found'}`);
541
- lines.push(`- <cc!>: ${ccBangBlock ? `${lineNumberAt(jspContent, ccBangBlock.start)}-${lineNumberAt(jspContent, ccBangBlock.end)}` : 'not found'}`);
542
- lines.push(`- visualHtmlSegment: ${visualSegment ? `${lineNumberAt(jspContent, visualSegment.start)}-${lineNumberAt(jspContent, visualSegment.end)}` : 'not found'}`);
543
- lines.push('');
544
-
545
- lines.push('## Global Dependencies');
546
- lines.push('');
547
- lines.push(`- cquery/cqlQuery objects: ${objectApis.length > 0 ? objectApis.join(', ') : '无'}`);
548
- lines.push(`- PageClsInvoker calls: ${invokers.length > 0 ? invokers.join(', ') : '无'}`);
549
- lines.push('');
550
-
551
- lines.push('## Reason Branches');
552
- lines.push('');
553
- if (branches.length === 0) {
554
- lines.push('- 未识别到 `if("xxx".equals(reason))` 分支,请手工分析。');
555
- } else {
556
- branches.forEach(branch => {
557
- lines.push(`### ${branch.reason}`);
558
- lines.push(`- lineRange: ${branch.startLine}-${branch.endLine}`);
559
- lines.push(`- generatedMethod: ${branch.methodName}`);
560
- lines.push(`- objectApis: ${branch.objectApis.length > 0 ? branch.objectApis.join(', ') : '无'}`);
561
- lines.push(`- invokers: ${branch.invokers.length > 0 ? branch.invokers.join(', ') : '无'}`);
562
- lines.push('');
563
- });
564
- }
565
-
566
- lines.push('## Help Doc Snapshot');
567
- lines.push('');
568
- if (!helpDocContent) {
569
- lines.push('- 未提供帮助文档路径,跳过。');
570
- } else {
571
- const snapshot = helpDocContent
572
- .split(/\r?\n/)
573
- .map(line => line.trim())
574
- .filter(Boolean)
575
- .slice(0, 20);
576
- if (snapshot.length === 0) {
577
- lines.push('- 帮助文档为空或不可解析。');
578
- } else {
579
- snapshot.forEach(line => lines.push(`- ${line}`));
580
- }
581
- }
582
-
583
- lines.push('');
584
- lines.push('## Next Actions');
585
- lines.push('');
586
- lines.push(`- 在 \`classes/${className}/${className}.java\` 完成各 reason 分支迁移。`);
587
- lines.push(`- 在 \`plugins/${componentName}/${componentName}.vue\` 接入真实触发流程。`);
588
- lines.push('- 按报告中的 object/invoker 依赖逐项验证。');
589
-
590
- return `${lines.join('\n')}\n`;
591
- }
592
-
593
- function parseMigrationInput({ jspPath, className, componentName }) {
594
- const jspFileName = path.basename(jspPath);
595
- const jspBaseName = jspFileName.replace(/\.[^.]+$/, '');
596
-
597
- const resolvedClassName = className && className.trim()
598
- ? className.trim()
599
- : `${toPascalCase(jspBaseName)}MigratedService`;
600
-
601
- const resolvedComponentName = componentName && componentName.trim()
602
- ? componentName.trim()
603
- : `${toKebabCase(jspBaseName)}-migrated`;
604
-
33
+ async function getJspMigrationRules() {
34
+ const run = runCcCommand(["doc", "jsp", "devguide"])
35
+ if (!run.success) {
36
+ return failureText("✗ 获取 JSP 迁移规则失败", run)
37
+ }
38
+ try {
605
39
  return {
606
- jspFileName,
607
- jspBaseName,
608
- resolvedClassName,
609
- resolvedComponentName
610
- };
611
- }
612
-
613
- function prepareAnalysis({ jspContent, sourcePath }) {
614
- const ccBlock = extractTagBlock(jspContent, '<cc>');
615
- const ccBangBlock = extractTagBlock(jspContent, '<cc!>');
616
- const pageMatch = jspContent.match(/<cc:page\s+([^>]*?)\/>/);
617
- const pageDirective = pageMatch ? pageMatch[0] : '';
618
-
619
- let visualSegment = null;
620
- if (ccBlock) {
621
- const visualStart = ccBlock.end;
622
- const visualEnd = ccBangBlock ? ccBangBlock.start : jspContent.length;
623
- const rawVisual = jspContent.slice(visualStart, visualEnd);
624
- if (/<\s*(div|table|form|section|header|main|script|style|span|input|button|ul|ol|li)\b/i.test(rawVisual)) {
625
- visualSegment = {
626
- start: visualStart,
627
- end: visualEnd,
628
- content: rawVisual
629
- };
630
- }
40
+ content: [{ type: "text", text: extractRulesSection(run.stdout || "") }],
631
41
  }
632
-
633
- const branches = buildReasonBranches(jspContent);
634
- const objectApis = extractObjectApis(jspContent);
635
- const invokers = extractInvokers(jspContent);
636
-
42
+ } catch (error) {
637
43
  return {
638
- sourcePath,
639
- ccBlock,
640
- ccBangBlock,
641
- pageDirective,
642
- visualSegment,
643
- branches,
644
- objectApis,
645
- invokers
646
- };
647
- }
648
-
649
- function buildResultText({
650
- mode,
651
- sourcePath,
652
- classPath,
653
- componentPath,
654
- reportPath,
655
- className,
656
- componentName,
657
- reasonCount,
658
- objectApiCount,
659
- invokerCount,
660
- createdFiles
661
- }) {
662
- const lines = [];
663
- lines.push(`✓ JSP 迁移${mode === 'dry-run' ? '分析' : '生成'}完成`);
664
- lines.push('');
665
- lines.push(`source: ${sourcePath}`);
666
- lines.push(`class: ${className}`);
667
- lines.push(`component: ${componentName}`);
668
- lines.push(`reason分支: ${reasonCount}`);
669
- lines.push(`对象依赖数: ${objectApiCount}`);
670
- lines.push(`invoker依赖数: ${invokerCount}`);
671
- lines.push('');
672
-
673
- if (mode === 'dry-run') {
674
- lines.push('dry-run 模式未写入任何文件。');
675
- lines.push(`报告预期路径: ${reportPath}`);
676
- } else {
677
- lines.push('已生成文件:');
678
- createdFiles.forEach(file => lines.push(`- ${file}`));
679
- lines.push('');
680
- lines.push(`class目录: ${classPath}`);
681
- lines.push(`component目录: ${componentPath}`);
682
- lines.push(`报告路径: ${reportPath}`);
44
+ content: [{ type: "text", text: `✗ 获取 JSP 迁移规则失败: ${error.message}` }],
683
45
  }
684
-
685
- return lines.join('\n');
686
- }
687
-
688
- async function runMigration({
689
- jspPath,
690
- helpDocPath,
691
- projectPath = process.cwd(),
692
- outputProjectPath,
693
- className,
694
- componentName,
695
- reportDir = 'migration-report',
696
- mode = 'apply',
697
- overwrite = false
698
- }) {
699
- try {
700
- if (!jspPath) {
701
- return {
702
- content: [{ type: 'text', text: '✗ 参数缺失: jspPath 必填' }]
703
- };
704
- }
705
-
706
- const resolvedProjectPath = path.resolve(projectPath);
707
- const resolvedOutputProjectPath = path.resolve(outputProjectPath || projectPath);
708
- const sourcePath = path.isAbsolute(jspPath)
709
- ? jspPath
710
- : path.resolve(resolvedProjectPath, jspPath);
711
-
712
- if (!fs.existsSync(sourcePath)) {
713
- return {
714
- content: [{ type: 'text', text: `✗ JSP 文件不存在: ${sourcePath}` }]
715
- };
716
- }
717
-
718
- const helpPath = helpDocPath
719
- ? (path.isAbsolute(helpDocPath) ? helpDocPath : path.resolve(resolvedProjectPath, helpDocPath))
720
- : null;
721
-
722
- const jspContent = fs.readFileSync(sourcePath, 'utf8');
723
- const helpDocContent = helpPath && fs.existsSync(helpPath)
724
- ? fs.readFileSync(helpPath, 'utf8')
725
- : '';
726
-
727
- const naming = parseMigrationInput({ jspPath: sourcePath, className, componentName });
728
- const { jspFileName, jspBaseName, resolvedClassName, resolvedComponentName } = naming;
729
-
730
- const analysis = prepareAnalysis({ jspContent, sourcePath });
731
- const classSource = buildClassSource({
732
- className: resolvedClassName,
733
- jspFileName,
734
- branches: analysis.branches,
735
- hasHelperBlock: Boolean(analysis.ccBangBlock),
736
- sourcePath
737
- });
738
-
739
- const classTestSource = buildClassTestSource({
740
- className: resolvedClassName,
741
- firstReason: analysis.branches[0] ? analysis.branches[0].reason : ''
742
- });
743
-
744
- const componentSource = buildComponentSource({
745
- componentName: resolvedComponentName,
746
- className: resolvedClassName,
747
- jspFileName,
748
- reasons: analysis.branches.map(item => item.reason)
749
- });
750
-
751
- const reportPath = path.join(resolvedOutputProjectPath, reportDir, `${jspBaseName}.migration.md`);
752
- const reportContent = buildReport({
753
- sourcePath,
754
- helpDocPath: helpPath,
755
- jspContent,
756
- helpDocContent,
757
- pageDirective: analysis.pageDirective,
758
- ccBlock: analysis.ccBlock,
759
- ccBangBlock: analysis.ccBangBlock,
760
- visualSegment: analysis.visualSegment,
761
- branches: analysis.branches,
762
- objectApis: analysis.objectApis,
763
- invokers: analysis.invokers,
764
- className: resolvedClassName,
765
- componentName: resolvedComponentName,
766
- reportPath,
767
- mode
768
- });
769
-
770
- const classDir = path.join(resolvedOutputProjectPath, 'classes', resolvedClassName);
771
- const componentDir = path.join(resolvedOutputProjectPath, 'plugins', resolvedComponentName);
772
-
773
- const fileMap = {
774
- [path.join(classDir, `${resolvedClassName}.java`)]: classSource,
775
- [path.join(classDir, `${resolvedClassName}Test.java`)]: classTestSource,
776
- [path.join(classDir, 'config.json')]: JSON.stringify({
777
- name: resolvedClassName,
778
- version: '2'
779
- }, null, 4),
780
- [path.join(componentDir, `${resolvedComponentName}.vue`)]: componentSource,
781
- [path.join(componentDir, 'config.json')]: JSON.stringify({
782
- component: `component-${resolvedComponentName}`,
783
- compName: `compName-${resolvedComponentName}`,
784
- compDesc: `Migrated from JSP ${jspFileName}`
785
- }),
786
- [reportPath]: reportContent
787
- };
788
-
789
- if (mode === 'apply') {
790
- const existingFiles = Object.keys(fileMap).filter(file => fs.existsSync(file));
791
- if (!overwrite && existingFiles.length > 0) {
792
- return {
793
- content: [{
794
- type: 'text',
795
- text: `✗ 目标文件已存在(可设置 overwrite=true 覆盖):\n${existingFiles.map(item => `- ${item}`).join('\n')}`
796
- }]
797
- };
798
- }
799
-
800
- Object.keys(fileMap).forEach(filePath => {
801
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
802
- fs.writeFileSync(filePath, fileMap[filePath], 'utf8');
803
- });
804
- }
805
-
806
- const text = buildResultText({
807
- mode,
808
- sourcePath,
809
- classPath: classDir,
810
- componentPath: componentDir,
811
- reportPath,
812
- className: resolvedClassName,
813
- componentName: resolvedComponentName,
814
- reasonCount: analysis.branches.length,
815
- objectApiCount: analysis.objectApis.length,
816
- invokerCount: analysis.invokers.length,
817
- createdFiles: Object.keys(fileMap)
818
- });
819
-
820
- return {
821
- content: [{ type: 'text', text }]
822
- };
823
- } catch (error) {
824
- return {
825
- content: [{ type: 'text', text: `✗ JSP 迁移失败: ${error.message}` }]
826
- };
827
- }
828
- }
829
-
830
- /**
831
- * 供 MCP 工具 get_jsp_migration_rules 返回的固定说明,便于 AI 在批量迁移前统一遵循。
832
- */
833
- const JSP_MIGRATION_RULES_MARKDOWN = `# JSP 迁移处理规则(CloudCC)
834
-
835
- ## 1. 从旧系统 URL 定位 JSP 源文件
836
-
837
- - 若页面或配置中出现形如 \`/controller.action?name=<name>\` 的地址(示例:\`name=wt_ly\`),应在**项目根目录下的 customize 目录**中查找对应 JSP。
838
- - 常见约定:\`<projectPath>/customize/<name>.jsp\`(若实际仓库使用子目录或其它扩展名,以仓库为准)。
839
- - 解析出真实路径后,再将其作为 \`jspPath\` 传入 \`analyze_jsp_migration\` 或 \`split_jsp_to_cloudcc\`(可相对 \`projectPath\`)。
840
-
841
- ## 2. 批量迁移时的建议流程
842
-
843
- 1. 先调用本说明(get_jsp_migration_rules)确认规则。
844
- 2. 在仓库中列举或搜索 \`customize\` 下需处理的 \`.jsp\`(或根据 URL 中的 \`name\` 逐个解析路径)。
845
- 3. 对每个 JSP 单独调用 \`analyze_jsp_migration\`(仅分析)或 \`split_jsp_to_cloudcc\`(生成类与组件);不要假设 MCP 会在服务端自动循环多个文件。
846
-
847
- ## 3. 自定义页面 / 自定义类中调用其它类(PageClsInvoker)
848
-
849
- 在服务端自定义类中,通过 \`PageClsInvoker\` 调用其它自定义类方法,典型形式如下:
850
-
851
- \`\`\`java
852
- // userInfo 为当前上下文的 UserInfo;类名、方法名为目标自定义类及其方法;最后一个参数为参数列表(如 List)
853
- Object result = new PageClsInvoker(userInfo).invoker("TargetClassName", "targetMethodName", argList);
854
- \`\`\`
855
-
856
- 示例(类名、方法名以实际平台为准):
857
-
858
- \`\`\`java
859
- Object result2 = new PageClsInvoker(userInfo).invoker("LabDingTalkProdlemMessage", "sendTextMessageByUserCodeList", arglist0);
860
- \`\`\`
861
-
862
- 迁移 JSP 内出现的同类调用时,保持上述调用形态,并核对目标类、方法在平台中已存在且参数类型一致。
863
-
864
- ## 4. 与迁移工具的配合
865
-
866
- - \`analyze_jsp_migration\` / \`split_jsp_to_cloudcc\` 会从 JSP 文本中识别 \`PageClsInvoker(userInfo).invoker("类名","方法名", ...)\` 形式的依赖,并写入迁移报告;具体业务实现需在生成后的自定义类中补全或调整。
867
- `;
868
-
869
- async function getJspMigrationRules() {
870
- return {
871
- content: [{ type: 'text', text: JSP_MIGRATION_RULES_MARKDOWN }]
872
- };
46
+ }
873
47
  }
874
48
 
875
49
  async function analyzeJspMigration(params) {
876
- return runMigration({ ...(params || {}), mode: 'dry-run' });
50
+ const resolvedPath = path.resolve((params && params.projectPath) || process.cwd())
51
+ const encoded = encodeURI(JSON.stringify(params || {}))
52
+ const run = runCcCommand(["analyze", "jsp", encoded], { cwd: resolvedPath })
53
+ if (!run.success) {
54
+ return failureText("✗ JSP 迁移分析失败", run)
55
+ }
56
+ return successText(run)
877
57
  }
878
58
 
879
59
  async function splitJspToCloudcc(params) {
880
- return runMigration(params || {});
60
+ const resolvedPath = path.resolve((params && params.projectPath) || process.cwd())
61
+ const encoded = encodeURI(JSON.stringify(params || {}))
62
+ const run = runCcCommand(["split", "jsp", encoded], { cwd: resolvedPath })
63
+ if (!run.success) {
64
+ return failureText("✗ JSP 迁移拆分失败", run)
65
+ }
66
+ return successText(run)
881
67
  }
882
68
 
883
69
  module.exports = {
884
- getJspMigrationRules,
885
- analyzeJspMigration,
886
- splitJspToCloudcc,
887
- runMigrationForCli: runMigration
888
- };
70
+ getJspMigrationRules,
71
+ analyzeJspMigration,
72
+ splitJspToCloudcc,
73
+ }