dsh-contextos-devops 0.0.0-stage → 1.48.31
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/LICENSE +21 -0
- package/README.md +207 -2
- package/assets/avatar.jpg +0 -0
- package/assets/client.css +3316 -0
- package/assets/icon-180.png +0 -0
- package/build/client.mjs +64 -0
- package/client.js +7622 -0
- package/cordis.patch.yml +3 -0
- package/icon.svg +15 -0
- package/index.js +345 -0
- package/lib/base/env.js +22 -0
- package/lib/base/field.js +25 -0
- package/lib/base/http.js +140 -0
- package/lib/base/platform.js +33 -0
- package/lib/base/registry.js +186 -0
- package/lib/base/state.js +66 -0
- package/lib/base/table.js +47 -0
- package/lib/base/texttable.js +26 -0
- package/lib/base/url.js +178 -0
- package/lib/core/README.md +17 -0
- package/lib/core/apps.js +675 -0
- package/lib/core/auth.js +110 -0
- package/lib/core/avatar.js +289 -0
- package/lib/core/backup.js +105 -0
- package/lib/core/codegen.js +401 -0
- package/lib/core/copilot.js +419 -0
- package/lib/core/data.js +157 -0
- package/lib/core/device.js +148 -0
- package/lib/core/files.js +152 -0
- package/lib/core/gobuild.js +63 -0
- package/lib/core/iam.js +122 -0
- package/lib/core/identity.js +22 -0
- package/lib/core/nodes.js +627 -0
- package/lib/core/ops.js +306 -0
- package/lib/core/project.js +393 -0
- package/lib/core/spaces.js +406 -0
- package/lib/core/terminal.js +137 -0
- package/lib/core/terminal2.js +141 -0
- package/lib/core/users.js +51 -0
- package/lib/core/vocab.js +158 -0
- package/lib/misc/async.js +15 -0
- package/lib/misc/shell.js +27 -0
- package/lib/misc/static.js +33 -0
- package/locale/en.json +10 -0
- package/locale/zh.json +10 -0
- package/package.json +91 -4
- package/src/client/README.md +37 -0
- package/src/client/parts/00-prelude.js +315 -0
- package/src/client/parts/05-bootstrap.js +61 -0
- package/src/client/parts/08-store.js +85 -0
- package/src/client/parts/10-ui.js +188 -0
- package/src/client/parts/12-debug.js +361 -0
- package/src/client/parts/15-layout.js +86 -0
- package/src/client/parts/20-devworkspace.js +789 -0
- package/src/client/parts/25-editor-ui.js +28 -0
- package/src/client/parts/30-modals.js +85 -0
- package/src/client/parts/32-cards.js +176 -0
- package/src/client/parts/34-forms.js +176 -0
- package/src/client/parts/36-data.js +267 -0
- package/src/client/parts/40-project.js +430 -0
- package/src/client/parts/45-map.js +109 -0
- package/src/client/parts/46-pm.js +158 -0
- package/src/client/parts/47-three.js +324 -0
- package/src/client/parts/49-msg.js +167 -0
- package/src/client/parts/50-chat.js +1422 -0
- package/src/client/parts/51-pmselection.js +31 -0
- package/src/client/parts/52-pmdata.js +29 -0
- package/src/client/parts/55-pm.js +146 -0
- package/src/client/parts/58-config.js +381 -0
- package/src/client/parts/59-monitor.js +262 -0
- package/src/client/parts/60-page.js +1518 -0
- package/src/client/styles/kit.css +20 -0
- package/src/client/styles/main.css +3294 -0
- package/src/client/styles/tokens.css +2 -0
- package/vendor/leaflet/leaflet.css +661 -0
- package/vendor/leaflet/leaflet.js +6 -0
- package/vendor/three/three.module.js +53044 -0
- package/vendor/xterm/addon-fit.js +2 -0
- package/vendor/xterm/xterm.css +218 -0
- package/vendor/xterm/xterm.js +2 -0
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
// 代码生成:把「加动作 / 加条件按键 / 加字段 / 新写功能」写成模板,按用户需求渲染并落盘。
|
|
2
|
+
// 规范真源见 docs/CODE-GEN.md;本文件只做参数替换与文件写入,不发明新写法。
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import { moduleSkeleton, guardOf } from './vocab.js';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
|
|
7
|
+
const IDENT = /^[a-zA-Z][a-zA-Z0-9_]*$/;
|
|
8
|
+
const ZONE = /^[a-z][a-z0-9_]*$/;
|
|
9
|
+
|
|
10
|
+
// 条件表达式:eq/ne/gt/lt/nonempty/empty
|
|
11
|
+
const OPS = {
|
|
12
|
+
eq: (a, b) => `${a} == "${b}"`,
|
|
13
|
+
ne: (a, b) => `${a} != "${b}"`,
|
|
14
|
+
gt: (a, b) => `${a} > "${b}"`,
|
|
15
|
+
lt: (a, b) => `${a} < "${b}"`,
|
|
16
|
+
nonempty: (a) => `${a} != ""`,
|
|
17
|
+
empty: (a) => `${a} == ""`,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function codegenCond({ scope, field = 'status', op = 'eq', value = '' } = {}) {
|
|
21
|
+
const expr = scope === 'toolbar' ? `m.Option("${field}")` : `value["${field}"]`;
|
|
22
|
+
return (OPS[op] || OPS.eq)(expr, value);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const decl = (v) => {
|
|
26
|
+
const attrs = [`name:"${v.action} ${v.params || 'title*:text content:textarea'}"`];
|
|
27
|
+
const help = v.help || v.label || v.Action;
|
|
28
|
+
if (help) attrs.push(`help:"${help}"`);
|
|
29
|
+
if (v.icon) attrs.push(`icon:"${v.icon}"`);
|
|
30
|
+
if (v.role) attrs.push(`role:"${v.role}"`);
|
|
31
|
+
if (v.style) attrs.push(`style:"${v.style}"`);
|
|
32
|
+
return `\t${v.action} string \`${attrs.join(' ')}\`\n`;
|
|
33
|
+
};
|
|
34
|
+
const actionFn = (v) => {
|
|
35
|
+
// 系统级模块没有成员角色(没有 Table),成员级检查只能靠声明上的 role
|
|
36
|
+
const memberRole = ['creator', 'leader', 'worker', 'member', 'visitor'].includes(String(v.role || ''));
|
|
37
|
+
const guard = (v.class === 'system' && memberRole) ? '' : guardOf(v.role);
|
|
38
|
+
return `
|
|
39
|
+
func (s ${v.module}) ${v.Action}(m *ice.Message, arg ...string) {
|
|
40
|
+
${guard ? '\t' + guard + '\n' : ''}\ts.Create(m, arg...)
|
|
41
|
+
\ts.List(m)
|
|
42
|
+
}
|
|
43
|
+
`;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const T = [
|
|
47
|
+
{
|
|
48
|
+
key: 'action',
|
|
49
|
+
title: '给功能加一个动作',
|
|
50
|
+
vars: ['zone', 'module', 'Action', 'action', 'params'],
|
|
51
|
+
file: (v) => `${v.zone}/${v.module}.go`,
|
|
52
|
+
body: decl,
|
|
53
|
+
actionMethod: actionFn,
|
|
54
|
+
note: '结构体加一行声明 + 一个方法;再在 List 里挂按钮',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
key: 'field',
|
|
58
|
+
title: '给功能加一个字段',
|
|
59
|
+
vars: ['zone', 'module', 'field', 'type'],
|
|
60
|
+
file: (v) => `${v.zone}/${v.module}.go`,
|
|
61
|
+
body: (v) => `\t${v.field} string \`name:"${v.field}" help:"${v.field}"\`\n`,
|
|
62
|
+
note: '字段加进 field data 标签,同时加进 create/modify 的 name 标签',
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
key: 'function',
|
|
66
|
+
title: '新建一个功能(模块文件)',
|
|
67
|
+
vars: ['zone', 'module'],
|
|
68
|
+
file: (v) => `${v.zone}/${v.module}.go`,
|
|
69
|
+
whole: (v) => `package ${v.zone}
|
|
70
|
+
|
|
71
|
+
import "shylinux.com/x/ice"
|
|
72
|
+
|
|
73
|
+
type ${v.module} struct {
|
|
74
|
+
\tice.Hash
|
|
75
|
+
\tshort string \`data:"index,title"\`
|
|
76
|
+
\tfield string \`data:"time,index,title,content,user_uid"\`
|
|
77
|
+
\tcreate string \`name:"create title* content* user_uid"\`
|
|
78
|
+
\tlist string \`name:"list index auto"\`
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
func init() { ice.Cmd("web.${v.zone}.${v.module}", ${v.module}{}) }
|
|
82
|
+
`,
|
|
83
|
+
note: '文件名 = 模块名;init 里注册命令 web.<zone>.<module>',
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
key: 'module',
|
|
87
|
+
title: '新建模块(含 Init/Exit/Inputs/Create/Modify/Remove/List)',
|
|
88
|
+
vars: ['zone', 'module'],
|
|
89
|
+
file: (v) => `${v.zone}/${v.module}.go`,
|
|
90
|
+
whole: (v) => `package ${v.zone}
|
|
91
|
+
|
|
92
|
+
import "shylinux.com/x/ice"
|
|
93
|
+
|
|
94
|
+
type ${v.module} struct {
|
|
95
|
+
\tice.Hash
|
|
96
|
+
\tshort string \`data:"index,title"\`
|
|
97
|
+
\tfield string \`data:"time,index,title,content,user_uid"\`
|
|
98
|
+
\tcreate string \`name:"create title*:text content:textarea user_uid"\`
|
|
99
|
+
\tlist string \`name:"list index auto"\`
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
func init() { ice.Cmd("web.${v.zone}.${v.module}", ${v.module}{}) }
|
|
103
|
+
${moduleSkeleton(v.module)}
|
|
104
|
+
`,
|
|
105
|
+
note: '七个常用函数按 Init→Exit→Inputs→Create→Modify→Remove→List 排列;已有同名文件拒绝覆盖',
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
key: 'buttons',
|
|
109
|
+
title: '列表每行挂操作按钮',
|
|
110
|
+
vars: ['module', 'Action', 'action'],
|
|
111
|
+
file: null,
|
|
112
|
+
method: (v) => `
|
|
113
|
+
func (s ${v.module}) List(m *ice.Message, arg ...string) *ice.Message {
|
|
114
|
+
\ts.Select(m, arg...)
|
|
115
|
+
\tm.Action(s.Create, s.${v.Action})
|
|
116
|
+
\tm.Table(func(value ice.Maps) {
|
|
117
|
+
\t\tm.PushButton(s.${v.Action}, s.Modify, s.Remove)
|
|
118
|
+
\t})
|
|
119
|
+
\treturn m
|
|
120
|
+
}
|
|
121
|
+
`,
|
|
122
|
+
note: 'PushButton 只在浏览器 UA 下渲染;curl 拿到的是数据',
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
key: 'toolbarButton',
|
|
126
|
+
title: '工具栏加条件按键',
|
|
127
|
+
vars: ['zone', 'module', 'Action', 'action', 'label', 'field', 'op', 'value', 'params'],
|
|
128
|
+
file: (v) => `${v.zone}/${v.module}.go`,
|
|
129
|
+
body: decl,
|
|
130
|
+
actionMethod: actionFn,
|
|
131
|
+
method: (v) => `
|
|
132
|
+
func (s ${v.module}) List(m *ice.Message, arg ...string) *ice.Message {
|
|
133
|
+
\ts.Select(m, arg...)
|
|
134
|
+
\tm.Action(s.Create)
|
|
135
|
+
\tif ${codegenCond({ ...v, scope: 'toolbar' })} {
|
|
136
|
+
\t\tm.Action(s.${v.Action})
|
|
137
|
+
\t}
|
|
138
|
+
\tm.Table(func(value ice.Maps) {
|
|
139
|
+
\t\tm.PushButton(s.Modify, s.Remove)
|
|
140
|
+
\t})
|
|
141
|
+
\treturn m
|
|
142
|
+
}
|
|
143
|
+
`,
|
|
144
|
+
note: '条件成立才显示工具栏按键;条件读 m.Option',
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
key: 'rowButton',
|
|
148
|
+
title: '行内加条件按键',
|
|
149
|
+
vars: ['zone', 'module', 'Action', 'action', 'label', 'field', 'op', 'value', 'params'],
|
|
150
|
+
file: (v) => `${v.zone}/${v.module}.go`,
|
|
151
|
+
body: decl,
|
|
152
|
+
actionMethod: actionFn,
|
|
153
|
+
method: (v) => `
|
|
154
|
+
func (s ${v.module}) List(m *ice.Message, arg ...string) *ice.Message {
|
|
155
|
+
\ts.Select(m, arg...)
|
|
156
|
+
\tm.Action(s.Create)
|
|
157
|
+
\tm.Table(func(value ice.Maps) {
|
|
158
|
+
\t\tif ${codegenCond({ ...v, scope: 'row' })} {
|
|
159
|
+
\t\t\tm.PushButton(s.${v.Action}, s.Modify, s.Remove)
|
|
160
|
+
\t\t} else {
|
|
161
|
+
\t\t\tm.PushButton(s.Modify, s.Remove)
|
|
162
|
+
\t\t}
|
|
163
|
+
\t})
|
|
164
|
+
\treturn m
|
|
165
|
+
}
|
|
166
|
+
`,
|
|
167
|
+
note: '条件读每行的 value,成立才显示该按钮',
|
|
168
|
+
},
|
|
169
|
+
];
|
|
170
|
+
|
|
171
|
+
export function codegenList() {
|
|
172
|
+
return T.map((t) => ({ key: t.key, title: t.title, vars: t.vars, note: t.note }));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function codegenRender({ template, vars = {} } = {}) {
|
|
176
|
+
const t = T.find((x) => x.key === template);
|
|
177
|
+
if (!t) throw new Error(`unknown template: ${template}`);
|
|
178
|
+
const v = { ...vars };
|
|
179
|
+
for (const k of t.vars) if (!v[k]) throw new Error(`missing var: ${k}`);
|
|
180
|
+
for (const k of ['zone', 'module', 'Action', 'action']) if (v[k] && !IDENT.test(v[k])) throw new Error(`bad ${k}: ${v[k]}`);
|
|
181
|
+
if (v.zone && !ZONE.test(v.zone)) throw new Error(`bad zone: ${v.zone}`);
|
|
182
|
+
if (v.field && !IDENT.test(v.field)) throw new Error(`bad field: ${v.field}`);
|
|
183
|
+
return {
|
|
184
|
+
template: t.key,
|
|
185
|
+
file: t.file ? t.file(v) : null,
|
|
186
|
+
body: t.whole ? t.whole(v) : (t.body ? t.body(v) : ''),
|
|
187
|
+
actionMethod: t.actionMethod ? t.actionMethod(v) : '',
|
|
188
|
+
method: t.method ? t.method(v) : '',
|
|
189
|
+
note: t.note,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// 把声明插进结构体、把方法追加到文件末尾(不覆盖已有内容)
|
|
194
|
+
export function codegenMerge({ existing = '', decl = '', methods = '' } = {}) {
|
|
195
|
+
let out = String(existing);
|
|
196
|
+
const d = String(decl || '').trimEnd();
|
|
197
|
+
if (d) {
|
|
198
|
+
const head = (d.trim().match(/^(\w+)/) || [])[1] || '';
|
|
199
|
+
const dup = head ? new RegExp('^\\s*' + head + '\\s+string\\s+\\x60[^\\x60]*\\x60[^\\n]*$', 'm') : null;
|
|
200
|
+
if (d.trim() && out.includes(d.trim())) {
|
|
201
|
+
// 已声明且完全一致,跳过
|
|
202
|
+
} else if (dup && dup.test(out)) {
|
|
203
|
+
out = dedupeDecl(out, head, d.trim()); // 同名声明:保留一行并补 role/help/style
|
|
204
|
+
} else {
|
|
205
|
+
const m = out.match(/^\s*(\w+)\s+string\s+`(?:name|data):"[^"]*"`\s*$/m);
|
|
206
|
+
if (m) {
|
|
207
|
+
const i = out.indexOf(m[0]) + m[0].length;
|
|
208
|
+
out = out.slice(0, i) + '\n' + d + out.slice(i);
|
|
209
|
+
} else {
|
|
210
|
+
out = out.replace(/(type\s+\w+\s+struct\s*\{\n)/, `$1${d}\n`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
const mo = String(methods || '').trim();
|
|
215
|
+
if (!mo) return out;
|
|
216
|
+
for (const fn of splitFuncs(mo)) {
|
|
217
|
+
if (out.includes(fn.text)) continue;
|
|
218
|
+
if (new RegExp(`func \\(s \\w+\\) ${fn.name}\\(`).test(out)) {
|
|
219
|
+
out = removeFunc(out, fn.name); // 同名方法:删掉全部旧实现
|
|
220
|
+
}
|
|
221
|
+
out = insertFunc(out, fn.name, fn.text);
|
|
222
|
+
}
|
|
223
|
+
return out;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function removeFunc(src, name) {
|
|
227
|
+
const re = new RegExp(`func \\(s \\w+\\) ${name}\\(`);
|
|
228
|
+
let out = src;
|
|
229
|
+
for (let guard = 0; guard < 20; guard++) {
|
|
230
|
+
const m = out.match(re);
|
|
231
|
+
if (!m) break;
|
|
232
|
+
const start = m.index;
|
|
233
|
+
const rest = out.slice(start + 1);
|
|
234
|
+
const nx = rest.search(/\nfunc \(s \w+\) /);
|
|
235
|
+
const end = nx >= 0 ? start + 1 + nx + 1 : out.length;
|
|
236
|
+
out = out.slice(0, start) + out.slice(end);
|
|
237
|
+
}
|
|
238
|
+
return out.replace(/\n{3,}/g, '\n\n');
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// 同名声明只留一行
|
|
242
|
+
function dedupeDecl(src, head, line) {
|
|
243
|
+
const re = new RegExp('^\\s*' + head + '\\s+string\\s+\\x60[^\\x60]*\\x60[^\\n]*$', 'gm');
|
|
244
|
+
const hits = src.match(re) || [];
|
|
245
|
+
if (!hits.length) return src;
|
|
246
|
+
let first = true;
|
|
247
|
+
return src.replace(re, () => {
|
|
248
|
+
if (first) { first = false; return line; }
|
|
249
|
+
return '\u0000';
|
|
250
|
+
}).replace(/\n\u0000/g, '').replace(/\u0000\n/g, '');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function splitFuncs(src) {
|
|
254
|
+
const parts = [];
|
|
255
|
+
const re = /func \(s \w+\) (\w+)\(/g;
|
|
256
|
+
const idx = [];
|
|
257
|
+
let m;
|
|
258
|
+
while ((m = re.exec(src))) idx.push({ at: m.index, name: m[1] });
|
|
259
|
+
idx.forEach((cur, i) => {
|
|
260
|
+
const end = i + 1 < idx.length ? idx[i + 1].at : src.length;
|
|
261
|
+
parts.push({ name: cur.name, text: src.slice(cur.at, end).trim() + '\n' });
|
|
262
|
+
});
|
|
263
|
+
return parts;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// 按 Init → Exit → Inputs → Create → Modify → Remove → 自定义 → List 插入
|
|
267
|
+
function insertFunc(src, name, text) {
|
|
268
|
+
const re = /func \(s \w+\) (\w+)\(/g;
|
|
269
|
+
const rank = funcRankOf(name);
|
|
270
|
+
let m;
|
|
271
|
+
while ((m = re.exec(src))) {
|
|
272
|
+
if (funcRankOf(m[1]) > rank) {
|
|
273
|
+
return src.slice(0, m.index) + text + '\n' + src.slice(m.index);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return src.replace(/\s*$/, '\n') + text + '\n';
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const ORDER = { Init: 0, Exit: 1, Inputs: 2, Create: 3, Modify: 4, Remove: 5, List: 7 };
|
|
280
|
+
function funcRankOf(name) {
|
|
281
|
+
return Object.prototype.hasOwnProperty.call(ORDER, name) ? ORDER[name] : 6;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// 只允许写进 <space>/src/<zone>/<file>
|
|
285
|
+
export function codegenPath({ root, space, file }) {
|
|
286
|
+
if (!/^[a-z][a-z0-9_]*\/[a-zA-Z][a-zA-Z0-9_]*\.go$/.test(file)) throw new Error(`bad file path: ${file}`);
|
|
287
|
+
const base = path.join(root, 'usr', 'local', 'work', space, 'src');
|
|
288
|
+
const target = path.join(base, file);
|
|
289
|
+
if (!target.startsWith(base + path.sep)) throw new Error('path escapes src');
|
|
290
|
+
return target;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export async function codegenWrite({ root, space, file, content }) {
|
|
294
|
+
const target = codegenPath({ root, space, file });
|
|
295
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
296
|
+
await fs.writeFile(target, content, 'utf8');
|
|
297
|
+
const st = await fs.stat(target);
|
|
298
|
+
return { file, bytes: st.size, target };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ---- 模块分类与字段插入 ----
|
|
302
|
+
// 系统级:ice.Hash(内存库,field 指定字段,short 指定索引)
|
|
303
|
+
// 平台级:db.Table(SQLite/MySQL,fields 指定字段,model/model.go 定义模型)
|
|
304
|
+
export function detectClass(src = '') {
|
|
305
|
+
if (/\bdb\.Table\b/.test(src)) return 'platform';
|
|
306
|
+
if (/\bfields\s+string\s+`(?:data|fields):/.test(src)) return 'platform';
|
|
307
|
+
if (/^\s*Table\s*$/m.test(src) && /\bmodel\./.test(src)) return 'platform';
|
|
308
|
+
if (/\bice\.Hash\b/.test(src)) return 'system';
|
|
309
|
+
return 'system';
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function readDataTag(src, keys) {
|
|
313
|
+
for (const k of keys) {
|
|
314
|
+
const re = new RegExp(`\\b${k}\\s+string\\s+\`(?:data|fields):"([^"]*)"\``);
|
|
315
|
+
const m = src.match(re);
|
|
316
|
+
if (m) return { key: k, list: m[1].split(',').map((x) => x.trim()).filter(Boolean), raw: m[0] };
|
|
317
|
+
}
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export function moduleInfo(src = '') {
|
|
322
|
+
const cls = detectClass(src);
|
|
323
|
+
const fieldKey = cls === 'platform' ? ['fields', 'field'] : ['field'];
|
|
324
|
+
const f = readDataTag(src, fieldKey);
|
|
325
|
+
const sh = readDataTag(src, ['short']);
|
|
326
|
+
const acts = [...src.matchAll(/^\s*(\w+)\s+string\s+`name:"([^"]*)"/gm)].map((m) => ({ name: m[1], decl: m[2] }));
|
|
327
|
+
return { class: cls, fields: f ? f.list : [], fieldKey: f ? f.key : fieldKey[0], short: sh ? sh.list : [], actions: acts };
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// 按逻辑顺序插入:user_uid 之前,time 之后;没有锚点就追加
|
|
331
|
+
export function insertField(list, field) {
|
|
332
|
+
const l = list.filter(Boolean);
|
|
333
|
+
if (l.includes(field)) return l;
|
|
334
|
+
const at = l.indexOf('user_uid');
|
|
335
|
+
if (at >= 0) l.splice(at, 0, field);
|
|
336
|
+
else l.push(field);
|
|
337
|
+
return l;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export function renderFields(src, field, cls = detectClass(src)) {
|
|
341
|
+
const keys = cls === 'platform' ? ['fields', 'field'] : ['field'];
|
|
342
|
+
const hit = readDataTag(src, keys);
|
|
343
|
+
if (!hit) throw new Error(`module has no ${keys[0]} tag`);
|
|
344
|
+
const next = insertField(hit.list, field).join(',');
|
|
345
|
+
const replaced = hit.raw.replace(/"([^"]*)"/, `"${next}"`);
|
|
346
|
+
const at = src.indexOf(hit.raw);
|
|
347
|
+
return src.slice(0, at) + replaced + src.slice(at + hit.raw.length);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// 平台级:model/model.go —— 常量 + 同名结构体字段(两处都要加)
|
|
351
|
+
export function renderModel(modelSrc, field, type = 'string', mod = '') {
|
|
352
|
+
if (!modelSrc) return modelSrc;
|
|
353
|
+
let out = modelSrc;
|
|
354
|
+
const name = String(field).toUpperCase();
|
|
355
|
+
if (!new RegExp(`\\b${name}\\s*=`).test(out)) {
|
|
356
|
+
const m = out.match(/const\s*\(\n([\s\S]*?)\n\)/);
|
|
357
|
+
if (m) {
|
|
358
|
+
const lines = m[1].split('\n');
|
|
359
|
+
lines.push(`\t${(name + ' '.repeat(Math.max(1, 18 - name.length))).trimEnd()} = "${field}"`);
|
|
360
|
+
out = out.slice(0, m.index) + `const (\n${lines.join('\n')}\n)` + out.slice(m.index + m[0].length);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (mod) {
|
|
364
|
+
const st = capitalize(mod);
|
|
365
|
+
const re = new RegExp(`(type\\s+${st}\\s+struct\\s*\\{)([\\s\\S]*?)(\\n\\})`);
|
|
366
|
+
const m2 = out.match(re);
|
|
367
|
+
if (m2) {
|
|
368
|
+
const body = m2[2];
|
|
369
|
+
if (!new RegExp(`\\n\\s*${capitalize(field)}\\s`).test(body)) {
|
|
370
|
+
const lines = body.split('\n');
|
|
371
|
+
lines.push(`\t${capitalize(field)} ${type === 'string' ? 'string' : type}`);
|
|
372
|
+
out = out.slice(0, m2.index) + m2[1] + lines.join('\n') + m2[3] + out.slice(m2.index + m2[0].length);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return out;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function capitalize(s) { return String(s || '').replace(/^./, (c) => c.toUpperCase()); }
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
// model.go 的常量字段
|
|
383
|
+
export function modelFields(modelSrc = '') {
|
|
384
|
+
const m = modelSrc.match(/const\s*\(\n([\s\S]*?)\n\)/);
|
|
385
|
+
if (!m) return [];
|
|
386
|
+
return m[1].split('\n').map((l) => {
|
|
387
|
+
const x = l.match(/^\s*(\w+)\s*=\s*"([^"]+)"/);
|
|
388
|
+
return x ? x[2] : '';
|
|
389
|
+
}).filter(Boolean);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// model.go 里同名结构体的字段(导出名 → 列名)
|
|
393
|
+
export function modelStructFields(modelSrc = '', mod = '') {
|
|
394
|
+
const st = String(mod || '').replace(/^./, (c) => c.toUpperCase());
|
|
395
|
+
const m = modelSrc.match(new RegExp('type\\s+' + st + '\\s+struct\\s*\\{([\\s\\S]*?)\\n\\}'));
|
|
396
|
+
if (!m) return [];
|
|
397
|
+
return m[1].split('\n').map((l) => {
|
|
398
|
+
const x = l.match(/^\s*(\w+)\s+[\w.\[\]]+/);
|
|
399
|
+
return x ? x[1] : '';
|
|
400
|
+
}).filter(Boolean);
|
|
401
|
+
}
|