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.
- package/.claude/skills/r2mo-rad-lain/SKILL.md +63 -374
- package/.trae/skills/algorithmic-art/LICENSE.txt +202 -0
- package/.trae/skills/algorithmic-art/SKILL.md +405 -0
- package/.trae/skills/algorithmic-art/templates/generator_template.js +223 -0
- package/.trae/skills/algorithmic-art/templates/viewer.html +599 -0
- package/.trae/skills/doc-coauthoring/SKILL.md +375 -0
- package/.trae/skills/frontend-design/LICENSE.txt +177 -0
- package/.trae/skills/frontend-design/SKILL.md +42 -0
- package/.trae/skills/r2mo-rad-lain/SKILL.md +101 -0
- package/README.md +9 -32
- package/docs/images/r2mo-lain.png +0 -0
- package/package.json +11 -11
- package/skills/r2mo-rad-domain/SKILL.md +70 -0
- package/src/_skill/repositories.json +9 -3
- package/src/_template/LAIN/.obsidian/app.json +1 -0
- package/src/_template/LAIN/.obsidian/appearance.json +10 -0
- package/src/_template/LAIN/.obsidian/community-plugins.json +7 -0
- package/src/_template/LAIN/.obsidian/core-plugins.json +33 -0
- package/src/_template/LAIN/.obsidian/plugins/dataview/main.js +20876 -0
- package/src/_template/LAIN/.obsidian/plugins/dataview/manifest.json +11 -0
- package/src/_template/LAIN/.obsidian/plugins/dataview/styles.css +141 -0
- package/src/_template/LAIN/.obsidian/plugins/obsidian-excalidraw-plugin/data.json +815 -0
- package/src/_template/LAIN/.obsidian/plugins/obsidian-excalidraw-plugin/main.js +10 -0
- package/src/_template/LAIN/.obsidian/plugins/obsidian-excalidraw-plugin/manifest.json +12 -0
- package/src/_template/LAIN/.obsidian/plugins/obsidian-excalidraw-plugin/styles.css +1 -0
- package/src/_template/LAIN/.obsidian/plugins/obsidian-kanban/main.js +153 -0
- package/src/_template/LAIN/.obsidian/plugins/obsidian-kanban/manifest.json +11 -0
- package/src/_template/LAIN/.obsidian/plugins/obsidian-kanban/styles.css +1 -0
- package/src/_template/LAIN/.obsidian/plugins/obsidian-plantuml/main.js +7732 -0
- package/src/_template/LAIN/.obsidian/plugins/obsidian-plantuml/manifest.json +10 -0
- package/src/_template/LAIN/.obsidian/plugins/obsidian-plantuml/styles.css +38 -0
- package/src/_template/LAIN/.obsidian/plugins/obsidian-tasks-plugin/main.js +504 -0
- package/src/_template/LAIN/.obsidian/plugins/obsidian-tasks-plugin/manifest.json +12 -0
- package/src/_template/LAIN/.obsidian/plugins/obsidian-tasks-plugin/styles.css +1 -0
- package/src/_template/LAIN/.obsidian/snippets/body-font.css +27 -0
- package/src/_template/LAIN/.obsidian/themes/Primary/manifest.json +9 -0
- package/src/_template/LAIN/.obsidian/themes/Primary/theme.css +3878 -0
- package/src/_template/LAIN/.obsidian/themes/Retro Windows/manifest.json +7 -0
- package/src/_template/LAIN/.obsidian/themes/Retro Windows/theme.css +582 -0
- package/src/_template/LAIN/.obsidian/themes/RetroOS 98/manifest.json +9 -0
- package/src/_template/LAIN/.obsidian/themes/RetroOS 98/theme.css +2566 -0
- package/src/_template/LAIN/.obsidian/types.json +28 -0
- package/src/_template/LAIN/.obsidian/workspace.json +184 -0
- package/src/_template/LAIN/AGENTS.md +170 -16
- package/src/_template/R2MO/domain-enhance.md +10 -0
- package/src/commander/app.json +13 -0
- package/src/commander/apply.json +13 -0
- package/src/commander/ask.json +6 -0
- package/src/commander/docs.json +13 -0
- package/src/commander/domain.json +19 -0
- package/src/commander/init.json +1 -1
- package/src/commander/mmr0.json +6 -0
- package/src/commander/mmr2.json +6 -0
- package/src/executor/executeApp.js +133 -0
- package/src/executor/{executeSkills.js → executeApply.js} +166 -302
- package/src/executor/executeAsk.js +274 -0
- package/src/executor/executeDocs.js +498 -0
- package/src/executor/executeDomain.js +293 -0
- package/src/executor/executeInit.js +159 -383
- package/src/executor/executeMcp.js +74 -1
- package/src/executor/executeMmr0.js +488 -0
- package/src/executor/executeMmr2.js +880 -0
- package/src/executor/index.js +15 -3
- package/src/python/r2mo_proto.py +418 -0
- package/src/python/r2mo_proto_database.py +369 -0
- package/src/python/r2mo_proto_domain.py +458 -0
- package/src/utils/momo-menu.js +43 -13
- package/.claude/skills/r2mo-rad-lain/PROMPT.md +0 -281
- package/.claude/skills/r2mo-rad-lain/README.md +0 -192
- package/.claude/skills/r2mo-rad-lain/examples/argument-parsing.js +0 -154
- package/.claude/skills/r2mo-rad-lain/examples/file-operations.js +0 -182
- package/.claude/skills/r2mo-rad-lain/file-utils-api.md +0 -281
- package/.claude/skills/r2mo-rad-lain/menu-api.md +0 -187
- package/.claude/skills/r2mo-rad-lain/scripts/file-utils.js +0 -223
- package/.claude/skills/r2mo-rad-lain/scripts/menu.js +0 -289
- package/.claude/skills/r2mo-rad-lain/scripts/yaml-parser.js +0 -209
- package/.claude/skills/r2mo-rad-lain/templates/command.json.template +0 -13
- package/.claude/skills/r2mo-rad-lain/templates/executor.js.template +0 -32
- package/.claude/skills/r2mo-rad-lain/templates/interactive-menu.js.template +0 -221
- package/src/_template/LAIN/.momo/advanced/actor.md +0 -42
- package/src/_template/LAIN/.momo/advanced/refer.json +0 -46
- package/src/_template/LAIN/.momo/scripts/submodule-clean.sh +0 -56
- package/src/_template/LAIN/changes/proposal.md +0 -39
- package/src/_template/LAIN/changes/tasks/task-detail.md +0 -45
- package/src/_template/LAIN/changes/tasks.md +0 -49
- package/src/_template/LAIN/execute/admin-n-f-dashboard.md +0 -53
- package/src/_template/LAIN/execute/admin-n-f-form.md +0 -51
- package/src/_template/LAIN/execute/admin-n-f-home.md +0 -49
- package/src/_template/LAIN/execute/admin-n-f-list.md +0 -52
- package/src/_template/LAIN/execute/admin-n-f-login.md +0 -56
- package/src/_template/LAIN/specification/project-model.md +0 -13
- package/src/_template/LAIN/specification/project.md +0 -73
- package/src/_template/LAIN/specification/requirement.md +0 -25
- 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()
|
package/src/utils/momo-menu.js
CHANGED
|
@@ -3,7 +3,12 @@
|
|
|
3
3
|
* 交互式菜单 (Raw Mode)
|
|
4
4
|
*/
|
|
5
5
|
const readline = require('readline');
|
|
6
|
-
|
|
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
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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') {
|
|
33
|
-
|
|
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') {
|
|
36
|
-
|
|
37
|
-
|
|
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 = () => {
|
|
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 || '
|
|
52
|
-
selectSingle: (items, title) => _baseSelect(items, title || '
|
|
81
|
+
selectMultiple: (items, title) => _baseSelect(items, title || '选择选项', true),
|
|
82
|
+
selectSingle: (items, title) => _baseSelect(items, title || '选择一项', false),
|
|
53
83
|
clearScreen
|
|
54
84
|
};
|