Skip to content

Commit 7be428b

Browse files
Sisyphus-JuniorSisyphus-Junior
authored andcommitted
feat: add module management and installation capabilities
- Implemented a new module manager to handle module definitions and installations. - Added built-in module definitions for NapCat and an example adapter. - Introduced a resolver for download mirrors with fallback mechanisms. - Enhanced the exec runner to support environment variables during command execution. - Added prerequisite checks in the installation script to ensure required tools are available. - Implemented retry logic for git operations with configurable backoff. - Created tests for module installation, mirror resolution, and command execution. - Improved logging for better visibility during module installation and execution.
1 parent 5431d7c commit 7be428b

File tree

18 files changed

+1403
-12
lines changed

18 files changed

+1403
-12
lines changed

README.md

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
# MaiBot Bootstrap(单实例 CLI + TUI)
22

33
本项目提供 MaiBot 的一键安装入口与跨平台命令行管理能力。
4-
采用 Git 风格 workspace:在项目目录执行 `maibot init` 创建 `.maibot/`,后续在该目录(或其子目录)运行命令即可。
4+
采用 Git 风格 workspace:在项目目录执行 `maibot init` 后,目录约定如下:
5+
- `.maibot/`:仅存放 maibot 命令行工具的配置/状态/日志数据
6+
- `modules/`:存放附属模块(如 napcat、适配器)
7+
- `MaiBot/`:存放本体文件
58

69
## 快速安装
710

@@ -11,6 +14,8 @@
1114
curl -fsSL https://raw.githubusercontent.com/Mai-with-u/maibot-bootstrap/main/scripts/install.sh | bash
1215
```
1316

17+
安装脚本会预检 `git``uv`;若缺失会先征求确认,确认后尝试自动安装。
18+
1419
安装后可用:
1520

1621
```bash
@@ -30,8 +35,13 @@ maibot update
3035
maibot stop
3136
maibot workspace ls .
3237
maibot -C ../other-workspace status
38+
maibot modules list
39+
maibot modules install napcat
3340
```
3441

42+
内置 `napcat` 模块会执行系统依赖安装、LinuxQQ 安装、launcher 编译等步骤。
43+
这些步骤可能触发 sudo 认证,建议在 TTY 环境执行(例如直接在终端或 TUI 中运行)。
44+
3545
服务管理:
3646

3747
```bash
@@ -42,10 +52,12 @@ maibot service stop
4252
maibot service uninstall
4353
```
4454

55+
说明:service 按 workspace 路径生成唯一服务名;可用 `maibot -C <path> service ...` 管理其他 workspace 的服务。
56+
4557
自更新与清理:
4658

4759
```bash
48-
maibot self-update
60+
maibot upgrade
4961
maibot cleanup --test-artifacts
5062
maibot run echo devtool
5163
```
@@ -54,8 +66,27 @@ maibot run echo devtool
5466

5567
全局配置文件默认位于 `~/.maibot/maibot.conf`,为 JSON 格式。
5668
workspace 运行数据位于工作区目录下的 `.maibot/`(通过 `maibot init` 创建)。
69+
附属模块安装到工作区根目录的 `modules/`,本体目录使用 `MaiBot/`
5770
支持环境变量覆盖(`MAIBOT_` 前缀)。
5871

72+
`modules` 支持两种来源:
73+
- 内置模块列表(写死在代码中)
74+
- 远程 `catalog_urls`(HTTP JSON)
75+
76+
git 与 modules 共用顶层 `mirrors` 镜像池配置。
77+
78+
远程 catalog JSON 可为以下两种格式之一:
79+
80+
```json
81+
{"modules":[{"name":"napcat","description":"...","install":[{"name":"step","command":"bash","args":["-lc","..."]}]}]}
82+
```
83+
84+
85+
86+
```json
87+
[{"name":"napcat","description":"...","install":[{"name":"step","command":"bash","args":["-lc","..."]}]}]
88+
```
89+
5990
核心字段示例:
6091

6192
```json
@@ -77,6 +108,36 @@ workspace 运行数据位于工作区目录下的 `.maibot/`(通过 `maibot in
77108
"updater": {
78109
"require_signature": false,
79110
"minisign_public_key": ""
111+
},
112+
"mirrors": {
113+
"urls": [
114+
"https://ghfast.top",
115+
"https://gh.wuliya.xin",
116+
"https://gh-proxy.com",
117+
"https://github.moeyy.xyz"
118+
],
119+
"probe_url": "https://raw.githubusercontent.com/Mai-with-u/plugin-repo/refs/heads/main/plugins.json",
120+
"probe_seconds": 8
121+
},
122+
"git": {
123+
"mirrors": [
124+
{
125+
"name": "fastgit",
126+
"base_url": "https://hub.fastgit.org",
127+
"enabled": false
128+
}
129+
],
130+
"mirror_first": true,
131+
"retry_per_source": 2,
132+
"retry_backoff_seconds": 1,
133+
"command_timeout_seconds": 120
134+
},
135+
"modules": {
136+
"catalog_urls": [],
137+
"catalog_timeout_seconds": 5,
138+
"install_retries": 2,
139+
"install_backoff_seconds": 1,
140+
"prefer_catalog_source": false
80141
}
81142
}
82143
```

internal/app/app.go

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/spf13/cobra"
1111
"maibot/internal/config"
1212
"maibot/internal/logging"
13+
"maibot/internal/modules"
1314
"maibot/internal/version"
1415
)
1516

