momo-ai 1.0.21 → 1.0.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 (94) hide show
  1. package/.claude/skills/r2mo-rad-lain/SKILL.md +63 -374
  2. package/.trae/skills/algorithmic-art/LICENSE.txt +202 -0
  3. package/.trae/skills/algorithmic-art/SKILL.md +405 -0
  4. package/.trae/skills/algorithmic-art/templates/generator_template.js +223 -0
  5. package/.trae/skills/algorithmic-art/templates/viewer.html +599 -0
  6. package/.trae/skills/doc-coauthoring/SKILL.md +375 -0
  7. package/.trae/skills/frontend-design/LICENSE.txt +177 -0
  8. package/.trae/skills/frontend-design/SKILL.md +42 -0
  9. package/.trae/skills/r2mo-rad-lain/SKILL.md +101 -0
  10. package/README.md +9 -32
  11. package/docs/images/r2mo-lain.png +0 -0
  12. package/package.json +11 -11
  13. package/skills/r2mo-rad-domain/SKILL.md +70 -0
  14. package/src/_skill/repositories.json +9 -3
  15. package/src/_template/LAIN/.obsidian/app.json +1 -0
  16. package/src/_template/LAIN/.obsidian/appearance.json +10 -0
  17. package/src/_template/LAIN/.obsidian/community-plugins.json +7 -0
  18. package/src/_template/LAIN/.obsidian/core-plugins.json +33 -0
  19. package/src/_template/LAIN/.obsidian/plugins/dataview/main.js +20876 -0
  20. package/src/_template/LAIN/.obsidian/plugins/dataview/manifest.json +11 -0
  21. package/src/_template/LAIN/.obsidian/plugins/dataview/styles.css +141 -0
  22. package/src/_template/LAIN/.obsidian/plugins/obsidian-excalidraw-plugin/data.json +815 -0
  23. package/src/_template/LAIN/.obsidian/plugins/obsidian-excalidraw-plugin/main.js +10 -0
  24. package/src/_template/LAIN/.obsidian/plugins/obsidian-excalidraw-plugin/manifest.json +12 -0
  25. package/src/_template/LAIN/.obsidian/plugins/obsidian-excalidraw-plugin/styles.css +1 -0
  26. package/src/_template/LAIN/.obsidian/plugins/obsidian-kanban/main.js +153 -0
  27. package/src/_template/LAIN/.obsidian/plugins/obsidian-kanban/manifest.json +11 -0
  28. package/src/_template/LAIN/.obsidian/plugins/obsidian-kanban/styles.css +1 -0
  29. package/src/_template/LAIN/.obsidian/plugins/obsidian-plantuml/main.js +7732 -0
  30. package/src/_template/LAIN/.obsidian/plugins/obsidian-plantuml/manifest.json +10 -0
  31. package/src/_template/LAIN/.obsidian/plugins/obsidian-plantuml/styles.css +38 -0
  32. package/src/_template/LAIN/.obsidian/plugins/obsidian-tasks-plugin/main.js +504 -0
  33. package/src/_template/LAIN/.obsidian/plugins/obsidian-tasks-plugin/manifest.json +12 -0
  34. package/src/_template/LAIN/.obsidian/plugins/obsidian-tasks-plugin/styles.css +1 -0
  35. package/src/_template/LAIN/.obsidian/snippets/body-font.css +27 -0
  36. package/src/_template/LAIN/.obsidian/themes/Primary/manifest.json +9 -0
  37. package/src/_template/LAIN/.obsidian/themes/Primary/theme.css +3878 -0
  38. package/src/_template/LAIN/.obsidian/themes/Retro Windows/manifest.json +7 -0
  39. package/src/_template/LAIN/.obsidian/themes/Retro Windows/theme.css +582 -0
  40. package/src/_template/LAIN/.obsidian/themes/RetroOS 98/manifest.json +9 -0
  41. package/src/_template/LAIN/.obsidian/themes/RetroOS 98/theme.css +2566 -0
  42. package/src/_template/LAIN/.obsidian/types.json +28 -0
  43. package/src/_template/LAIN/.obsidian/workspace.json +184 -0
  44. package/src/_template/LAIN/AGENTS.md +170 -16
  45. package/src/_template/R2MO/domain-enhance.md +10 -0
  46. package/src/commander/app.json +13 -0
  47. package/src/commander/apply.json +13 -0
  48. package/src/commander/ask.json +6 -0
  49. package/src/commander/docs.json +13 -0
  50. package/src/commander/domain.json +19 -0
  51. package/src/commander/init.json +1 -1
  52. package/src/commander/mmr0.json +6 -0
  53. package/src/commander/mmr2.json +6 -0
  54. package/src/executor/executeApp.js +133 -0
  55. package/src/executor/{executeSkills.js → executeApply.js} +166 -302
  56. package/src/executor/executeAsk.js +274 -0
  57. package/src/executor/executeDocs.js +498 -0
  58. package/src/executor/executeDomain.js +293 -0
  59. package/src/executor/executeInit.js +159 -383
  60. package/src/executor/executeMcp.js +74 -1
  61. package/src/executor/executeMmr0.js +488 -0
  62. package/src/executor/executeMmr2.js +880 -0
  63. package/src/executor/index.js +15 -3
  64. package/src/python/r2mo_proto.py +418 -0
  65. package/src/python/r2mo_proto_database.py +369 -0
  66. package/src/python/r2mo_proto_domain.py +458 -0
  67. package/src/utils/momo-menu.js +43 -13
  68. package/.claude/skills/r2mo-rad-lain/PROMPT.md +0 -281
  69. package/.claude/skills/r2mo-rad-lain/README.md +0 -192
  70. package/.claude/skills/r2mo-rad-lain/examples/argument-parsing.js +0 -154
  71. package/.claude/skills/r2mo-rad-lain/examples/file-operations.js +0 -182
  72. package/.claude/skills/r2mo-rad-lain/file-utils-api.md +0 -281
  73. package/.claude/skills/r2mo-rad-lain/menu-api.md +0 -187
  74. package/.claude/skills/r2mo-rad-lain/scripts/file-utils.js +0 -223
  75. package/.claude/skills/r2mo-rad-lain/scripts/menu.js +0 -289
  76. package/.claude/skills/r2mo-rad-lain/scripts/yaml-parser.js +0 -209
  77. package/.claude/skills/r2mo-rad-lain/templates/command.json.template +0 -13
  78. package/.claude/skills/r2mo-rad-lain/templates/executor.js.template +0 -32
  79. package/.claude/skills/r2mo-rad-lain/templates/interactive-menu.js.template +0 -221
  80. package/src/_template/LAIN/.momo/advanced/actor.md +0 -42
  81. package/src/_template/LAIN/.momo/advanced/refer.json +0 -46
  82. package/src/_template/LAIN/.momo/scripts/submodule-clean.sh +0 -56
  83. package/src/_template/LAIN/changes/proposal.md +0 -39
  84. package/src/_template/LAIN/changes/tasks/task-detail.md +0 -45
  85. package/src/_template/LAIN/changes/tasks.md +0 -49
  86. package/src/_template/LAIN/execute/admin-n-f-dashboard.md +0 -53
  87. package/src/_template/LAIN/execute/admin-n-f-form.md +0 -51
  88. package/src/_template/LAIN/execute/admin-n-f-home.md +0 -49
  89. package/src/_template/LAIN/execute/admin-n-f-list.md +0 -52
  90. package/src/_template/LAIN/execute/admin-n-f-login.md +0 -56
  91. package/src/_template/LAIN/specification/project-model.md +0 -13
  92. package/src/_template/LAIN/specification/project.md +0 -73
  93. package/src/_template/LAIN/specification/requirement.md +0 -25
  94. package/src/commander/skills.json +0 -20
