@xcanwin/manyoyo 5.3.6 → 5.3.9
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/README.md +117 -87
- package/bin/manyoyo.js +73 -90
- package/lib/serve-log.js +116 -0
- package/lib/web/server.js +6 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
# <p align="center"><a href="https://github.com/xcanwin/manyoyo">MANYOYO(慢悠悠)</a></p>
|
|
6
|
-
<p align="center"
|
|
6
|
+
<p align="center">面向 AI Agent CLI 的 Docker / Podman 安全沙箱。</p>
|
|
7
|
+
<p align="center">用于隔离 Claude Code、Codex、Gemini、OpenCode 等命令行智能体,降低宿主机风险,并保持可复现的运行环境。</p>
|
|
7
8
|
<p align="center">
|
|
8
9
|
<a href="https://www.npmjs.com/package/@xcanwin/manyoyo"><img alt="npm" src="https://img.shields.io/npm/v/@xcanwin/manyoyo?style=flat-square" /></a>
|
|
9
10
|
<a href="https://github.com/xcanwin/manyoyo/actions/workflows/npm-publish.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/xcanwin/manyoyo/npm-publish.yml?style=flat-square" /></a>
|
|
@@ -15,157 +16,185 @@
|
|
|
15
16
|
<a href="https://xcanwin.github.io/manyoyo/en/">English</a>
|
|
16
17
|
</p>
|
|
17
18
|
<p align="center">
|
|
18
|
-
|
|
19
|
+
文档:<a href="https://xcanwin.github.io/manyoyo/">https://xcanwin.github.io/manyoyo/</a>
|
|
19
20
|
</p>
|
|
20
21
|
|
|
21
22
|
---
|
|
22
23
|
|
|
23
|
-
##
|
|
24
|
+
## 为什么是 MANYOYO
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
AI Agent CLI 往往需要:
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
- 访问代码仓库
|
|
29
|
+
- 执行 shell 命令
|
|
30
|
+
- 读写文件
|
|
31
|
+
- 安装依赖或调用容器能力
|
|
28
32
|
|
|
29
|
-
|
|
33
|
+
直接在宿主机上裸跑这些工具,风险边界通常不清晰。**MANYOYO** 的目标不是替代容器平台,而是把常见 Agent CLI 的运行方式收敛到一个更清晰、可复现、可审计的沙箱入口。
|
|
30
34
|
|
|
31
|
-
|
|
35
|
+
你可以把它理解为:
|
|
32
36
|
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
-
|
|
36
|
-
- **便捷操作**:快速进入 `/bin/bash`
|
|
37
|
-
- **会话恢复**:安装 Skills Marketplace 可快速恢复会话
|
|
38
|
-
- **灵活自定义**:支持配置各 CLI 的 `*_BASE_URL` / `*_AUTH_TOKEN` / `*_API_KEY` 等变量
|
|
39
|
-
- **配置管理**:快捷导入配置文件
|
|
40
|
-
- **高级模式**:支持危险容器嵌套(mount-docker-socket)、自定义沙箱镜像
|
|
37
|
+
- 面向 Agent CLI 的运行包装层
|
|
38
|
+
- 面向团队协作的配置与镜像约定
|
|
39
|
+
- 面向高风险模式的显式边界说明
|
|
41
40
|
|
|
42
|
-
|
|
41
|
+
## 核心能力
|
|
42
|
+
|
|
43
|
+
- **多 Agent 支持**:支持 `claude`、`gemini`、`codex`、`opencode`
|
|
44
|
+
- **容器隔离**:基于 Docker / Podman 运行,降低宿主机暴露面
|
|
45
|
+
- **YOLO / SOLO 工作流**:适配跳过权限确认的高效率模式
|
|
46
|
+
- **统一配置入口**:集中管理 `runs.<name>`、环境变量、挂载与镜像参数
|
|
47
|
+
- **命令可预览**:支持查看配置合并结果与最终命令拼装
|
|
48
|
+
- **会话与 Web 模式**:支持容器会话管理与网页访问入口
|
|
49
|
+
- **镜像可定制**:支持 common / full / 自定义工具集镜像
|
|
43
50
|
|
|
44
51
|
## 快速开始
|
|
45
52
|
|
|
46
53
|
```bash
|
|
47
|
-
npm install -g @xcanwin/manyoyo
|
|
54
|
+
npm install -g @xcanwin/manyoyo
|
|
48
55
|
podman pull ubuntu:24.04 # 仅 Podman 需要
|
|
49
|
-
manyoyo build --iv 1.8.4-common
|
|
50
|
-
manyoyo init all
|
|
51
|
-
manyoyo run -r claude
|
|
56
|
+
manyoyo build --iv 1.8.4-common
|
|
57
|
+
manyoyo init all
|
|
58
|
+
manyoyo run -r claude
|
|
52
59
|
```
|
|
53
60
|
|
|
54
|
-
|
|
61
|
+
系统要求:
|
|
55
62
|
|
|
56
|
-
|
|
63
|
+
- Node.js >= 22
|
|
64
|
+
- Podman(推荐)或 Docker
|
|
57
65
|
|
|
58
|
-
|
|
66
|
+
注意:
|
|
59
67
|
|
|
60
|
-
-
|
|
61
|
-
-
|
|
62
|
-
|
|
63
|
-
|
|
68
|
+
- `YOLO / SOLO` 会跳过权限确认,只适合在可控环境中使用
|
|
69
|
+
- `sock` 模式会暴露宿主机 Docker socket,不属于强隔离
|
|
70
|
+
|
|
71
|
+
## 适合什么场景
|
|
72
|
+
|
|
73
|
+
- 在容器中运行 **Claude Code YOLO / SOLO**
|
|
74
|
+
- 为 **Codex CLI** 提供独立于宿主机的运行边界
|
|
75
|
+
- 隔离运行 **Gemini CLI / OpenCode** 的代码任务
|
|
76
|
+
- 用统一镜像和配置管理团队 Agent 环境
|
|
77
|
+
- 在调试和自动化任务中快速切换 Agent 与 `/bin/bash`
|
|
64
78
|
|
|
65
79
|
## 裸跑 vs MANYOYO
|
|
66
80
|
|
|
67
81
|
| 对比项 | 裸跑 Agent CLI | MANYOYO |
|
|
68
82
|
| --- | --- | --- |
|
|
69
|
-
|
|
|
70
|
-
|
|
|
71
|
-
|
|
|
72
|
-
|
|
|
83
|
+
| 宿主机暴露面 | 高 | 更低 |
|
|
84
|
+
| 运行边界 | 分散 | 集中到容器与配置 |
|
|
85
|
+
| 环境复现 | 弱 | 强(镜像 + 配置) |
|
|
86
|
+
| 高风险模式说明 | 通常依赖工具自身 | 明确提示 YOLO / SOLO / sock 风险 |
|
|
87
|
+
| 团队统一性 | 弱 | 更强 |
|
|
73
88
|
|
|
74
|
-
|
|
89
|
+
## 安全边界
|
|
75
90
|
|
|
76
|
-
|
|
91
|
+
MANYOYO 可以降低风险,但不是“绝对安全”:
|
|
77
92
|
|
|
78
|
-
-
|
|
79
|
-
-
|
|
93
|
+
- 它的主要隔离手段是容器,不是虚拟机
|
|
94
|
+
- `YOLO / SOLO` 仍然可能执行危险命令
|
|
95
|
+
- `sock` 模式本质上会把宿主机容器控制权暴露给容器
|
|
96
|
+
- 自定义挂载、环境变量和网络访问会直接影响实际安全边界
|
|
80
97
|
|
|
81
|
-
|
|
98
|
+
相关文档:
|
|
82
99
|
|
|
83
|
-
|
|
100
|
+
- [AI 智能体说明](https://xcanwin.github.io/manyoyo/zh/reference/agents)
|
|
101
|
+
- [容器模式说明](https://xcanwin.github.io/manyoyo/zh/reference/container-modes)
|
|
102
|
+
- [网页认证与安全](https://xcanwin.github.io/manyoyo/zh/advanced/web-server-auth)
|
|
103
|
+
|
|
104
|
+
## 常用命令
|
|
84
105
|
|
|
85
106
|
```bash
|
|
86
|
-
|
|
87
|
-
|
|
107
|
+
# 初始化与迁移
|
|
108
|
+
manyoyo init all
|
|
109
|
+
|
|
110
|
+
# 启动常见 Agent
|
|
111
|
+
manyoyo run -y c
|
|
112
|
+
manyoyo run -y gm
|
|
113
|
+
manyoyo run -y cx
|
|
114
|
+
manyoyo run -y oc
|
|
88
115
|
|
|
89
|
-
|
|
116
|
+
# 更新
|
|
117
|
+
manyoyo update
|
|
118
|
+
|
|
119
|
+
# 容器与调试
|
|
120
|
+
manyoyo ps
|
|
121
|
+
manyoyo images
|
|
122
|
+
manyoyo run -n my-dev -x /bin/bash
|
|
123
|
+
manyoyo rm my-dev
|
|
90
124
|
|
|
91
|
-
|
|
92
|
-
|
|
125
|
+
# Web 模式
|
|
126
|
+
manyoyo serve 127.0.0.1:3000
|
|
127
|
+
manyoyo serve 127.0.0.1:3000 -U admin -P 123456
|
|
128
|
+
manyoyo serve 127.0.0.1:3000 -U admin -P 123456 -d
|
|
129
|
+
manyoyo serve 127.0.0.1:3000 -d # 未设置密码时会打印本次随机密码
|
|
93
130
|
|
|
94
|
-
|
|
131
|
+
# 查看配置与命令拼装
|
|
132
|
+
manyoyo config show
|
|
133
|
+
manyoyo config command
|
|
134
|
+
```
|
|
95
135
|
|
|
96
|
-
##
|
|
136
|
+
## 镜像构建
|
|
97
137
|
|
|
98
138
|
```bash
|
|
99
|
-
#
|
|
139
|
+
# common 版本
|
|
100
140
|
manyoyo build --iv 1.8.4-common
|
|
101
141
|
|
|
102
|
-
#
|
|
142
|
+
# full 版本
|
|
103
143
|
manyoyo build --iv 1.8.4-full
|
|
104
144
|
|
|
105
|
-
#
|
|
145
|
+
# 自定义工具集
|
|
106
146
|
manyoyo build --iba TOOL=go,codex,java,gemini
|
|
107
147
|
```
|
|
108
148
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
## 常用命令
|
|
149
|
+
说明:
|
|
112
150
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
151
|
+
- 首次构建会把依赖缓存到 `docker/cache/`
|
|
152
|
+
- 在缓存有效期内重复构建,通常会更快
|
|
153
|
+
- `imageVersion` 格式必须为 `x.y.z-后缀`
|
|
116
154
|
|
|
117
|
-
|
|
118
|
-
manyoyo run -y c # Claude Code(或 claude / cc)
|
|
119
|
-
manyoyo run -y gm # Gemini(或 gemini / g)
|
|
120
|
-
manyoyo run -y cx # Codex(或 codex)
|
|
121
|
-
manyoyo run -y oc # OpenCode(或 opencode)
|
|
122
|
-
manyoyo update # 更新 MANYOYO(全局 npm 安装场景)
|
|
155
|
+
## 配置模型
|
|
123
156
|
|
|
124
|
-
|
|
125
|
-
manyoyo ps
|
|
126
|
-
manyoyo images
|
|
127
|
-
manyoyo run -n my-dev -x /bin/bash
|
|
128
|
-
manyoyo rm my-dev
|
|
129
|
-
manyoyo serve 127.0.0.1:3000
|
|
130
|
-
manyoyo serve 127.0.0.1:3000 -U admin -P 123456
|
|
157
|
+
MANYOYO 的配置重点不是“多”,而是“可预测”:
|
|
131
158
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
```
|
|
159
|
+
- 标量值按 `命令行参数 > runs.<name> > 全局配置 > 默认值` 覆盖
|
|
160
|
+
- 数组值按 `全局配置 -> runs.<name> -> 命令行参数` 追加合并
|
|
161
|
+
- `env` 使用 map 合并,按 key 覆盖
|
|
136
162
|
|
|
137
|
-
|
|
163
|
+
相关文档:
|
|
138
164
|
|
|
139
|
-
配置优先级:命令行参数 > runs.<name> > 全局配置 > 默认值
|
|
140
|
-
详细说明请参考:
|
|
141
165
|
- [配置系统概览](https://xcanwin.github.io/manyoyo/zh/configuration/)
|
|
142
|
-
- [环境变量详解](https://xcanwin.github.io/manyoyo/zh/configuration/environment)
|
|
143
166
|
- [配置文件详解](https://xcanwin.github.io/manyoyo/zh/configuration/config-files)
|
|
167
|
+
- [环境变量详解](https://xcanwin.github.io/manyoyo/zh/configuration/environment)
|
|
144
168
|
|
|
145
|
-
##
|
|
169
|
+
## 文档入口
|
|
146
170
|
|
|
147
|
-
|
|
171
|
+
中文文档:
|
|
148
172
|
|
|
149
|
-
**中文文档:**
|
|
150
173
|
- [快速开始](https://xcanwin.github.io/manyoyo/zh/guide/quick-start)
|
|
151
174
|
- [安装详解](https://xcanwin.github.io/manyoyo/zh/guide/installation)
|
|
152
|
-
- [
|
|
175
|
+
- [CLI 选项](https://xcanwin.github.io/manyoyo/zh/reference/cli-options)
|
|
153
176
|
- [故障排查](https://xcanwin.github.io/manyoyo/zh/troubleshooting/)
|
|
154
177
|
|
|
155
|
-
|
|
178
|
+
English Documentation:
|
|
179
|
+
|
|
156
180
|
- [Quick Start](https://xcanwin.github.io/manyoyo/en/guide/quick-start)
|
|
157
181
|
- [Installation](https://xcanwin.github.io/manyoyo/en/guide/installation)
|
|
158
|
-
- [
|
|
182
|
+
- [CLI Options](https://xcanwin.github.io/manyoyo/en/reference/cli-options)
|
|
159
183
|
- [Troubleshooting](https://xcanwin.github.io/manyoyo/en/troubleshooting/)
|
|
160
184
|
|
|
161
|
-
##
|
|
185
|
+
## 安装与卸载
|
|
186
|
+
|
|
187
|
+
安装:
|
|
162
188
|
|
|
163
189
|
```bash
|
|
164
|
-
|
|
165
|
-
|
|
190
|
+
npm install -g @xcanwin/manyoyo
|
|
191
|
+
```
|
|
166
192
|
|
|
167
|
-
|
|
168
|
-
|
|
193
|
+
卸载:
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
npm uninstall -g @xcanwin/manyoyo
|
|
197
|
+
rm -rf ~/.manyoyo/ # 可选
|
|
169
198
|
```
|
|
170
199
|
|
|
171
200
|
## 许可证
|
|
@@ -174,6 +203,7 @@ MIT
|
|
|
174
203
|
|
|
175
204
|
## 贡献
|
|
176
205
|
|
|
177
|
-
欢迎提交 Issue 和 Pull Request
|
|
206
|
+
欢迎提交 Issue 和 Pull Request。
|
|
178
207
|
|
|
179
|
-
|
|
208
|
+
- Issues: <https://github.com/xcanwin/manyoyo/issues>
|
|
209
|
+
- Repository: <https://github.com/xcanwin/manyoyo>
|
package/bin/manyoyo.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
const { spawnSync } = require('child_process');
|
|
3
|
+
const { spawn, spawnSync } = require('child_process');
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const os = require('os');
|
|
@@ -16,6 +16,12 @@ const { buildImage } = require('../lib/image-build');
|
|
|
16
16
|
const { resolveAgentResumeArg, buildAgentResumeCommand } = require('../lib/agent-resume');
|
|
17
17
|
const { runPluginCommand } = require('../lib/plugin');
|
|
18
18
|
const { buildManyoyoLogPath } = require('../lib/log-path');
|
|
19
|
+
const {
|
|
20
|
+
sanitizeSensitiveData,
|
|
21
|
+
sanitizeServeLogText,
|
|
22
|
+
formatServeLogValue,
|
|
23
|
+
getServeProcessSnapshot
|
|
24
|
+
} = require('../lib/serve-log');
|
|
19
25
|
const { version: BIN_VERSION, imageVersion: IMAGE_VERSION_DEFAULT } = require('../package.json');
|
|
20
26
|
const IMAGE_VERSION_BASE = String(IMAGE_VERSION_DEFAULT || '1.0.0').split('-')[0];
|
|
21
27
|
const IMAGE_VERSION_HELP_EXAMPLE = IMAGE_VERSION_DEFAULT || `${IMAGE_VERSION_BASE}-common`;
|
|
@@ -190,93 +196,6 @@ function ensureWebServerAuthCredentials() {
|
|
|
190
196
|
}
|
|
191
197
|
}
|
|
192
198
|
|
|
193
|
-
/**
|
|
194
|
-
* 敏感信息脱敏(用于 config show 输出)
|
|
195
|
-
* @param {Object} obj - 配置对象
|
|
196
|
-
* @returns {Object} 脱敏后的配置对象
|
|
197
|
-
*/
|
|
198
|
-
function sanitizeSensitiveData(obj) {
|
|
199
|
-
const sensitiveKeys = ['KEY', 'TOKEN', 'SECRET', 'PASSWORD', 'PASS', 'AUTH', 'CREDENTIAL'];
|
|
200
|
-
|
|
201
|
-
function sanitizeValue(key, value) {
|
|
202
|
-
if (typeof value !== 'string') return value;
|
|
203
|
-
const upperKey = key.toUpperCase();
|
|
204
|
-
if (sensitiveKeys.some(k => upperKey.includes(k))) {
|
|
205
|
-
if (value.length <= 8) return '****';
|
|
206
|
-
return value.slice(0, 4) + '****' + value.slice(-4);
|
|
207
|
-
}
|
|
208
|
-
return value;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function sanitizeArray(arr) {
|
|
212
|
-
return arr.map(item => {
|
|
213
|
-
if (typeof item === 'string' && item.includes('=')) {
|
|
214
|
-
const idx = item.indexOf('=');
|
|
215
|
-
const key = item.slice(0, idx);
|
|
216
|
-
const value = item.slice(idx + 1);
|
|
217
|
-
return `${key}=${sanitizeValue(key, value)}`;
|
|
218
|
-
}
|
|
219
|
-
return item;
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
const result = {};
|
|
224
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
225
|
-
if (Array.isArray(value)) {
|
|
226
|
-
result[key] = sanitizeArray(value);
|
|
227
|
-
} else if (typeof value === 'object' && value !== null) {
|
|
228
|
-
result[key] = sanitizeSensitiveData(value);
|
|
229
|
-
} else {
|
|
230
|
-
result[key] = sanitizeValue(key, value);
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
return result;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
function stripAnsi(text) {
|
|
237
|
-
if (typeof text !== 'string') return '';
|
|
238
|
-
return text.replace(/\x1b\[[0-9;]*m/g, '');
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function sanitizeServeLogText(input) {
|
|
242
|
-
let text = stripAnsi(String(input || ''));
|
|
243
|
-
if (!text) return text;
|
|
244
|
-
|
|
245
|
-
text = text.replace(/(--pass|-P)\s+\S+/gi, '$1 ****');
|
|
246
|
-
text = text.replace(
|
|
247
|
-
/\b(MANYOYO_SERVER_PASS|OPENAI_API_KEY|ANTHROPIC_AUTH_TOKEN|GEMINI_API_KEY|GOOGLE_API_KEY|OPENCODE_API_KEY)\s*=\s*([^\s'"]+)/gi,
|
|
248
|
-
'$1=****'
|
|
249
|
-
);
|
|
250
|
-
text = text.replace(
|
|
251
|
-
/("?(?:password|pass|token|api[_-]?key|authorization|cookie)"?\s*[:=]\s*)("[^"]*"|'[^']*'|[^,\s]+)/gi,
|
|
252
|
-
'$1"****"'
|
|
253
|
-
);
|
|
254
|
-
return text;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
function formatServeLogValue(value) {
|
|
258
|
-
if (value instanceof Error) {
|
|
259
|
-
return sanitizeServeLogText(value.stack || value.message || String(value));
|
|
260
|
-
}
|
|
261
|
-
if (typeof value === 'object' && value !== null) {
|
|
262
|
-
try {
|
|
263
|
-
return sanitizeServeLogText(JSON.stringify(sanitizeSensitiveData(value)));
|
|
264
|
-
} catch (e) {
|
|
265
|
-
return sanitizeServeLogText(String(value));
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
return sanitizeServeLogText(String(value));
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
function getServeProcessSnapshot() {
|
|
272
|
-
return {
|
|
273
|
-
pid: process.pid,
|
|
274
|
-
ppid: process.ppid,
|
|
275
|
-
cwd: process.cwd(),
|
|
276
|
-
argv: Array.isArray(process.argv) ? process.argv.slice() : []
|
|
277
|
-
};
|
|
278
|
-
}
|
|
279
|
-
|
|
280
199
|
function createServeLogger() {
|
|
281
200
|
function formatLocalTimestamp(date = new Date()) {
|
|
282
201
|
const y = date.getFullYear();
|
|
@@ -1120,7 +1039,8 @@ async function setupCommander() {
|
|
|
1120
1039
|
${MANYOYO_NAME} run -n test -- -c 恢复之前会话
|
|
1121
1040
|
${MANYOYO_NAME} run -x "echo 123" 使用完整命令
|
|
1122
1041
|
${MANYOYO_NAME} serve 127.0.0.1:3000 启动本机网页服务
|
|
1123
|
-
${MANYOYO_NAME} serve
|
|
1042
|
+
${MANYOYO_NAME} serve 127.0.0.1:3000 -d 后台启动;未设密码时会打印本次随机密码
|
|
1043
|
+
${MANYOYO_NAME} serve 0.0.0.0:3000 -U admin -P 123 -d 后台启动并监听全部网卡
|
|
1124
1044
|
${MANYOYO_NAME} playwright up host-headless 启动 playwright 默认场景(推荐)
|
|
1125
1045
|
${MANYOYO_NAME} plugin playwright up host-headless 通过 plugin 命名空间启动
|
|
1126
1046
|
${MANYOYO_NAME} run -n test -q tip -q cmd 多次使用静默选项
|
|
@@ -1163,6 +1083,7 @@ Notes:
|
|
|
1163
1083
|
|
|
1164
1084
|
const serveCommand = program.command('serve [listen]').description('启动网页交互服务 (默认 127.0.0.1:3000)');
|
|
1165
1085
|
applyRunStyleOptions(serveCommand, { includeRmOnExit: false, includeWebAuthOptions: true });
|
|
1086
|
+
serveCommand.option('-d, --detach', '后台启动网页服务并立即返回');
|
|
1166
1087
|
serveCommand.action((listen, options) => {
|
|
1167
1088
|
selectAction('serve', {
|
|
1168
1089
|
...options,
|
|
@@ -1498,6 +1419,7 @@ Notes:
|
|
|
1498
1419
|
isRemoveMode,
|
|
1499
1420
|
isShowCommandMode,
|
|
1500
1421
|
isServerMode,
|
|
1422
|
+
isServerDetach: Boolean(selectedAction === 'serve' && options.detach),
|
|
1501
1423
|
isPluginMode: false
|
|
1502
1424
|
};
|
|
1503
1425
|
}
|
|
@@ -1524,6 +1446,7 @@ function createRuntimeContext(modeState = {}) {
|
|
|
1524
1446
|
showCommand: Boolean(modeState.isShowCommandMode),
|
|
1525
1447
|
rmOnExit: RM_ON_EXIT,
|
|
1526
1448
|
serverMode: Boolean(modeState.isServerMode),
|
|
1449
|
+
serverDetach: Boolean(modeState.isServerDetach),
|
|
1527
1450
|
serverHost: SERVER_HOST,
|
|
1528
1451
|
serverPort: SERVER_PORT,
|
|
1529
1452
|
serverAuthUser: SERVER_AUTH_USER,
|
|
@@ -1558,6 +1481,62 @@ function validateHostPath(runtime) {
|
|
|
1558
1481
|
}
|
|
1559
1482
|
}
|
|
1560
1483
|
|
|
1484
|
+
function validateHostPathOrThrow(hostPath) {
|
|
1485
|
+
if (!fs.existsSync(hostPath)) {
|
|
1486
|
+
throw new Error(`宿主机路径不存在: ${hostPath}`);
|
|
1487
|
+
}
|
|
1488
|
+
const realHostPath = fs.realpathSync(hostPath);
|
|
1489
|
+
const homeDir = process.env.HOME || '/home';
|
|
1490
|
+
if (realHostPath === '/' || realHostPath === '/home' || realHostPath === homeDir) {
|
|
1491
|
+
throw new Error('不允许挂载根目录或home目录。');
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
function buildDetachedServeArgv(argv) {
|
|
1496
|
+
const result = [];
|
|
1497
|
+
for (let i = 0; i < argv.length; i++) {
|
|
1498
|
+
const arg = String(argv[i] || '');
|
|
1499
|
+
if (arg === '-d' || arg === '--detach') {
|
|
1500
|
+
continue;
|
|
1501
|
+
}
|
|
1502
|
+
result.push(arg);
|
|
1503
|
+
}
|
|
1504
|
+
return result;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
function buildDetachedServeEnv(runtime) {
|
|
1508
|
+
const env = { ...process.env };
|
|
1509
|
+
if (runtime.serverAuthUser) {
|
|
1510
|
+
env.MANYOYO_SERVER_USER = runtime.serverAuthUser;
|
|
1511
|
+
}
|
|
1512
|
+
if (runtime.serverAuthPass) {
|
|
1513
|
+
env.MANYOYO_SERVER_PASS = runtime.serverAuthPass;
|
|
1514
|
+
}
|
|
1515
|
+
return env;
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
function relaunchServeDetached(runtime) {
|
|
1519
|
+
const serveLog = buildManyoyoLogPath('serve');
|
|
1520
|
+
fs.mkdirSync(serveLog.dir, { recursive: true });
|
|
1521
|
+
|
|
1522
|
+
const child = spawn(process.argv[0], buildDetachedServeArgv(process.argv.slice(1)), {
|
|
1523
|
+
detached: true,
|
|
1524
|
+
stdio: 'ignore',
|
|
1525
|
+
env: buildDetachedServeEnv(runtime)
|
|
1526
|
+
});
|
|
1527
|
+
child.unref();
|
|
1528
|
+
|
|
1529
|
+
console.log(`${GREEN}✅ serve 已转入后台运行${NC}`);
|
|
1530
|
+
console.log(`PID: ${child.pid}`);
|
|
1531
|
+
console.log(`日志: ${serveLog.path}`);
|
|
1532
|
+
console.log(`登录用户名: ${runtime.serverAuthUser}`);
|
|
1533
|
+
if (runtime.serverAuthPassAuto) {
|
|
1534
|
+
console.log(`登录密码(本次随机): ${runtime.serverAuthPass}`);
|
|
1535
|
+
} else {
|
|
1536
|
+
console.log('登录密码: 使用你配置的 serve -P / serverPass / MANYOYO_SERVER_PASS');
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1561
1540
|
/**
|
|
1562
1541
|
* 等待容器就绪(使用指数退避算法)
|
|
1563
1542
|
* @param {string} containerName - 容器名称
|
|
@@ -1850,7 +1829,7 @@ async function runWebServerMode(runtime) {
|
|
|
1850
1829
|
containerEnvs: runtime.containerEnvs,
|
|
1851
1830
|
containerVolumes: runtime.containerVolumes,
|
|
1852
1831
|
containerPorts: runtime.containerPorts,
|
|
1853
|
-
validateHostPath:
|
|
1832
|
+
validateHostPath: value => validateHostPathOrThrow(value),
|
|
1854
1833
|
formatDate,
|
|
1855
1834
|
isValidContainerName,
|
|
1856
1835
|
containerExists,
|
|
@@ -1892,6 +1871,10 @@ async function main() {
|
|
|
1892
1871
|
|
|
1893
1872
|
// 2. Start web server mode
|
|
1894
1873
|
if (runtime.serverMode) {
|
|
1874
|
+
if (runtime.serverDetach) {
|
|
1875
|
+
relaunchServeDetached(runtime);
|
|
1876
|
+
return;
|
|
1877
|
+
}
|
|
1895
1878
|
const serveLogger = createServeLogger();
|
|
1896
1879
|
runtime.logger = serveLogger;
|
|
1897
1880
|
installServeProcessDiagnostics(serveLogger);
|
package/lib/serve-log.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
function stripAnsi(text) {
|
|
2
|
+
if (typeof text !== 'string') return '';
|
|
3
|
+
return text.replace(/\x1b\[[0-9;]*m/g, '');
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function sanitizeProcessArgv(argv) {
|
|
7
|
+
if (!Array.isArray(argv)) {
|
|
8
|
+
return [];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const result = [];
|
|
12
|
+
for (let i = 0; i < argv.length; i++) {
|
|
13
|
+
const arg = String(argv[i] || '');
|
|
14
|
+
if (arg === '--pass' || arg === '-P') {
|
|
15
|
+
result.push(arg);
|
|
16
|
+
if (i + 1 < argv.length) {
|
|
17
|
+
result.push('****');
|
|
18
|
+
i += 1;
|
|
19
|
+
}
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (arg.startsWith('--pass=')) {
|
|
23
|
+
result.push('--pass=****');
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
result.push(arg);
|
|
27
|
+
}
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function sanitizeServeLogText(input) {
|
|
32
|
+
let text = stripAnsi(String(input || ''));
|
|
33
|
+
if (!text) return text;
|
|
34
|
+
|
|
35
|
+
text = text.replace(/(--pass|-P)\s+\S+/gi, '$1 ****');
|
|
36
|
+
text = text.replace(/("--pass"|"-P")\s*,\s*"[^"]*"/gi, '$1,"****"');
|
|
37
|
+
text = text.replace(/--pass=([^\s'"]+)/gi, '--pass=****');
|
|
38
|
+
text = text.replace(
|
|
39
|
+
/\b(MANYOYO_SERVER_PASS|OPENAI_API_KEY|ANTHROPIC_AUTH_TOKEN|GEMINI_API_KEY|GOOGLE_API_KEY|OPENCODE_API_KEY)\s*=\s*([^\s'"]+)/gi,
|
|
40
|
+
'$1=****'
|
|
41
|
+
);
|
|
42
|
+
text = text.replace(
|
|
43
|
+
/(?<![-\w])("?(?:password|pass|token|api[_-]?key|authorization|cookie)"?\s*[:=]\s*)("[^"]*"|'[^']*'|[^,\s]+)/gi,
|
|
44
|
+
'$1"****"'
|
|
45
|
+
);
|
|
46
|
+
return text;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function sanitizeSensitiveData(obj) {
|
|
50
|
+
const sensitiveKeys = ['KEY', 'TOKEN', 'SECRET', 'PASSWORD', 'PASS', 'AUTH', 'CREDENTIAL'];
|
|
51
|
+
|
|
52
|
+
function sanitizeValue(key, value) {
|
|
53
|
+
if (typeof value !== 'string') return value;
|
|
54
|
+
const upperKey = key.toUpperCase();
|
|
55
|
+
if (sensitiveKeys.some(k => upperKey.includes(k))) {
|
|
56
|
+
if (value.length <= 8) return '****';
|
|
57
|
+
return value.slice(0, 4) + '****' + value.slice(-4);
|
|
58
|
+
}
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function sanitizeArray(arr) {
|
|
63
|
+
return arr.map(item => {
|
|
64
|
+
if (typeof item === 'string' && item.includes('=')) {
|
|
65
|
+
const idx = item.indexOf('=');
|
|
66
|
+
const key = item.slice(0, idx);
|
|
67
|
+
const value = item.slice(idx + 1);
|
|
68
|
+
return `${key}=${sanitizeValue(key, value)}`;
|
|
69
|
+
}
|
|
70
|
+
return item;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const result = {};
|
|
75
|
+
for (const [key, value] of Object.entries(obj || {})) {
|
|
76
|
+
if (Array.isArray(value)) {
|
|
77
|
+
result[key] = sanitizeArray(value);
|
|
78
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
79
|
+
result[key] = sanitizeSensitiveData(value);
|
|
80
|
+
} else {
|
|
81
|
+
result[key] = sanitizeValue(key, value);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function formatServeLogValue(value) {
|
|
88
|
+
if (value instanceof Error) {
|
|
89
|
+
return sanitizeServeLogText(value.stack || value.message || String(value));
|
|
90
|
+
}
|
|
91
|
+
if (typeof value === 'object' && value !== null) {
|
|
92
|
+
try {
|
|
93
|
+
return sanitizeServeLogText(JSON.stringify(sanitizeSensitiveData(value)));
|
|
94
|
+
} catch (e) {
|
|
95
|
+
return sanitizeServeLogText(String(value));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return sanitizeServeLogText(String(value));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function getServeProcessSnapshot(processRef = process) {
|
|
102
|
+
return {
|
|
103
|
+
pid: processRef.pid,
|
|
104
|
+
ppid: processRef.ppid,
|
|
105
|
+
cwd: typeof processRef.cwd === 'function' ? processRef.cwd() : '',
|
|
106
|
+
argv: sanitizeProcessArgv(Array.isArray(processRef.argv) ? processRef.argv.slice() : [])
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
module.exports = {
|
|
111
|
+
sanitizeProcessArgv,
|
|
112
|
+
sanitizeServeLogText,
|
|
113
|
+
sanitizeSensitiveData,
|
|
114
|
+
formatServeLogValue,
|
|
115
|
+
getServeProcessSnapshot
|
|
116
|
+
};
|
package/lib/web/server.js
CHANGED
|
@@ -812,7 +812,11 @@ function buildCreateRuntime(ctx, state, payload) {
|
|
|
812
812
|
validateContainerNameStrict(containerName);
|
|
813
813
|
|
|
814
814
|
const hostPath = pickFirstString(requestOptions.hostPath, config.hostPath, ctx.hostPath);
|
|
815
|
-
|
|
815
|
+
if (typeof ctx.validateHostPath === 'function') {
|
|
816
|
+
ctx.validateHostPath(hostPath);
|
|
817
|
+
} else {
|
|
818
|
+
validateWebHostPath(hostPath);
|
|
819
|
+
}
|
|
816
820
|
|
|
817
821
|
const containerPath = pickFirstString(requestOptions.containerPath, config.containerPath, ctx.containerPath, hostPath) || hostPath;
|
|
818
822
|
const imageName = pickFirstString(requestOptions.imageName, config.imageName, ctx.imageName);
|
|
@@ -1788,7 +1792,7 @@ async function startWebServer(options) {
|
|
|
1788
1792
|
};
|
|
1789
1793
|
|
|
1790
1794
|
if (!ctx.authUser || !ctx.authPass) {
|
|
1791
|
-
throw new Error('Web 认证配置缺失,请设置 serve -
|
|
1795
|
+
throw new Error('Web 认证配置缺失,请设置 serve -U / serve -P');
|
|
1792
1796
|
}
|
|
1793
1797
|
|
|
1794
1798
|
const state = {
|
|
@@ -1798,7 +1802,6 @@ async function startWebServer(options) {
|
|
|
1798
1802
|
terminalSessions: new Map()
|
|
1799
1803
|
};
|
|
1800
1804
|
|
|
1801
|
-
ctx.validateHostPath();
|
|
1802
1805
|
ensureWebHistoryDir(state.webHistoryDir);
|
|
1803
1806
|
|
|
1804
1807
|
const wsServer = new WebSocket.Server({
|