@@ -25,6 +26,7 @@ type App struct {
2526
instanceLog *logging.Logger
2627
updateLog *logging.Logger
2728
cleanupLog *logging.Logger
29+
modulesLog *logging.Logger
2830
}
2931

3032
func New() (*App, error) {
@@ -47,6 +49,7 @@ func New() (*App, error) {
4749
instanceLog: rootLog.Module("instance"),
4850
updateLog: rootLog.Module("update"),
4951
cleanupLog: rootLog.Module("cleanup"),
52+
modulesLog: rootLog.Module("modules"),
5053
}, nil
5154
}
5255

@@ -148,7 +151,7 @@ func (a *App) newRootCommand() *cobra.Command {
148151
return nil
149152
}})
150153

151-
root.AddCommand(&cobra.Command{Use: "self-update", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error {
154+
root.AddCommand(&cobra.Command{Use: "upgrade", Aliases: []string{"self-update"}, Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error {
152155
if err := a.selfUpdate(); err != nil {
153156
return err
154157
}
@@ -213,6 +216,34 @@ func (a *App) newRootCommand() *cobra.Command {
213216
workspaceCmd.AddCommand(workspaceList)
214217
root.AddCommand(workspaceCmd)
215218

219+
modulesCmd := &cobra.Command{Use: "modules", Short: "Manage installable modules"}
220+
modulesInstall := &cobra.Command{Use: "install <module>", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error {
221+
mgr := modules.New(a.cfg.Modules, a.cfg.Mirrors, a.modulesLog, nil)
222+
report, err := mgr.Install(cmd.Context(), args[0])
223+
if err != nil {
224+
return err
225+
}
226+
a.modulesLog.Okf("module install completed module=%s source=%s attempts=%d", report.Module, report.Source, len(report.Attempts))
227+
return nil
228+
}}
229+
modulesList := &cobra.Command{Use: "list", Aliases: []string{"ls"}, Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error {
230+
mgr := modules.New(a.cfg.Modules, a.cfg.Mirrors, a.modulesLog, nil)
231+
defs, err := mgr.List(cmd.Context())
232+
if err != nil {
233+
return err
234+
}
235+
for _, def := range defs {
236+
desc := strings.TrimSpace(def.Description)
237+
if desc == "" {
238+
desc = "(no description)"
239+
}
240+
fmt.Printf("%s\t%s\n", def.Name, desc)
241+
}
242+
return nil
243+
}}
244+
modulesCmd.AddCommand(modulesInstall, modulesList)
245+
root.AddCommand(modulesCmd)
246+
216247
root.AddCommand(&cobra.Command{Use: instanceProc, Hidden: true, RunE: func(cmd *cobra.Command, args []string) error {
217248
id := workspaceID
218249
displayName := defaultName
@@ -243,7 +274,9 @@ func (a *App) printHelp() {
243274
fmt.Println(" maibot workspace ls [paths...] Discover workspaces under paths")
244275
fmt.Println(" maibot logs [--tail N] Show workspace logs")
245276
fmt.Println(" maibot update Update workspace")
246-
fmt.Println(" maibot self-update Update maibot command")
277+
fmt.Println(" maibot upgrade Upgrade maibot command")
278+
fmt.Println(" maibot modules install <name> Install module by catalog name")
279+
fmt.Println(" maibot modules list List configured/catalog modules")
247280
fmt.Println(" maibot service <action> Manage workspace service")
248281
fmt.Println(" maibot run <cmd...> Run developer command")
249282
fmt.Println(" maibot cleanup --test-artifacts Clean local test artifacts")

internal/app/i18n/en.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"input_hint": "Press Enter to continue, Esc to cancel.",
1010
"menu.instances": "workspace",
1111
"menu.services": "services",
12+
"menu.modules": "modules",
1213
"action.install": "initialize workspace",
1314
"action.start": "start workspace",
1415
"action.stop": "stop workspace",
@@ -17,18 +18,21 @@
1718
"action.logs": "workspace logs",
1819
"action.list": "list instances (deprecated)",
1920
"action.update": "update workspace",
20-
"action.self_update": "self-update",
21+
"action.self_update": "upgrade",
2122
"action.service_install": "service install",
2223
"action.service_start": "service start",
2324
"action.service_stop": "service stop",
2425
"action.service_status": "service status",
2526
"action.service_uninstall": "service uninstall",
27+
"action.modules_list": "modules list",
28+
"action.modules_install": "modules install",
2629
"action.cleanup": "cleanup --test-artifacts",
2730
"action.version": "version",
2831
"action.help": "help",
2932
"action.quit": "quit",
3033
"action.back": "back",
3134
"field.instance_name": "Instance name (deprecated)",
3235
"field.tail_lines": "Tail lines",
36+
"field.module_name": "Module name",
3337
"field.cleanup_names": "Instance names (deprecated)"
3438
}

internal/app/i18n/zh-CN.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"input_hint": "回车继续,Esc 取消。",
1010
"menu.instances": "工作区",
1111
"menu.services": "服务管理",
12+
"menu.modules": "模块管理",
1213
"action.install": "初始化工作区",
1314
"action.start": "启动工作区",
1415
"action.stop": "停止工作区",
@@ -17,18 +18,21 @@
1718
"action.logs": "查看工作区日志",
1819
"action.list": "列出实例(已弃用)",
1920
"action.update": "更新工作区",
20-
"action.self_update": "更新 maibot 本体",
21+
"action.self_update": "升级 maibot 本体",
2122
"action.service_install": "安装服务",
2223
"action.service_start": "启动服务",
2324
"action.service_stop": "停止服务",
2425
"action.service_status": "查看服务状态",
2526
"action.service_uninstall": "卸载服务",
27+
"action.modules_list": "列出模块",
28+
"action.modules_install": "安装模块",
2629
"action.cleanup": "清理测试产物",
2730
"action.version": "版本",
2831
"action.help": "帮助",
2932
"action.quit": "退出",
3033
"action.back": "返回",
3134
"field.instance_name": "实例名称(已弃用)",
3235
"field.tail_lines": "日志行数",
36+
"field.module_name": "模块名",
3337
"field.cleanup_names": "实例名称(已弃用)"
3438
}

internal/app/service.go

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"os"
66
"os/exec"
77
"path/filepath"
8+
"strings"
89
"time"
910

1011
kservice "github.com/kardianos/service"
@@ -63,11 +64,11 @@ func (a *App) serviceAction(action, _ string) error {
6364
args: []string{instanceProc, workspaceID, defaultName},
6465
workdir: workdir,
6566
}
66-
serviceName := "maibot-workspace"
67+
serviceName := workspaceServiceName(workdir)
6768
svc, err := kservice.New(prg, &kservice.Config{
6869
Name: serviceName,
6970
DisplayName: "MaiBot Workspace",
70-
Description: "MaiBot single workspace service",
71+
Description: "MaiBot workspace service " + serviceName,
7172
Arguments: prg.args,
7273
WorkingDirectory: workdir,
7374
})
@@ -104,3 +105,37 @@ func (a *App) serviceAction(action, _ string) error {
104105
a.instanceLog.Infof("service action %s completed", action)
105106
return nil
106107
}
108+
109+
func workspaceServiceName(workdir string) string {
110+
workspaceRoot := filepath.Dir(workdir)
111+
base := sanitizeServiceToken(filepath.Base(workspaceRoot))
112+
hash := sha256Hex([]byte(workspaceRoot))
113+
if len(hash) > 8 {
114+
hash = hash[:8]
115+
}
116+
name := fmt.Sprintf("maibot-%s-%s", base, hash)
117+
if len(name) > 80 {
118+
name = name[:80]
119+
}
120+
return name
121+
}
122+
123+
func sanitizeServiceToken(raw string) string {
124+
raw = strings.TrimSpace(raw)
125+
if raw == "" {
126+
return "workspace"
127+
}
128+
var b strings.Builder
129+
for _, r := range raw {
130+
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '-' || r == '_' {
131+
b.WriteRune(r)
132+
} else {
133+
b.WriteRune('-')
134+
}
135+
}
136+
out := strings.Trim(b.String(), "-")
137+
if out == "" {
138+
return "workspace"
139+
}
140+
return out
141+
}

internal/app/tui.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ type tuiModel struct {
4141
rootMenu []tuiAction
4242
instanceMenu []tuiAction
4343
serviceMenu []tuiAction
44+
modulesMenu []tuiAction
4445
cursor int
4546
mode tuiMode
4647
activeAction int
@@ -84,10 +85,19 @@ func newTUIModel(i18n *tuiI18n) tuiModel {
8485
{id: "back", labelKey: "action.back", build: func(_ []string) []string { return nil }},
8586
}
8687

88+
modulesMenu := []tuiAction{
89+
{id: "modules-list", labelKey: "action.modules_list", build: func(_ []string) []string { return []string{"modules", "list"} }},
90+
{id: "modules-install", labelKey: "action.modules_install", fields: []tuiField{{labelKey: "field.module_name", def: "napcat"}}, build: func(v []string) []string {
91+
return []string{"modules", "install", nonEmpty(v[0], "napcat")}
92+
}},
93+
{id: "back", labelKey: "action.back", build: func(_ []string) []string { return nil }},
94+
}
95+
8796
rootMenu := []tuiAction{
8897
{id: "instances", labelKey: "menu.instances", build: func(_ []string) []string { return nil }},
8998
{id: "services", labelKey: "menu.services", build: func(_ []string) []string { return nil }},
90-
{id: "self-update", labelKey: "action.self_update", build: func(_ []string) []string { return []string{"self-update"} }},
99+
{id: "modules", labelKey: "menu.modules", build: func(_ []string) []string { return nil }},
100+
{id: "self-update", labelKey: "action.self_update", build: func(_ []string) []string { return []string{"upgrade"} }},
91101
{id: "version", labelKey: "action.version", build: func(_ []string) []string { return []string{"version"} }},
92102
{id: "help", labelKey: "action.help", build: func(_ []string) []string { return []string{"help"} }},
93103
{id: "quit", labelKey: "action.quit", quit: true, build: func(_ []string) []string { return nil }},
@@ -100,6 +110,7 @@ func newTUIModel(i18n *tuiI18n) tuiModel {
100110
rootMenu: rootMenu,
101111
instanceMenu: instanceMenu,
102112
serviceMenu: serviceMenu,
113+
modulesMenu: modulesMenu,
103114
mode: tuiModeMenu,
104115
activeAction: -1,
105116
}
@@ -154,6 +165,9 @@ func (m tuiModel) updateMenu(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
154165
if action.id == "services" {
155166
return m.pushMenu(m.serviceMenu), nil
156167
}
168+
if action.id == "modules" {
169+
return m.pushMenu(m.modulesMenu), nil
170+
}
157171
if action.id == "back" {
158172
return m.popMenu(), nil
159173
}

internal/app/workspace.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,9 +128,16 @@ func (a *App) installInstance(name string) error {
128128
if err != nil {
129129
return err
130130
}
131+
workspaceRoot := filepath.Dir(dir)
131132
if err := os.MkdirAll(dir, 0o755); err != nil {
132133
return err
133134
}
135+
if err := os.MkdirAll(filepath.Join(workspaceRoot, "modules"), 0o755); err != nil {
136+
return err
137+
}
138+
if err := os.MkdirAll(filepath.Join(workspaceRoot, "MaiBot"), 0o755); err != nil {
139+
return err
140+
}
134141

135142
now := time.Now().UTC()
136143
workspaceName := sanitizeWorkspaceName(name)

0 commit comments

Comments
 (0)