@@ -0,0 +1,458 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ R2MO Java Domain to Protobuf Converter (V9 - Enhanced Java Property & Constraints)
6
+ 从 Java Domain 实体类生成 Protobuf 文件
7
+ 更新内容:
8
+ 1. 【强化】Java 属性名追加到注释:proto 字段用下划线,注释中保留原始驼峰名 [Java:xxx]
9
+ 2. 【强化】约束格式规范化:长度、必填、数值范围等统一为"中文:值"格式
10
+ 3. 【强化】更全面的注解支持:@Pattern、@DecimalMax/Min、@ApiModelProperty 等
11
+ 4. 【强化】注释提取优先级:@Schema > JavaDoc > 行内注释
12
+ 5. 保留所有 V8 特性(jOOQ Getter 提取、BaseEntity 补全、自动导入)
13
+ """
14
+
15
+ import os
16
+ import re
17
+ import sys
18
+ import argparse
19
+ import xml.etree.ElementTree as ET
20
+
21
+ # ================= 1. 配置与映射表 =================
22
+
23
+ # BaseEntity 公共字段:proto 名、类型、Java 属性名(驼峰)、中文描述
24
+ BASE_ENTITY_FIELDS = [
25
+ {'name': 'key', 'type': 'string', 'label': '', 'comment': '主键', 'java_name': 'id'},
26
+ {'name': 'active', 'type': 'bool', 'label': '', 'comment': '是否启用', 'java_name': 'active'},
27
+ {'name': 'sigma', 'type': 'string', 'label': '', 'comment': '统一标识', 'java_name': 'sigma'},
28
+ {'name': 'metadata', 'type': 'string', 'label': '', 'comment': '附加配置', 'java_name': 'metadata'},
29
+ {'name': 'language', 'type': 'string', 'label': '', 'comment': '语言', 'java_name': 'language'},
30
+ {'name': 'created_at', 'type': 'string', 'label': '', 'comment': '创建时间', 'java_name': 'createdAt'},
31
+ {'name': 'created_by', 'type': 'string', 'label': '', 'comment': '创建人', 'java_name': 'createdBy'},
32
+ {'name': 'updated_at', 'type': 'string', 'label': '', 'comment': '更新时间', 'java_name': 'updatedAt'},
33
+ {'name': 'updated_by', 'type': 'string', 'label': '', 'comment': '更新人', 'java_name': 'updatedBy'},
34
+ ]
35
+
36
+ # 基础类型映射
37
+ TYPE_MAPPING = {
38
+ 'String': 'string', 'Integer': 'int32', 'int': 'int32',
39
+ 'Long': 'int64', 'long': 'int64', 'Boolean': 'bool', 'boolean': 'bool',
40
+ 'Double': 'double', 'double': 'double', 'Float': 'float', 'float': 'float',
41
+ 'BigDecimal': 'string', 'LocalDateTime': 'string', 'LocalDate': 'string',
42
+ 'LocalTime': 'string', 'Date': 'string', 'UUID': 'string',
43
+ 'JsonObject': 'string', 'JsonArray': 'string'
44
+ }
45
+
46
+ CONSTRAINT_MAPPING = {
47
+ 'NotNull': '必填', 'NotBlank': '必填', 'NotEmpty': '必填',
48
+ 'Deprecated': '已废弃', 'Email': '邮箱格式', 'Phone': '手机号格式'
49
+ }
50
+
51
+ # ================= 2. 辅助函数 =================
52
+
53
+ def get_project_name():
54
+ if os.path.exists('pom.xml'):
55
+ try:
56
+ tree = ET.parse('pom.xml')
57
+ root = tree.getroot()
58
+ ns = re.match(r'\{.*\}', root.tag)
59
+ ns_map = { 'mvn': ns.group(0).strip('{}') } if ns else {}
60
+ if ns:
61
+ aid = root.find(f"{{{ns_map['mvn']}}}artifactId")
62
+ else:
63
+ aid = root.find('artifactId')
64
+ if aid is not None: return aid.text
65
+ except Exception: pass
66
+ return os.path.basename(os.getcwd())
67
+
68
+ def camel_to_snake(name):
69
+ s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
70
+ return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
71
+
72
+ def get_package_name(content):
73
+ match = re.search(r'package\s+([\w\.]+);', content)
74
+ return match.group(1) if match else "domain"
75
+
76
+ def extract_constraints(lines):
77
+ """
78
+ 从注解中提取约束信息(规范化格式),包括:
79
+ 1. 长度约束 (@Size, @Length)
80
+ 2. 数值约束 (@Max, @Min, @DecimalMax, @DecimalMin)
81
+ 3. 验证约束 (@NotNull, @NotBlank, @NotEmpty, @Email, @Pattern 等)
82
+ 4. Swagger 描述 (@Schema, @ApiModelProperty)
83
+ """
84
+ constraints = []
85
+ block = " ".join(lines)
86
+
87
+ # 提取 @Size / @Length 约束(规范化格式)
88
+ size = re.search(r'@(?:Size|Length)\(([^)]*)\)', block)
89
+ if size:
90
+ params = size.group(1)
91
+ min_v = re.search(r'min\s*=\s*(\d+)', params)
92
+ max_v = re.search(r'max\s*=\s*(\d+)', params)
93
+ if min_v and max_v:
94
+ constraints.append(f"长度:{min_v.group(1)}-{max_v.group(1)}")
95
+ elif min_v:
96
+ constraints.append(f"最小长度:{min_v.group(1)}")
97
+ elif max_v:
98
+ constraints.append(f"最大长度:{max_v.group(1)}")
99
+
100
+ # 提取数值约束(规范化)
101
+ for ann_name, cn_name in [('Max', '最大值'), ('Min', '最小值'),
102
+ ('DecimalMax', '最大值'), ('DecimalMin', '最小值')]:
103
+ val = re.search(fr'@{ann_name}\(?\s*(?:value\s*=\s*)?["\']?([0-9.]+)["\']?\s*\)?', block)
104
+ if val:
105
+ constraints.append(f"{cn_name}:{val.group(1)}")
106
+
107
+ # 提取验证约束(规范化)
108
+ for k, v in CONSTRAINT_MAPPING.items():
109
+ if f'@{k}' in block:
110
+ constraints.append(v)
111
+
112
+ # 提取 @Pattern 正则约束
113
+ pattern = re.search(r'@Pattern\([^)]*regexp\s*=\s*"([^"]+)"', block)
114
+ if pattern:
115
+ constraints.append(f"格式:{pattern.group(1)[:30]}") # 截断过长正则
116
+
117
+ # 提取 @Schema 或 @ApiModelProperty 中的 description (中文注释)
118
+ schema_desc = re.search(r'@(?:Schema|ApiModelProperty)\([^)]*(?:description|value)\s*=\s*"([^"]+)"', block)
119
+ if schema_desc:
120
+ desc_text = schema_desc.group(1)
121
+ # 优先使用包含中文的描述
122
+ if re.search(r'[\u4e00-\u9fff]', desc_text):
123
+ return constraints, desc_text
124
+
125
+ return constraints, None
126
+
127
+ # ================= 3. 解析逻辑 =================
128
+
129
+ def parse_java_enum(content, class_name):
130
+ start_match = re.search(r'public\s+enum\s+\w+\s*\{', content)
131
+ if not start_match: return []
132
+
133
+ start_idx = start_match.end()
134
+ end_idx = content.find(';', start_idx)
135
+ if end_idx == -1: end_idx = content.rfind('}')
136
+
137
+ body = content[start_idx:end_idx]
138
+ body = re.sub(r'//.*', '', body)
139
+ body = re.sub(r'/\*.*?\*/', '', body, flags=re.DOTALL)
140
+
141
+ raw_items = body.split(',')
142
+ enum_items = []
143
+ auto_idx = 0
144
+
145
+ for item in raw_items:
146
+ item = item.strip()
147
+ if not item: continue
148
+
149
+ name_match = re.match(r'([A-Z0-9_]+)', item)
150
+ if not name_match: continue
151
+ name = name_match.group(1)
152
+
153
+ val_match = re.search(r'\(\s*(\d+)', item)
154
+ if val_match:
155
+ val = int(val_match.group(1))
156
+ else:
157
+ val = auto_idx
158
+ auto_idx += 1
159
+
160
+ enum_items.append({'name': name, 'value': val})
161
+
162
+ enum_items.sort(key=lambda x: x['value'])
163
+ if not enum_items or enum_items[0]['value'] != 0:
164
+ prefix = camel_to_snake(class_name).upper()
165
+ enum_items.insert(0, {'name': f"{prefix}_UNSPECIFIED", 'value': 0})
166
+
167
+ return enum_items
168
+
169
+ def parse_java_class(content):
170
+ """
171
+ 解析 Class,返回 (字段列表, 依赖导入集合)
172
+ 支持两种模式:
173
+ 1. 字段声明上的注解和注释
174
+ 2. Getter 方法中的 JavaDoc 注释(jOOQ 风格)
175
+ """
176
+ fields = []
177
+ imports = set()
178
+
179
+ if 'extends BaseEntity' in content:
180
+ fields.extend(BASE_ENTITY_FIELDS)
181
+
182
+ lines = content.split('\n')
183
+ buf_anno, buf_doc = [], ""
184
+
185
+ field_pat = re.compile(r'private\s+([\w<>?]+)\s+(\w+)\s*;')
186
+ json_pat = re.compile(r'@JsonProperty\("([^"]+)"\)')
187
+
188
+ # 首先尝试从字段声明处理(带注解的方式)
189
+ for i, line in enumerate(lines):
190
+ line_stripped = line.strip()
191
+ if not line_stripped: continue
192
+
193
+ if line_stripped.startswith('/**'):
194
+ buf_doc = re.sub(r'/\*\*|\*/|\*', '', line_stripped).strip()
195
+ continue
196
+ if line_stripped.startswith('*'):
197
+ clean = line_stripped.replace('*', '').strip()
198
+ if clean and clean != '/': buf_doc += " " + clean
199
+ continue
200
+ if line_stripped.startswith('@'):
201
+ buf_anno.append(line_stripped)
202
+ continue
203
+ if 'static final' in line_stripped:
204
+ buf_anno, buf_doc = [], ""
205
+ continue
206
+
207
+ match = field_pat.search(line)
208
+ if match:
209
+ j_type, raw_name = match.group(1), match.group(2)
210
+
211
+ # 1. 命名:转为 snake_case
212
+ final_name = camel_to_snake(raw_name)
213
+ for anno in buf_anno:
214
+ jp = json_pat.search(anno)
215
+ if jp: final_name = jp.group(1); break
216
+
217
+ # 2. 类型映射与依赖分析
218
+ p_type, label, target_type_for_import = _map_java_type(j_type)
219
+
220
+ # 依赖收集逻辑
221
+ if target_type_for_import:
222
+ import_file = f"{camel_to_snake(target_type_for_import)}.proto"
223
+ imports.add(import_file)
224
+
225
+ # 3. 注释和约束提取
226
+ constraints, schema_desc = extract_constraints(buf_anno)
227
+
228
+ # 提取行尾注释
229
+ eol_cmt = lines[i].split('//')[1].strip() if '//' in lines[i] else ""
230
+
231
+ # 优先级:Schema description (中文) > JavaDoc > 行尾注释
232
+ if schema_desc:
233
+ base_desc = schema_desc
234
+ elif buf_doc:
235
+ base_desc = buf_doc
236
+ elif eol_cmt:
237
+ base_desc = eol_cmt
238
+ else:
239
+ base_desc = ""
240
+
241
+ # **追加 Java 属性名(大小写敏感)到注释开头**
242
+ java_field_hint = f"[Java:{raw_name}]"
243
+
244
+ # 构建最终注释:Java属性名 + 描述 + 约束
245
+ if base_desc and constraints:
246
+ final_cmt = f"{java_field_hint} {base_desc} ({', '.join(constraints)})"
247
+ elif base_desc:
248
+ final_cmt = f"{java_field_hint} {base_desc}"
249
+ elif constraints:
250
+ final_cmt = f"{java_field_hint} ({', '.join(constraints)})"
251
+ else:
252
+ final_cmt = java_field_hint
253
+
254
+ fields.append({'name': final_name, 'type': p_type, 'label': label, 'comment': final_cmt})
255
+ buf_anno, buf_doc = [], ""
256
+
257
+ # 如果字段列表为空、只有 BaseEntity,或大部分字段无注释(jOOQ 风格),尝试从 Getter 提取
258
+ has_meaningful_comments = sum(1 for f in fields if f.get('comment') and f.get('comment').strip()) > len(fields) * 0.5
259
+
260
+ if len(fields) <= len(BASE_ENTITY_FIELDS) or not has_meaningful_comments:
261
+ fields_from_getters = _extract_fields_from_getters(content)
262
+ if fields_from_getters:
263
+ # 清空字段,重新从 getter 提取
264
+ fields = []
265
+ if 'extends BaseEntity' in content or 'implements VertxPojo' in content:
266
+ fields.extend(BASE_ENTITY_FIELDS)
267
+ fields.extend(fields_from_getters)
268
+
269
+ return fields, imports
270
+
271
+ def _map_java_type(j_type):
272
+ """映射 Java 类型到 Proto 类型"""
273
+ p_type = j_type
274
+ label = ""
275
+ target_type_for_import = None
276
+
277
+ if j_type.startswith("List<"):
278
+ label = "repeated "
279
+ inner = re.search(r'List<(\w+)>', j_type)
280
+ if inner:
281
+ inner_type = inner.group(1)
282
+ if inner_type in TYPE_MAPPING:
283
+ p_type = TYPE_MAPPING[inner_type]
284
+ else:
285
+ p_type = inner_type
286
+ target_type_for_import = inner_type
287
+ else:
288
+ if j_type in TYPE_MAPPING:
289
+ p_type = TYPE_MAPPING[j_type]
290
+ else:
291
+ p_type = j_type
292
+ target_type_for_import = j_type
293
+
294
+ return p_type, label, target_type_for_import
295
+
296
+ def _extract_fields_from_getters(content):
297
+ """
298
+ 从 Getter 方法中提取字段信息(用于 jOOQ 生成的代码)
299
+ 匹配格式: /** Getter for <code>ZDB.TABLE.FIELD</code>. 「fieldName」- 描述 */
300
+ 返回字段时附带 Java 属性名
301
+ """
302
+ fields = []
303
+
304
+ # 匹配 getter 方法及其 JavaDoc
305
+ getter_pattern = re.compile(
306
+ r'/\*\*\s*Getter for[^*]*?\*/' # JavaDoc
307
+ r'\s*@Override\s*' # @Override
308
+ r'public\s+([\w<>?]+)\s+get(\w+)\(\)', # 方法签名
309
+ re.DOTALL
310
+ )
311
+
312
+ for match in getter_pattern.finditer(content):
313
+ javadoc = match.group(0)
314
+ j_type = match.group(1)
315
+ field_name_camel = match.group(2) # 如 CreatedAt
316
+
317
+ # 提取 Java 属性名(getter 去掉 get 后首字母小写)
318
+ java_prop_name = field_name_camel[0].lower() + field_name_camel[1:] if field_name_camel else field_name_camel
319
+
320
+ # 提取中文注释:「fieldName」- 描述
321
+ chinese_match = re.search(r'[「」]([^」]+)[」][\s\-—]*([^*\n]+)', javadoc)
322
+ if chinese_match:
323
+ description = chinese_match.group(2).strip() # 如 "主键"
324
+ else:
325
+ # 回退:提取任何中文内容
326
+ chinese_text = re.findall(r'[\u4e00-\u9fff]+', javadoc)
327
+ description = ''.join(chinese_text) if chinese_text else ''
328
+
329
+ # 字段名转换为 snake_case
330
+ field_name = camel_to_snake(java_prop_name)
331
+
332
+ # 类型映射
333
+ p_type, label, _ = _map_java_type(j_type)
334
+
335
+ # **追加 Java 属性名**
336
+ java_field_hint = f"[Java:{java_prop_name}]"
337
+ final_comment = f"{java_field_hint} {description}" if description else java_field_hint
338
+
339
+ fields.append({
340
+ 'name': field_name,
341
+ 'type': p_type,
342
+ 'label': label,
343
+ 'comment': final_comment
344
+ })
345
+
346
+ return fields
347
+
348
+ def generate_proto(name, pkg, content, ftype):
349
+ lines = [
350
+ 'syntax = "proto3";',
351
+ 'package domain;', ''
352
+ ]
353
+
354
+ # 只有 class 类型才需要 import
355
+ imports = set()
356
+ fields = []
357
+ enum_items = []
358
+
359
+ if ftype == 'enum':
360
+ enum_items = parse_java_enum(content, name)
361
+ if not enum_items: return None
362
+ else:
363
+ # 解析字段并获取依赖
364
+ fields, imports = parse_java_class(content)
365
+ if not fields: return None
366
+
367
+ # 生成 import 语句
368
+ self_import = f"{camel_to_snake(name)}.proto"
369
+ if self_import in imports:
370
+ imports.remove(self_import)
371
+
372
+ if imports:
373
+ sorted_imports = sorted(list(imports))
374
+ for imp in sorted_imports:
375
+ lines.append(f'import "{imp}";')
376
+ lines.append('') # 空行分隔
377
+
378
+ lines.append(f'// Generated from {pkg}.{name}')
379
+ lines.append(f'option java_package = "{pkg}";')
380
+ lines.append('option java_multiple_files = true;')
381
+ lines.append('')
382
+
383
+ if ftype == 'enum':
384
+ lines.append(f'enum {name} {{')
385
+ for i in enum_items: lines.append(f' {i["name"]} = {i["value"]};')
386
+ lines.append('}')
387
+ else:
388
+ lines.append(f'message {name} {{')
389
+ for idx, f in enumerate(fields, 1):
390
+ # BaseEntity 等字段用 java_name 拼注释;其余字段 comment 已含 [Java:xxx]
391
+ comment_str = f.get("comment") or ""
392
+ if f.get("java_name"):
393
+ comment_str = f"[Java:{f['java_name']}] {comment_str}".strip() if comment_str else f"[Java:{f['java_name']}]"
394
+ suf = f' // {comment_str}' if comment_str else ''
395
+ lines.append(f' {f["label"]}{f["type"]} {f["name"]} = {idx};{suf}')
396
+ lines.append('}')
397
+
398
+ return "\n".join(lines)
399
+
400
+ # ================= 4. 主程序 =================
401
+
402
+ def main():
403
+ parser = argparse.ArgumentParser(description='R2MO Java-to-Proto (V8 - Domain Mode)')
404
+ parser.add_argument('-i', '--input', help='指定输入目录')
405
+ parser.add_argument('-o', '--output', help='指定输出目录')
406
+ args = parser.parse_args()
407
+
408
+ cwd = os.getcwd()
409
+ project_name = get_project_name()
410
+
411
+ if args.input: input_dir = os.path.abspath(args.input)
412
+ else: input_dir = os.path.join(cwd, f"{project_name}-domain")
413
+
414
+ if args.output: output_dir = os.path.abspath(args.output)
415
+ else: output_dir = os.path.join(cwd, f"{project_name}-ui", ".r2mo", "domain")
416
+
417
+ if not os.path.exists(input_dir):
418
+ if f"{project_name}-domain" in cwd: input_dir = cwd
419
+ else:
420
+ print(f"❌ 输入目录不存在: {input_dir}"); sys.exit(1)
421
+
422
+ if not os.path.exists(output_dir): os.makedirs(output_dir)
423
+
424
+ print(f"🚀 R2MO Proto Generator (V9 - Domain Mode)")
425
+ print(f" In: {input_dir}")
426
+ print(f" Out: {output_dir}")
427
+ print("-" * 40)
428
+
429
+ count = 0
430
+ for root, dirs, files in os.walk(input_dir):
431
+ for f in files:
432
+ if f.endswith(".java") and f != "BaseEntity.java":
433
+ path = os.path.join(root, f)
434
+ try:
435
+ with open(path, 'r', encoding='utf-8') as fh: content = fh.read()
436
+
437
+ is_enum = 'public enum' in content
438
+ match = re.search(r'public\s+(class|enum)\s+(\w+)', content)
439
+
440
+ if match:
441
+ name = match.group(2)
442
+ pkg = get_package_name(content)
443
+ ftype = 'enum' if is_enum else 'class'
444
+
445
+ proto = generate_proto(name, pkg, content, ftype)
446
+ if proto:
447
+ out_name = f"{camel_to_snake(name)}.proto"
448
+ with open(os.path.join(output_dir, out_name), 'w') as fh: fh.write(proto)
449
+ count += 1
450
+ print(f"✅ {name} -> {out_name}")
451
+ except Exception as e:
452
+ print(f"⚠️ Skip {f}: {e}")
453
+
454
+ print("-" * 40)
455
+ print(f"🎉 处理完成: {count} 个文件")
456
+
457
+ if __name__ == "__main__":
458
+ main()
@@ -3,7 +3,12 @@
3
3
  * 交互式菜单 (Raw Mode)
4
4
  */
5
5
  const readline = require('readline');
6
- const clearScreen = () => process.stdout.write('\x1B[2J\x1B[0f');
6
+
7
+ // 使用更精确的光标控制
8
+ const clearScreen = () => {
9
+ // 清屏并移动到左上角
10
+ process.stdout.write('\x1B[2J\x1B[0f');
11
+ };
7
12
 
8
13
  const _baseSelect = (items, title, isMulti) => new Promise(resolve => {
9
14
  let cursor = 0;
@@ -11,13 +16,17 @@ const _baseSelect = (items, title, isMulti) => new Promise(resolve => {
11
16
  const maxLen = Math.max(...items.map(i => (i.name||'').length), 4);
12
17
 
13
18
  const render = () => {
14
- clearScreen();
15
- console.log(`\n[Momo AI]`.blue.bold + ` ====== ${title} ======`.blue + '\n');
19
+ // 清屏并移动到左上角(使用组合命令确保光标位置正确)
20
+ process.stdout.write('\x1B[2J\x1B[H');
21
+ // 输出内容
22
+ process.stdout.write(`\n[Momo AI]`.blue.bold + ` ====== ${title} ======`.blue + '\n');
16
23
  items.forEach((item, i) => {
17
24
  const active = i === cursor, check = isMulti ? (selected[i] ? '[✓]'.green : '[ ]') : '';
18
- console.log(` ${active ? '▸'.cyan : ' '} ${check} ${(item.name||'').padEnd(maxLen)[active?'cyan':'reset']}${active?''.bold:''} ${(item.description||'').gray}`);
25
+ const nameStr = (item.name||'').padEnd(maxLen);
26
+ const nameDisplay = active ? nameStr.cyan.bold : nameStr;
27
+ process.stdout.write(` ${active ? '▸'.cyan : ' '} ${check} ${nameDisplay} ${(item.description||'').gray}\n`);
19
28
  });
20
- console.log('\n ' + (isMulti ? 'Space:Toggle A:All N:None ' : '') + 'Enter:Confirm Q:Quit'.gray + '\n');
29
+ process.stdout.write('\n ' + (isMulti ? 'Space:切换 A:全选 N:取消 ' : '') + 'Enter:确定 Q:退出'.gray + '\n');
21
30
  };
22
31
 
23
32
  readline.emitKeypressEvents(process.stdin);
@@ -29,12 +38,27 @@ const _baseSelect = (items, title, isMulti) => new Promise(resolve => {
29
38
  if ((key.ctrl && key.name === 'c') || key.name === 'q' || key.name === 'escape') {
30
39
  cleanup(); resolve(isMulti ? { indices: [], items: [] } : null); return;
31
40
  }
32
- if (key.name === 'up') { cursor = cursor > 0 ? cursor - 1 : items.length - 1; render(); }
33
- if (key.name === 'down') { cursor = cursor < items.length - 1 ? cursor + 1 : 0; render(); }
41
+ if (key.name === 'up') {
42
+ cursor = cursor > 0 ? cursor - 1 : items.length - 1;
43
+ render();
44
+ }
45
+ if (key.name === 'down') {
46
+ cursor = cursor < items.length - 1 ? cursor + 1 : 0;
47
+ render();
48
+ }
34
49
  if (isMulti) {
35
- if (key.name === 'space') { selected[cursor] = !selected[cursor]; render(); }
36
- if (key.name === 'a') { selected.fill(true); render(); }
37
- if (key.name === 'n') { selected.fill(false); render(); }
50
+ if (key.name === 'space') {
51
+ selected[cursor] = !selected[cursor];
52
+ render();
53
+ }
54
+ if (key.name === 'a') {
55
+ selected.fill(true);
56
+ render();
57
+ }
58
+ if (key.name === 'n') {
59
+ selected.fill(false);
60
+ render();
61
+ }
38
62
  }
39
63
  if (key.name === 'return') {
40
64
  cleanup();
@@ -43,12 +67,18 @@ const _baseSelect = (items, title, isMulti) => new Promise(resolve => {
43
67
  }
44
68
  };
45
69
 
46
- const cleanup = () => { process.stdin.setRawMode(false); process.stdin.removeListener('keypress', onKey); clearScreen(); };
70
+ const cleanup = () => {
71
+ process.stdin.setRawMode(false);
72
+ process.stdin.removeListener('keypress', onKey);
73
+ // 清理时只清屏,不移动光标
74
+ process.stdout.write('\x1B[2J');
75
+ process.stdout.write('\x1B[H');
76
+ };
47
77
  process.stdin.on('keypress', onKey);
48
78
  });
49
79
 
50
80
  module.exports = {
51
- selectMultiple: (items, title) => _baseSelect(items, title || 'Select Options', true),
52
- selectSingle: (items, title) => _baseSelect(items, title || 'Select One', false),
81
+ selectMultiple: (items, title) => _baseSelect(items, title || '选择选项', true),
82
+ selectSingle: (items, title) => _baseSelect(items, title || '选择一项', false),
53
83
  clearScreen
54
84
  };