momo-ai 1.0.70 → 1.0.71
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/package.json +1 -1
- package/skills/mo-dev-antd-exform/SKILL.md +162 -0
- package/skills/mo-dev-antd-exlist/SKILL.md +95 -0
- package/skills/mo-dev-antd-lifecycle/SKILL.md +108 -0
- package/skills/mo-dev-go-backend-route/SKILL.md +71 -0
- package/skills/mo-dev-go-frontend-route/SKILL.md +63 -0
- package/skills/mo-dev-go-module-api/SKILL.md +249 -0
- package/src/executor/executeRun.js +2 -2
- package/src/executor/executeTask.js +5 -2
package/package.json
CHANGED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mo-dev-antd-exform
|
|
3
|
+
description: Use when implementing ADD/EDIT/FILTER forms with ExForm component in React/Ant Design projects using the zero-ui framework
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# ExForm Component Patterns
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
ExForm provides standardized form handling with three modes (ADD/EDIT/FILTER), automatic validation, i18n integration, and Redux state synchronization.
|
|
11
|
+
|
|
12
|
+
## When to Use
|
|
13
|
+
|
|
14
|
+
- Building ADD forms for creating new records
|
|
15
|
+
- Building EDIT forms for updating existing records
|
|
16
|
+
- Building FILTER forms for search/query interfaces
|
|
17
|
+
- Need automatic form validation and submission handling
|
|
18
|
+
- Need integration with framework's dialog/notification system
|
|
19
|
+
|
|
20
|
+
## Form Modes Quick Reference
|
|
21
|
+
|
|
22
|
+
| Mode | yoForm Call | Purpose |
|
|
23
|
+
|------|-------------|---------|
|
|
24
|
+
| ADD | `Ex.yoForm(this, null)` | Create new record |
|
|
25
|
+
| EDIT | `Ex.yoForm(this, null, $inited)` | Edit existing record |
|
|
26
|
+
| FILTER | `Ex.yoFilter(this)` | Filter/search form |
|
|
27
|
+
|
|
28
|
+
## Hard Rules (No Exceptions)
|
|
29
|
+
|
|
30
|
+
- **Never** use raw Ant Design `<Form>` instead of `<ExForm>`
|
|
31
|
+
- **Never** hardcode form configuration in JSX (use resource JSON `_form` block)
|
|
32
|
+
- **Never** write custom `onFinish` handlers (use `$op` prop with `Ex.form()` API)
|
|
33
|
+
- **Never** skip `@Ux.zero` decorator with `Ux.rxEtat` builder
|
|
34
|
+
- **Never** forget `.form()` in decorator for FILTER mode
|
|
35
|
+
- **Never** forget `.raft(1)` for single-column FILTER layout
|
|
36
|
+
- **Never** pass `$inited` to ADD mode (only EDIT needs it)
|
|
37
|
+
|
|
38
|
+
## Basic Pattern
|
|
39
|
+
|
|
40
|
+
```jsx
|
|
41
|
+
import {ExForm} from "ei";
|
|
42
|
+
|
|
43
|
+
// ADD mode
|
|
44
|
+
const form = Ex.yoForm(this, null);
|
|
45
|
+
return <ExForm {...form} $height="300px" $op={Op.actions}/>;
|
|
46
|
+
|
|
47
|
+
// EDIT mode with data
|
|
48
|
+
const {$inited = {}} = this.props;
|
|
49
|
+
const form = Ex.yoForm(this, null, $inited);
|
|
50
|
+
return <ExForm {...form} $height="300px" $op={Op.actions}/>;
|
|
51
|
+
|
|
52
|
+
// FILTER mode
|
|
53
|
+
const form = Ex.yoFilter(this);
|
|
54
|
+
return <ExForm {...form} $op={Op.actions}/>;
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## RxEtat Decorator Configuration
|
|
58
|
+
|
|
59
|
+
```js
|
|
60
|
+
// ADD form
|
|
61
|
+
@Ux.zero(Ux.rxEtat(require('../Cab'))
|
|
62
|
+
.cab("UI.Add")
|
|
63
|
+
.to()
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
// EDIT form
|
|
67
|
+
@Ux.zero(Ux.rxEtat(require('../Cab'))
|
|
68
|
+
.cab("UI.Edit")
|
|
69
|
+
.to()
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
// FILTER form (single column)
|
|
73
|
+
@Ux.zero(Ux.rxEtat(require('../Cab'))
|
|
74
|
+
.cab("UI.Filter")
|
|
75
|
+
.raft(1) // Single column layout
|
|
76
|
+
.form() // Enable form binding
|
|
77
|
+
.to()
|
|
78
|
+
)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Op.js Handler Pattern
|
|
82
|
+
|
|
83
|
+
```js
|
|
84
|
+
// All handlers MUST have $op prefix
|
|
85
|
+
const $opAdd = (reference) => (params) =>
|
|
86
|
+
Ex.form(reference).add(params, {
|
|
87
|
+
uri: '/api/resource',
|
|
88
|
+
dialog: 'added' // i18n key for success message
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const $opSave = (reference) => (params) =>
|
|
92
|
+
Ex.form(reference).save(params, {
|
|
93
|
+
uri: '/api/resource/:key',
|
|
94
|
+
dialog: 'saved'
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const $opFilter = (reference) => (params) =>
|
|
98
|
+
Ex.form(reference).filter(params);
|
|
99
|
+
|
|
100
|
+
export default { $opAdd, $opSave, $opFilter };
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Post-Operation Redux Chaining
|
|
104
|
+
|
|
105
|
+
```js
|
|
106
|
+
// Update Redux after save
|
|
107
|
+
const $opAdd = (reference) => (params) =>
|
|
108
|
+
Ex.form(reference).add(params, { uri: "/api/dept", dialog: "added" })
|
|
109
|
+
.then(data => Ux.of(reference)._.ioIn(Ex.K.DEPT, data));
|
|
110
|
+
|
|
111
|
+
// Remove from Redux after delete
|
|
112
|
+
const $opDelete = (reference) => (params) =>
|
|
113
|
+
Ex.form(reference).remove(params, { uri: "/api/dept/:key", dialog: "removed" })
|
|
114
|
+
.then(data => Ux.of(reference)._.ioOut(Ex.K.DEPT, data));
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Resource JSON Configuration
|
|
118
|
+
|
|
119
|
+
```json
|
|
120
|
+
{
|
|
121
|
+
"_form": {
|
|
122
|
+
"ui": ["name", "code", "status"],
|
|
123
|
+
"hidden": ["id", "tenantId"],
|
|
124
|
+
"rules": {
|
|
125
|
+
"name": [{ "required": true, "message": "Name is required" }]
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Common Mistakes
|
|
132
|
+
|
|
133
|
+
| Mistake | Fix |
|
|
134
|
+
|---------|-----|
|
|
135
|
+
| Using raw `<Form>` | Use `<ExForm>` from "ei" |
|
|
136
|
+
| Hardcoding form config in JSX | Define in resource JSON `_form` block |
|
|
137
|
+
| Custom `onFinish` with fetch/axios | Use `$op` prop with `Ex.form().add/save` |
|
|
138
|
+
| Missing `.form()` in FILTER decorator | Add `.form()` to enable form binding |
|
|
139
|
+
| Missing `.raft(1)` for FILTER | Add `.raft(1)` for single-column layout |
|
|
140
|
+
| Passing `$inited` to ADD mode | Only pass to EDIT: `Ex.yoForm(this, null, $inited)` |
|
|
141
|
+
| Op handler without `$op` prefix | All handler keys MUST start with `$op` |
|
|
142
|
+
| Hardcoded success messages | Use `dialog` option with i18n key |
|
|
143
|
+
|
|
144
|
+
## Red Flags — STOP and Fix
|
|
145
|
+
|
|
146
|
+
- You're using `<Form>` from antd instead of `<ExForm>` from ei
|
|
147
|
+
- You're writing custom form submission logic with fetch/axios
|
|
148
|
+
- You're hardcoding form fields in JSX instead of resource JSON
|
|
149
|
+
- You're missing `@Ux.zero` decorator
|
|
150
|
+
- You're missing `.form()` or `.raft(1)` for FILTER mode
|
|
151
|
+
- Op handler keys don't start with `$op`
|
|
152
|
+
|
|
153
|
+
**All of these mean: Follow the ExForm pattern exactly.**
|
|
154
|
+
|
|
155
|
+
## Common Rationalizations (and Why They're Wrong)
|
|
156
|
+
|
|
157
|
+
| Excuse | Reality |
|
|
158
|
+
|--------|---------|
|
|
159
|
+
| "Raw Form is more flexible" | ExForm provides validation, i18n, Redux integration automatically |
|
|
160
|
+
| "Hardcoding is faster" | Resource JSON enables reuse and i18n; hardcoding breaks framework |
|
|
161
|
+
| "Custom handlers give more control" | `Ex.form()` API handles dialogs, loading states, Redux updates automatically |
|
|
162
|
+
| "Decorator is too complex" | Decorator wires i18n, Redux, lifecycle; skipping breaks integration |
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mo-dev-antd-exlist
|
|
3
|
+
description: Use when building list/table pages with ExListFast or ExListComplex components in React/Ant Design projects using the zero-ui framework
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# ExList Component Family
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
ExList provides two variants for list/table pages with built-in search, pagination, and CRUD operations.
|
|
11
|
+
|
|
12
|
+
## When to Use
|
|
13
|
+
|
|
14
|
+
- Building list/table pages with search and pagination
|
|
15
|
+
- Need quick list without embedded forms (use ExListFast)
|
|
16
|
+
- Need full CRUD with Add/Edit/Filter forms (use ExListComplex)
|
|
17
|
+
- Displaying data with custom column renderers
|
|
18
|
+
- Implementing batch operations
|
|
19
|
+
|
|
20
|
+
## Component Variants
|
|
21
|
+
|
|
22
|
+
| Component | Features | Use Case |
|
|
23
|
+
|-----------|----------|----------|
|
|
24
|
+
| `ExListFast` | Search + Pagination + Actions | Quick list without forms |
|
|
25
|
+
| `ExListComplex` | All Fast + Add/Edit/Filter forms | Full CRUD list page |
|
|
26
|
+
|
|
27
|
+
## Hard Rules (No Exceptions)
|
|
28
|
+
|
|
29
|
+
- **Never** use raw Ant Design `<Table>` instead of ExList components
|
|
30
|
+
- **Never** skip the attributes builder pattern (`yoList()` with `Ex.yoAmbient()`)
|
|
31
|
+
- **Never** use raw `fetch/axios` instead of `Ux.ajaxPost` for search
|
|
32
|
+
- **Never** implement manual pagination logic (use `$options`)
|
|
33
|
+
- **Never** hardcode columns array (use `$renders` pattern)
|
|
34
|
+
- **Never** skip `$executor` pattern for actions
|
|
35
|
+
- **Never** hardcode grid config (load from resource JSON via `Ux.fromHoc`)
|
|
36
|
+
|
|
37
|
+
## Basic ExListFast Pattern
|
|
38
|
+
|
|
39
|
+
```jsx
|
|
40
|
+
import {ExListFast} from "ei";
|
|
41
|
+
|
|
42
|
+
<ExListFast {...attrList}
|
|
43
|
+
$renders={columnFn(this)}
|
|
44
|
+
$executor={executeFn(this)}
|
|
45
|
+
rxSearch={query => Ux.ajaxPost("/api/search", query)}
|
|
46
|
+
/>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Attributes Builder (Op.js)
|
|
50
|
+
|
|
51
|
+
```js
|
|
52
|
+
const yoList = (reference) => {
|
|
53
|
+
const inherits = Ex.yoAmbient(reference);
|
|
54
|
+
|
|
55
|
+
// Load grid config from resource JSON
|
|
56
|
+
inherits.config = Ux.fromHoc(reference, "grid");
|
|
57
|
+
|
|
58
|
+
// Pagination options
|
|
59
|
+
inherits.$options = {
|
|
60
|
+
pageSize: 20,
|
|
61
|
+
showSizeChanger: true
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
return inherits;
|
|
65
|
+
};
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Custom Column Renderers
|
|
69
|
+
|
|
70
|
+
```js
|
|
71
|
+
const columnFn = (reference) => ({
|
|
72
|
+
// Custom text rendering
|
|
73
|
+
name: (text, record) => (
|
|
74
|
+
<a onClick={() => handleEdit(record)}>{text}</a>
|
|
75
|
+
),
|
|
76
|
+
|
|
77
|
+
// Status tag
|
|
78
|
+
status: (text) => (
|
|
79
|
+
<Tag color={text === 'active' ? 'green' : 'red'}>
|
|
80
|
+
{text === 'active' ? 'Active' : 'Inactive'}
|
|
81
|
+
</Tag>
|
|
82
|
+
),
|
|
83
|
+
|
|
84
|
+
// Date formatting
|
|
85
|
+
createdAt: (text) => moment(text).format('YYYY-MM-DD HH:mm'),
|
|
86
|
+
|
|
87
|
+
// Actions column
|
|
88
|
+
$op: (text, record) => (
|
|
89
|
+
<Space>
|
|
90
|
+
<a onClick={() => editFn(record)}>Edit</a>
|
|
91
|
+
<a onClick={() => deleteFn(record)}>Delete</a>
|
|
92
|
+
</Space>
|
|
93
|
+
)
|
|
94
|
+
});
|
|
95
|
+
```
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mo-dev-antd-lifecycle
|
|
3
|
+
description: Use when creating or updating React/Ant Design pages that must follow the zero-ui yi/yu/yo lifecycle pattern and @Ux.zero decorator
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Page Lifecycle Patterns (yi/yu/yo)
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
All Ant Design pages must use the zero-ui lifecycle pattern with `@Ux.zero` decorator, `yi/yu/yo` naming, and `Ex.yoRender` gated by `$ready`.
|
|
11
|
+
|
|
12
|
+
## When to Use
|
|
13
|
+
|
|
14
|
+
- Creating a new page component
|
|
15
|
+
- Refactoring a page component
|
|
16
|
+
- Fixing page initialization, data loading, or render issues
|
|
17
|
+
- Migrating a function component to zero-ui class component
|
|
18
|
+
|
|
19
|
+
## Hard Rules (No Exceptions)
|
|
20
|
+
|
|
21
|
+
- **Never** use function components or hooks for pages
|
|
22
|
+
- **Always** use class components with `React.PureComponent`
|
|
23
|
+
- **Always** use `@Ux.zero(Ux.rxEtat(...).to())` decorator
|
|
24
|
+
- **Always** call `Ex.yiAssist(this)` or `Ex.yiStandard(this)` in `componentDidMount`
|
|
25
|
+
- **Always** render with `Ex.yoRender(this, fn, debug)`
|
|
26
|
+
- **Always** gate rendering by `$ready` (handled by `Ex.yoRender`)
|
|
27
|
+
- **Always** use Op handlers with `$op` prefix and `(reference) => (params) => Promise`
|
|
28
|
+
|
|
29
|
+
## Canonical Pattern
|
|
30
|
+
|
|
31
|
+
```js
|
|
32
|
+
import Ux from 'ux';
|
|
33
|
+
import Ex from 'ex';
|
|
34
|
+
import Op from './Op';
|
|
35
|
+
|
|
36
|
+
@Ux.zero(Ux.rxEtat(require('./Cab.json'))
|
|
37
|
+
.cab("UI")
|
|
38
|
+
.ready(true)
|
|
39
|
+
.connect(state => Ux.dataIn(state).revamp(["datum"]), true)
|
|
40
|
+
.state({})
|
|
41
|
+
.to()
|
|
42
|
+
)
|
|
43
|
+
class Component extends React.PureComponent {
|
|
44
|
+
componentDidMount() {
|
|
45
|
+
Ex.yiAssist(this); // yi = initialize
|
|
46
|
+
}
|
|
47
|
+
render() {
|
|
48
|
+
return Ex.yoRender(this, () => {
|
|
49
|
+
// render logic here
|
|
50
|
+
}, Ex.parserOfColor("PageName"));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export default Component;
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## RxEtat Builder Checklist
|
|
58
|
+
|
|
59
|
+
```js
|
|
60
|
+
Ux.rxEtat(require('./Cab'))
|
|
61
|
+
.cab("UI")
|
|
62
|
+
.ready(true)
|
|
63
|
+
.state({})
|
|
64
|
+
.connect(s2pFn)
|
|
65
|
+
.bind(OP)
|
|
66
|
+
.to()
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Op Handler Pattern
|
|
70
|
+
|
|
71
|
+
```js
|
|
72
|
+
const $opAdd = (reference) => (params) =>
|
|
73
|
+
Ex.form(reference).add(params, {
|
|
74
|
+
uri: '/api/resource',
|
|
75
|
+
dialog: 'added'
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
export default { $opAdd };
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Common Mistakes
|
|
82
|
+
|
|
83
|
+
| Mistake | Fix |
|
|
84
|
+
|---------|-----|
|
|
85
|
+
| Using hooks/function component | Use class component + @Ux.zero decorator |
|
|
86
|
+
| Missing `Ex.yiAssist` | Call in `componentDidMount` |
|
|
87
|
+
| Rendering without `Ex.yoRender` | Wrap render with `Ex.yoRender` |
|
|
88
|
+
| Missing `$ready` gate | Use `.ready(true)` + `Ex.yoRender` |
|
|
89
|
+
| Op handler without `$op` prefix | Prefix keys with `$op` |
|
|
90
|
+
|
|
91
|
+
## Red Flags — STOP and Fix
|
|
92
|
+
|
|
93
|
+
- You are writing a function component for a page
|
|
94
|
+
- You are using React hooks in a page
|
|
95
|
+
- You are rendering without `Ex.yoRender`
|
|
96
|
+
- You are missing `@Ux.zero` decorator
|
|
97
|
+
- You are calling `setState` before `$ready`
|
|
98
|
+
- Op handlers are missing `$op` prefix
|
|
99
|
+
|
|
100
|
+
**All of these mean: refactor to the zero-ui lifecycle pattern.**
|
|
101
|
+
|
|
102
|
+
## Common Rationalizations (and Why They're Wrong)
|
|
103
|
+
|
|
104
|
+
| Excuse | Reality |
|
|
105
|
+
|--------|---------|
|
|
106
|
+
| "Hooks are modern best practice" | Zero-ui framework requires class components for decorator wiring |
|
|
107
|
+
| "Decorator is too complex" | Decorator injects i18n, Redux, lifecycle; skipping breaks framework |
|
|
108
|
+
| "Render directly is fine" | `Ex.yoRender` provides loading/error gate and `$ready` handling |
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mo-dev-go-backend-route
|
|
3
|
+
description: Use when adding, removing, or moving Go backend modules under internal/apps, internal/system, internal/widgets, or internal/jobs where routes are generated
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Backend Dynamic Routing (Codegen)
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
Backend module routing is **compile-time generated**. If you add/move a module and skip codegen, you create missing routes or duplicate route panics.
|
|
11
|
+
|
|
12
|
+
## When to Use
|
|
13
|
+
|
|
14
|
+
- You add/remove/move a module directory under:
|
|
15
|
+
- `internal/apps/<name>/`
|
|
16
|
+
- `internal/system/<name>/`
|
|
17
|
+
- `internal/widgets/<name>/`
|
|
18
|
+
- `internal/jobs/<name>/`
|
|
19
|
+
- You modify a module’s `routes.go` or its `RegisterRoutes` signature
|
|
20
|
+
- You see:
|
|
21
|
+
- “module not found” during mounting
|
|
22
|
+
- duplicate route registration panics
|
|
23
|
+
|
|
24
|
+
## Quick Reference
|
|
25
|
+
|
|
26
|
+
- **Codegen entry:** `cmd/codegen/main.go`
|
|
27
|
+
- **Generated output:** `internal/api/routes_gen.go`
|
|
28
|
+
- **Run:**
|
|
29
|
+
- `go run cmd/codegen/main.go`
|
|
30
|
+
- or `make codegen`
|
|
31
|
+
|
|
32
|
+
## Required Contract
|
|
33
|
+
|
|
34
|
+
### RegisterRoutes signature (MUST match)
|
|
35
|
+
|
|
36
|
+
```go
|
|
37
|
+
func RegisterRoutes(r *gin.RouterGroup, cfg *config.Config, db *gorm.DB) {
|
|
38
|
+
handler := NewHandler(cfg, db)
|
|
39
|
+
group := r.Group("/mymodule")
|
|
40
|
+
{
|
|
41
|
+
group.GET("/endpoint", handler.GetData)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Registry mounting (don’t bypass)
|
|
47
|
+
|
|
48
|
+
- Use `internal/registry/` to mount **generated** `BuiltinModules`.
|
|
49
|
+
- Do **not** manually import/register modules in `router.go`.
|
|
50
|
+
|
|
51
|
+
## Hard Rules (No Exceptions)
|
|
52
|
+
|
|
53
|
+
- **Never** hand-edit `internal/api/routes_gen.go`.
|
|
54
|
+
- **Never** “just wire it up quickly” by manually importing a module into `router.go`.
|
|
55
|
+
- **Never** re-apply `authMiddleware` inside module routes (parent group already has it).
|
|
56
|
+
|
|
57
|
+
## Common Rationalizations (and Why They’re Wrong)
|
|
58
|
+
|
|
59
|
+
| Excuse | Reality |
|
|
60
|
+
|---|---|
|
|
61
|
+
| “It’s one module, manual import is faster.” | Creates duplicates / drift; next codegen run breaks or overwrites behavior. |
|
|
62
|
+
| “Codegen is optional.” | Architecture requires generated registry; skipping causes missing mounts. |
|
|
63
|
+
| “I can tweak routes_gen.go directly.” | Generated file is overwritten; manual edits are lost and can break builds. |
|
|
64
|
+
|
|
65
|
+
## Red Flags — STOP
|
|
66
|
+
|
|
67
|
+
- You are about to edit `internal/api/routes_gen.go`
|
|
68
|
+
- You are about to import a module directly in `router.go`
|
|
69
|
+
- You changed `RegisterRoutes` signature
|
|
70
|
+
|
|
71
|
+
**All of these mean: run codegen and follow registry mounting.**
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mo-dev-go-frontend-route
|
|
3
|
+
description: Use when adding, removing, or moving Vue modules under web/src/apps, web/src/system, web/src/widgets, or web/src/jobs where module routing is code-generated
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Frontend Dynamic Routing (Codegen)
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
Frontend module routing is **compile-time generated**. Manually importing modules or skipping codegen creates missing registry entries and runtime module errors.
|
|
11
|
+
|
|
12
|
+
## When to Use
|
|
13
|
+
|
|
14
|
+
- You add/remove/move a module under:
|
|
15
|
+
- `web/src/apps/<name>/`
|
|
16
|
+
- `web/src/system/<name>/`
|
|
17
|
+
- `web/src/widgets/<name>/`
|
|
18
|
+
- `web/src/jobs/<name>/`
|
|
19
|
+
- You change a module `index.ts` manifest
|
|
20
|
+
- You see errors like `Cannot find module '@/generated/modules'` or modules missing in UI
|
|
21
|
+
|
|
22
|
+
## Quick Reference
|
|
23
|
+
|
|
24
|
+
- **Codegen script:** `web/scripts/codegen.js`
|
|
25
|
+
- **Generated output:** `web/src/generated/modules.ts`
|
|
26
|
+
- **Run:**
|
|
27
|
+
- `cd web && npm run codegen`
|
|
28
|
+
- or `make codegen`
|
|
29
|
+
|
|
30
|
+
## Required Manifest Contract
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
export const manifest: AppManifest = {
|
|
34
|
+
id: 'app-name',
|
|
35
|
+
title: { en: 'App Name', zh: '应用名称' },
|
|
36
|
+
icon: 'icon-name',
|
|
37
|
+
component: () => import('./App.vue')
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Critical:** Use `title` (not `name`) for i18n labels.
|
|
42
|
+
|
|
43
|
+
## Hard Rules (No Exceptions)
|
|
44
|
+
|
|
45
|
+
- **Never** hand-edit `web/src/generated/modules.ts`.
|
|
46
|
+
- **Never** manually import/register modules in window manager or control panel.
|
|
47
|
+
- **Never** skip codegen after adding/moving a module.
|
|
48
|
+
|
|
49
|
+
## Common Rationalizations (and Why They’re Wrong)
|
|
50
|
+
|
|
51
|
+
| Excuse | Reality |
|
|
52
|
+
|---|---|
|
|
53
|
+
| “It’s faster to import manually.” | Registry stays stale; module won’t appear in app launcher. |
|
|
54
|
+
| “Codegen is optional.” | Generated registry is required for discovery and lazy-loading. |
|
|
55
|
+
| “I can tweak generated file.” | Overwritten on next codegen run; breaks consistency. |
|
|
56
|
+
|
|
57
|
+
## Red Flags — STOP
|
|
58
|
+
|
|
59
|
+
- You are about to edit `web/src/generated/modules.ts`
|
|
60
|
+
- You are about to add manual imports for a module
|
|
61
|
+
- You are skipping `npm run codegen`
|
|
62
|
+
|
|
63
|
+
**All of these mean: run codegen and use the manifest discovery pattern.**
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mo-dev-go-module-api
|
|
3
|
+
description: Use when adding GET/PUT settings endpoints to existing Go modules with persistent JSON configuration
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Adding Module Settings API
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
Add settings APIs to Go modules following the workspace JSON storage pattern with strict validation and response envelope.
|
|
11
|
+
|
|
12
|
+
## When to Use
|
|
13
|
+
|
|
14
|
+
- Adding GET/PUT config endpoints to apps/system/widgets modules
|
|
15
|
+
- Module needs persistent user-specific settings
|
|
16
|
+
- Settings stored as JSON in workspace (not DB)
|
|
17
|
+
|
|
18
|
+
## Core Pattern
|
|
19
|
+
|
|
20
|
+
**Endpoint convention:** GET/PUT `/api/<module>/config`
|
|
21
|
+
**Storage:** `workspace/<username>/.ft-webos/<module>.json`
|
|
22
|
+
**Response:** Always `{"success":bool,"data":any,"error":string}`
|
|
23
|
+
|
|
24
|
+
## Quick Reference
|
|
25
|
+
|
|
26
|
+
| File | Purpose |
|
|
27
|
+
|------|---------|
|
|
28
|
+
| `settings.go` | Model + validation + defaults |
|
|
29
|
+
| `service.go` | GetSettings, UpdateSettings, getSettingsPath |
|
|
30
|
+
| `handler.go` | GetSettings, UpdateSettings handlers |
|
|
31
|
+
| `routes.go` | Register GET/PUT /config |
|
|
32
|
+
| `settings_test.go` | Unit tests |
|
|
33
|
+
|
|
34
|
+
## Implementation Checklist
|
|
35
|
+
|
|
36
|
+
- [ ] **Response envelope enforced**: All handlers return `{"success":bool,"data":any,"error":string}`
|
|
37
|
+
- [ ] **Path validation**: Validate workspace path against root to prevent traversal
|
|
38
|
+
- [ ] **GET/PUT /config naming**: Use `/api/<module>/config` (not /settings)
|
|
39
|
+
- [ ] **JSON file storage**: `workspace/<username>/.ft-webos/<module>.json` (no DB)
|
|
40
|
+
- [ ] **Input validation**: Schema or field checks before processing
|
|
41
|
+
- [ ] **Error handling**: Log + HTTP status + error field populated
|
|
42
|
+
|
|
43
|
+
## Settings Model Template
|
|
44
|
+
|
|
45
|
+
```go
|
|
46
|
+
package mymodule
|
|
47
|
+
|
|
48
|
+
import "errors"
|
|
49
|
+
|
|
50
|
+
type MyModuleSettings struct {
|
|
51
|
+
Field1 bool `json:"field1"`
|
|
52
|
+
Field2 string `json:"field2"`
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
var AllowedField2Values = []string{"option1", "option2"}
|
|
56
|
+
var ErrInvalidField2 = errors.New("invalid field2 value")
|
|
57
|
+
|
|
58
|
+
func DefaultSettings() *MyModuleSettings {
|
|
59
|
+
return &MyModuleSettings{
|
|
60
|
+
Field1: false,
|
|
61
|
+
Field2: "option1",
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
func (s *MyModuleSettings) Validate() error {
|
|
66
|
+
if !contains(AllowedField2Values, s.Field2) {
|
|
67
|
+
return ErrInvalidField2
|
|
68
|
+
}
|
|
69
|
+
return nil
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
func contains(slice []string, item string) bool {
|
|
73
|
+
for _, v := range slice {
|
|
74
|
+
if v == item {
|
|
75
|
+
return true
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return false
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Service Layer Template
|
|
83
|
+
|
|
84
|
+
```go
|
|
85
|
+
func (s *Service) GetSettings(username string) (*MyModuleSettings, error) {
|
|
86
|
+
path := s.getSettingsPath(username)
|
|
87
|
+
|
|
88
|
+
// Validate path against workspace root
|
|
89
|
+
if !strings.HasPrefix(path, s.cfg.WorkspaceRoot) {
|
|
90
|
+
return nil, errors.New("invalid settings path")
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
data, err := os.ReadFile(path)
|
|
94
|
+
if os.IsNotExist(err) {
|
|
95
|
+
return DefaultSettings(), nil
|
|
96
|
+
}
|
|
97
|
+
if err != nil {
|
|
98
|
+
return nil, fmt.Errorf("read settings: %w", err)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
var settings MyModuleSettings
|
|
102
|
+
if err := json.Unmarshal(data, &settings); err != nil {
|
|
103
|
+
return nil, fmt.Errorf("parse settings: %w", err)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return &settings, nil
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
func (s *Service) UpdateSettings(username string, settings *MyModuleSettings) error {
|
|
110
|
+
if err := settings.Validate(); err != nil {
|
|
111
|
+
return err
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
path := s.getSettingsPath(username)
|
|
115
|
+
|
|
116
|
+
// Validate path against workspace root
|
|
117
|
+
if !strings.HasPrefix(path, s.cfg.WorkspaceRoot) {
|
|
118
|
+
return errors.New("invalid settings path")
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Ensure directory exists
|
|
122
|
+
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
|
123
|
+
return fmt.Errorf("create settings dir: %w", err)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
data, err := json.MarshalIndent(settings, "", " ")
|
|
127
|
+
if err != nil {
|
|
128
|
+
return fmt.Errorf("marshal settings: %w", err)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if err := os.WriteFile(path, data, 0644); err != nil {
|
|
132
|
+
return fmt.Errorf("write settings: %w", err)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return nil
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
func (s *Service) getSettingsPath(username string) string {
|
|
139
|
+
return filepath.Join(s.cfg.WorkspaceRoot, username, ".ft-webos", "mymodule.json")
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Handler Template
|
|
144
|
+
|
|
145
|
+
```go
|
|
146
|
+
func (h *Handler) GetSettings(c *gin.Context) {
|
|
147
|
+
userVal, exists := c.Get("user")
|
|
148
|
+
if !exists {
|
|
149
|
+
c.JSON(http.StatusUnauthorized, gin.H{
|
|
150
|
+
"success": false,
|
|
151
|
+
"data": nil,
|
|
152
|
+
"error": "unauthorized",
|
|
153
|
+
})
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
user := userVal.(models.User)
|
|
158
|
+
settings, err := h.service.GetSettings(user.Username)
|
|
159
|
+
if err != nil {
|
|
160
|
+
logger.Error("Failed to get settings", "error", err)
|
|
161
|
+
c.JSON(http.StatusInternalServerError, gin.H{
|
|
162
|
+
"success": false,
|
|
163
|
+
"data": nil,
|
|
164
|
+
"error": "failed to load settings",
|
|
165
|
+
})
|
|
166
|
+
return
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
c.JSON(http.StatusOK, gin.H{
|
|
170
|
+
"success": true,
|
|
171
|
+
"data": settings,
|
|
172
|
+
"error": "",
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
func (h *Handler) UpdateSettings(c *gin.Context) {
|
|
177
|
+
userVal, exists := c.Get("user")
|
|
178
|
+
if !exists {
|
|
179
|
+
c.JSON(http.StatusUnauthorized, gin.H{
|
|
180
|
+
"success": false,
|
|
181
|
+
"data": nil,
|
|
182
|
+
"error": "unauthorized",
|
|
183
|
+
})
|
|
184
|
+
return
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
var settings MyModuleSettings
|
|
188
|
+
if err := c.ShouldBindJSON(&settings); err != nil {
|
|
189
|
+
c.JSON(http.StatusBadRequest, gin.H{
|
|
190
|
+
"success": false,
|
|
191
|
+
"data": nil,
|
|
192
|
+
"error": "invalid request body",
|
|
193
|
+
})
|
|
194
|
+
return
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
user := userVal.(models.User)
|
|
198
|
+
if err := h.service.UpdateSettings(user.Username, &settings); err != nil {
|
|
199
|
+
logger.Error("Failed to update settings", "error", err)
|
|
200
|
+
c.JSON(http.StatusBadRequest, gin.H{
|
|
201
|
+
"success": false,
|
|
202
|
+
"data": nil,
|
|
203
|
+
"error": err.Error(),
|
|
204
|
+
})
|
|
205
|
+
return
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
c.JSON(http.StatusOK, gin.H{
|
|
209
|
+
"success": true,
|
|
210
|
+
"data": settings,
|
|
211
|
+
"error": "",
|
|
212
|
+
})
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Route Registration
|
|
217
|
+
|
|
218
|
+
```go
|
|
219
|
+
func RegisterRoutes(r *gin.RouterGroup, cfg *config.Config, db *gorm.DB) {
|
|
220
|
+
handler := NewHandler(cfg, db)
|
|
221
|
+
group := r.Group("/mymodule")
|
|
222
|
+
{
|
|
223
|
+
group.GET("/config", handler.GetSettings)
|
|
224
|
+
group.PUT("/config", handler.UpdateSettings)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
## Common Mistakes
|
|
230
|
+
|
|
231
|
+
| Mistake | Fix |
|
|
232
|
+
|---------|-----|
|
|
233
|
+
| Using POST instead of PUT | Use PUT for updates (idempotent) |
|
|
234
|
+
| Returning raw struct | Always wrap in `{"success":bool,"data":any,"error":string}` |
|
|
235
|
+
| Skipping path validation | Validate against workspace root to prevent traversal |
|
|
236
|
+
| Using DB for settings | Use JSON files in workspace/<username>/.ft-webos/ |
|
|
237
|
+
| Endpoint named /settings | Use /config for consistency |
|
|
238
|
+
| No input validation | Call `settings.Validate()` before saving |
|
|
239
|
+
|
|
240
|
+
## Red Flags - STOP and Fix
|
|
241
|
+
|
|
242
|
+
- Response without success/data/error envelope
|
|
243
|
+
- File path not validated against workspace root
|
|
244
|
+
- Endpoint not named /config
|
|
245
|
+
- Settings stored in DB instead of JSON file
|
|
246
|
+
- No validation before UpdateSettings
|
|
247
|
+
- Swallowing errors without logging
|
|
248
|
+
|
|
249
|
+
**All of these mean: Fix before proceeding.**
|
|
@@ -69,8 +69,8 @@ const _listTaskFiles = async (taskDir) => {
|
|
|
69
69
|
return files.map((f) => f.name);
|
|
70
70
|
};
|
|
71
71
|
|
|
72
|
-
/** 提示词模板:阅读后为所选 md
|
|
73
|
-
const _promptForTask = (relativePath) => `执行任务:读取当前工作目录下 ${relativePath} 的正文(frontmatter
|
|
72
|
+
/** 提示词模板:阅读后为所选 md 的相对路径(统一模板) */
|
|
73
|
+
const _promptForTask = (relativePath) => `执行任务:读取当前工作目录下 ${relativePath} 的正文(frontmatter 之后),按其中要求完成任务,若开启了 Team 模式则考虑使用 Team 模式执行,最多创建的 Worker(不包括Team Leader)最多5个,自动评估和创建,执行完成后 Team Leader 回写 ${relativePath} 追加 Changes 记录。`;
|
|
74
74
|
|
|
75
75
|
|
|
76
76
|
module.exports = async () => {
|
|
@@ -117,8 +117,11 @@ const _yamlFrontmatter = (attrs) => {
|
|
|
117
117
|
return lines.join('\n') + '\n';
|
|
118
118
|
};
|
|
119
119
|
|
|
120
|
-
/** 生成针对某槽位 task
|
|
121
|
-
const _taskPromptForSlot = (filename) =>
|
|
120
|
+
/** 生成针对某槽位 task 文件的提示词(统一模板) */
|
|
121
|
+
const _taskPromptForSlot = (filename) => {
|
|
122
|
+
const relativePath = `.r2mo/task/${filename}`;
|
|
123
|
+
return `执行任务:读取当前工作目录下 ${relativePath} 的正文(frontmatter 之后),按其中要求完成任务,若开启了 Team 模式则考虑使用 Team 模式执行,最多创建的 Worker(不包括Team Leader)最多5个,自动评估和创建,执行完成后 Team Leader 回写 ${relativePath} 追加 Changes 记录。`;
|
|
124
|
+
};
|
|
122
125
|
|
|
123
126
|
module.exports = (options) => {
|
|
124
127
|
try {
|