@xcanwin/manyoyo 4.1.1 → 4.1.10
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/bin/manyoyo.js +325 -1016
- package/docker/manyoyo.Dockerfile +36 -39
- package/lib/agent-resume.js +72 -0
- package/lib/container-run.js +39 -0
- package/lib/image-build.js +323 -0
- package/lib/init-config.js +401 -0
- package/lib/web/frontend/app.css +420 -190
- package/lib/web/frontend/app.html +71 -4
- package/lib/web/frontend/app.js +840 -136
- package/lib/web/frontend/login.css +77 -63
- package/lib/web/server.js +757 -128
- package/package.json +2 -2
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
FROM ubuntu:24.04 AS cache-stage
|
|
5
5
|
|
|
6
6
|
ARG TARGETARCH
|
|
7
|
+
ARG TOOL="common"
|
|
7
8
|
|
|
8
9
|
# 复制缓存目录(可能为空)
|
|
9
10
|
COPY ./docker/cache/ /cache/
|
|
@@ -29,27 +30,31 @@ RUN <<EOX
|
|
|
29
30
|
curl -fsSL ${NVM_NODEJS_ORG_MIRROR}/latest-v24.x/${NODE_TAR} | tar -xz -C /opt/node --strip-components=1 --exclude='*.md' --exclude='LICENSE'
|
|
30
31
|
fi
|
|
31
32
|
|
|
32
|
-
# JDT LSP:
|
|
33
|
+
# JDT LSP: 仅在 full/java 时准备缓存
|
|
33
34
|
mkdir -p /opt/jdtls
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
35
|
+
case ",$TOOL," in *,full,*|*,java,*)
|
|
36
|
+
if [ -f /cache/jdtls/jdt-language-server-latest.tar.gz ]; then
|
|
37
|
+
echo "使用 JDT LSP 缓存"
|
|
38
|
+
tar -xzf /cache/jdtls/jdt-language-server-latest.tar.gz -C /opt/jdtls --no-same-owner
|
|
39
|
+
else
|
|
40
|
+
echo "下载 JDT LSP"
|
|
41
|
+
curl -fsSL https://download.eclipse.org/jdtls/snapshots/jdt-language-server-latest.tar.gz | tar -xz -C /opt/jdtls
|
|
42
|
+
fi
|
|
43
|
+
;; esac
|
|
41
44
|
|
|
42
|
-
# gopls:
|
|
45
|
+
# gopls: 仅在 full/go 时准备缓存
|
|
43
46
|
mkdir -p /opt/gopls
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
47
|
+
case ",$TOOL," in *,full,*|*,go,*)
|
|
48
|
+
if [ -f /cache/gopls/gopls-linux-${ARCH_GO} ]; then
|
|
49
|
+
echo "使用 gopls 缓存"
|
|
50
|
+
cp /cache/gopls/gopls-linux-${ARCH_GO} /opt/gopls/gopls
|
|
51
|
+
chmod +x /opt/gopls/gopls
|
|
52
|
+
else
|
|
53
|
+
echo "下载 gopls (需要 go 环境)"
|
|
54
|
+
# gopls 需要编译,这里跳过,在最终阶段处理
|
|
55
|
+
touch /opt/gopls/.no-cache
|
|
56
|
+
fi
|
|
57
|
+
;; esac
|
|
53
58
|
EOX
|
|
54
59
|
|
|
55
60
|
# ==============================================================================
|
|
@@ -68,6 +73,8 @@ ARG PIP_INDEX_URL=https://mirrors.tencent.com/pypi/simple
|
|
|
68
73
|
# 轻量级文本解析依赖(可通过 --build-arg 覆盖)
|
|
69
74
|
ARG PY_TEXT_PIP_PACKAGES="PyYAML python-dotenv tomlkit pyjson5 jsonschema"
|
|
70
75
|
ARG PY_TEXT_EXTRA_PIP_PACKAGES=""
|
|
76
|
+
ENV LANG=C.UTF-8 \
|
|
77
|
+
LC_ALL=C.UTF-8
|
|
71
78
|
|
|
72
79
|
# 合并系统依赖安装为单层,减少镜像体积
|
|
73
80
|
RUN <<EOX
|
|
@@ -128,7 +135,6 @@ ARG GIT_SSL_NO_VERIFY=false
|
|
|
128
135
|
RUN <<EOX
|
|
129
136
|
# 配置 node.js
|
|
130
137
|
npm config set registry=${NPM_REGISTRY}
|
|
131
|
-
npm install -g npm
|
|
132
138
|
|
|
133
139
|
# 安装 LSP服务(python、typescript)
|
|
134
140
|
npm install -g pyright typescript-language-server typescript
|
|
@@ -151,8 +157,12 @@ EOF
|
|
|
151
157
|
claude plugin install ralph-loop@claude-plugins-official
|
|
152
158
|
claude plugin install typescript-lsp@claude-plugins-official
|
|
153
159
|
claude plugin install pyright-lsp@claude-plugins-official
|
|
154
|
-
|
|
155
|
-
|
|
160
|
+
case ",$TOOL," in *,full,*|*,go,*)
|
|
161
|
+
claude plugin install gopls-lsp@claude-plugins-official
|
|
162
|
+
;; esac
|
|
163
|
+
case ",$TOOL," in *,full,*|*,java,*)
|
|
164
|
+
claude plugin install jdtls-lsp@claude-plugins-official
|
|
165
|
+
;; esac
|
|
156
166
|
|
|
157
167
|
GIT_SSL_NO_VERIFY=$GIT_SSL_NO_VERIFY claude plugin marketplace add https://github.com/anthropics/skills
|
|
158
168
|
claude plugin install example-skills@anthropic-agent-skills
|
|
@@ -200,21 +210,6 @@ enabled = false
|
|
|
200
210
|
EOF
|
|
201
211
|
;; esac
|
|
202
212
|
|
|
203
|
-
# 安装 Copilot CLI
|
|
204
|
-
case ",$TOOL," in *,full,*|*,copilot,*)
|
|
205
|
-
npm install -g @github/copilot
|
|
206
|
-
mkdir -p ~/.copilot/
|
|
207
|
-
cat > ~/.copilot/config.json <<EOF
|
|
208
|
-
{
|
|
209
|
-
"banner": "never",
|
|
210
|
-
"model": "gemini-3-pro-preview",
|
|
211
|
-
"render_markdown": true,
|
|
212
|
-
"screen_reader": false,
|
|
213
|
-
"theme": "auto"
|
|
214
|
-
}
|
|
215
|
-
EOF
|
|
216
|
-
;; esac
|
|
217
|
-
|
|
218
213
|
# 安装 OpenCode CLI
|
|
219
214
|
case ",$TOOL," in *,full,*|*,opencode,*)
|
|
220
215
|
npm install -g opencode-ai
|
|
@@ -251,7 +246,7 @@ EOF
|
|
|
251
246
|
EOX
|
|
252
247
|
|
|
253
248
|
# 从 cache-stage 复制 JDT LSP(缓存或下载)
|
|
254
|
-
COPY --from=cache-stage /opt/jdtls /
|
|
249
|
+
COPY --from=cache-stage /opt/jdtls /tmp/jdtls-cache
|
|
255
250
|
|
|
256
251
|
RUN <<EOX
|
|
257
252
|
# 安装 java
|
|
@@ -260,12 +255,15 @@ RUN <<EOX
|
|
|
260
255
|
apt-get install -y --no-install-recommends openjdk-21-jdk maven
|
|
261
256
|
|
|
262
257
|
# 配置 LSP服务(java)
|
|
258
|
+
mkdir -p ~/.local/share/
|
|
259
|
+
cp -a /tmp/jdtls-cache ~/.local/share/jdtls
|
|
263
260
|
ln -sf ~/.local/share/jdtls/bin/jdtls /usr/local/bin/jdtls
|
|
264
261
|
|
|
265
262
|
# 清理
|
|
266
263
|
apt-get clean
|
|
267
264
|
rm -rf /tmp/* /var/tmp/* /var/log/apt /var/log/*.log /var/lib/apt/lists/* ~/.cache ~/.npm ~/go/pkg/mod/cache
|
|
268
265
|
;; esac
|
|
266
|
+
rm -rf /tmp/jdtls-cache
|
|
269
267
|
EOX
|
|
270
268
|
|
|
271
269
|
# 从 cache-stage 复制 gopls(缓存或下载)
|
|
@@ -288,13 +286,12 @@ RUN <<EOX
|
|
|
288
286
|
go install golang.org/x/tools/gopls@latest
|
|
289
287
|
ln -sf ~/go/bin/gopls /usr/local/bin/gopls
|
|
290
288
|
fi
|
|
291
|
-
rm -rf /tmp/gopls-cache
|
|
292
|
-
|
|
293
289
|
# 清理
|
|
294
290
|
apt-get clean
|
|
295
291
|
go clean -modcache -cache
|
|
296
292
|
rm -rf /tmp/* /var/tmp/* /var/log/apt /var/log/*.log /var/lib/apt/lists/* ~/.cache ~/.npm ~/go/pkg/mod/cache
|
|
297
293
|
;; esac
|
|
294
|
+
rm -rf /tmp/gopls-cache
|
|
298
295
|
EOX
|
|
299
296
|
|
|
300
297
|
RUN <<EOX
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
const AGENT_RESUME_ARG_MAP = {
|
|
6
|
+
claude: '-r',
|
|
7
|
+
gemini: '-r',
|
|
8
|
+
codex: 'resume',
|
|
9
|
+
opencode: '-c'
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function stripLeadingAssignments(commandText) {
|
|
13
|
+
let rest = String(commandText || '').trim();
|
|
14
|
+
const assignmentPattern = /^(?:[A-Za-z_][A-Za-z0-9_]*=)(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|[^\s]+)(?:\s+|$)/;
|
|
15
|
+
|
|
16
|
+
while (rest) {
|
|
17
|
+
const matched = rest.match(assignmentPattern);
|
|
18
|
+
if (!matched) {
|
|
19
|
+
break;
|
|
20
|
+
}
|
|
21
|
+
rest = rest.slice(matched[0].length).trim();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return rest;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function readLeadingToken(commandText) {
|
|
28
|
+
const text = String(commandText || '').trim();
|
|
29
|
+
if (!text) {
|
|
30
|
+
return { token: '', rest: '' };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const tokenMatch = text.match(/^(?:"((?:\\.|[^"])*)"|'((?:\\.|[^'])*)'|([^\s]+))(?:\s+|$)/);
|
|
34
|
+
if (!tokenMatch) {
|
|
35
|
+
return { token: '', rest: '' };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const token = tokenMatch[1] || tokenMatch[2] || tokenMatch[3] || '';
|
|
39
|
+
const rest = text.slice(tokenMatch[0].length).trim();
|
|
40
|
+
return { token, rest };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function normalizeProgramName(token) {
|
|
44
|
+
if (!token) {
|
|
45
|
+
return '';
|
|
46
|
+
}
|
|
47
|
+
return path.basename(token).toLowerCase();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function resolveAgentProgram(commandText) {
|
|
51
|
+
let rest = stripLeadingAssignments(commandText);
|
|
52
|
+
let leading = readLeadingToken(rest);
|
|
53
|
+
let program = normalizeProgramName(leading.token);
|
|
54
|
+
|
|
55
|
+
// Support common wrapper: env KEY=VALUE cmd ...
|
|
56
|
+
if (program === 'env') {
|
|
57
|
+
rest = stripLeadingAssignments(leading.rest);
|
|
58
|
+
leading = readLeadingToken(rest);
|
|
59
|
+
program = normalizeProgramName(leading.token);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return program;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function resolveAgentResumeArg(commandText) {
|
|
66
|
+
const program = resolveAgentProgram(commandText);
|
|
67
|
+
return AGENT_RESUME_ARG_MAP[program] || '';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = {
|
|
71
|
+
resolveAgentResumeArg
|
|
72
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function sanitizeDefaultCommand(defaultCommand) {
|
|
4
|
+
return String(defaultCommand || '').replace(/[\r\n]/g, ' ');
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function buildContainerRunArgs(options) {
|
|
8
|
+
const args = [
|
|
9
|
+
'run', '-d',
|
|
10
|
+
'--name', options.containerName,
|
|
11
|
+
'--entrypoint', '',
|
|
12
|
+
...(options.contModeArgs || []),
|
|
13
|
+
...(options.containerEnvs || []),
|
|
14
|
+
...(options.containerVolumes || []),
|
|
15
|
+
'--volume', `${options.hostPath}:${options.containerPath}`,
|
|
16
|
+
'--workdir', options.containerPath,
|
|
17
|
+
'--label', `manyoyo.default_cmd=${sanitizeDefaultCommand(options.defaultCommand)}`,
|
|
18
|
+
`${options.imageName}:${options.imageVersion}`,
|
|
19
|
+
'tail', '-f', '/dev/null'
|
|
20
|
+
];
|
|
21
|
+
return args;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function quoteShellArg(value) {
|
|
25
|
+
const text = String(value);
|
|
26
|
+
if (text.includes(' ') || text.includes('"') || text.includes('=')) {
|
|
27
|
+
return `"${text.replace(/"/g, '\\"')}"`;
|
|
28
|
+
}
|
|
29
|
+
return text;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function buildContainerRunCommand(dockerCmd, args) {
|
|
33
|
+
return `${dockerCmd} ${args.map(quoteShellArg).join(' ')}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = {
|
|
37
|
+
buildContainerRunArgs,
|
|
38
|
+
buildContainerRunCommand
|
|
39
|
+
};
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const crypto = require('crypto');
|
|
6
|
+
|
|
7
|
+
function getFileSha256(filePath) {
|
|
8
|
+
return crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function quoteShellArg(value) {
|
|
12
|
+
const text = String(value);
|
|
13
|
+
if (text.includes(' ') || text.includes('"') || text.includes('=')) {
|
|
14
|
+
return `"${text.replace(/"/g, '\\"')}"`;
|
|
15
|
+
}
|
|
16
|
+
return text;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function ensureDirectoryIfMissing(dirPath) {
|
|
20
|
+
if (!fs.existsSync(dirPath)) fs.mkdirSync(dirPath, { recursive: true });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function loadBuildCacheTimestamps(timestampFile) {
|
|
24
|
+
if (!fs.existsSync(timestampFile)) return {};
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(fs.readFileSync(timestampFile, 'utf-8'));
|
|
27
|
+
} catch (e) {
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function saveBuildCacheTimestamps(timestampFile, timestamps) {
|
|
33
|
+
fs.writeFileSync(timestampFile, JSON.stringify(timestamps, null, 4));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function createBuildCacheContext(ctx) {
|
|
37
|
+
const cacheDir = path.join(ctx.rootDir, 'docker', 'cache');
|
|
38
|
+
ensureDirectoryIfMissing(cacheDir);
|
|
39
|
+
|
|
40
|
+
const config = ctx.loadConfig();
|
|
41
|
+
const timestampFile = path.join(cacheDir, '.timestamps.json');
|
|
42
|
+
return {
|
|
43
|
+
cacheDir,
|
|
44
|
+
timestampFile,
|
|
45
|
+
cacheTTLDays: config.cacheTTL || ctx.cacheTtlDays,
|
|
46
|
+
nodeMirrors: [config.nodeMirror, 'https://mirrors.tencent.com/nodejs-release', 'https://nodejs.org/dist'].filter(Boolean),
|
|
47
|
+
timestamps: loadBuildCacheTimestamps(timestampFile),
|
|
48
|
+
now: new Date()
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isBuildCacheExpired(cache, key) {
|
|
53
|
+
if (!cache.timestamps[key]) return true;
|
|
54
|
+
const cachedTime = new Date(cache.timestamps[key]);
|
|
55
|
+
const diffDays = (cache.now - cachedTime) / (1000 * 60 * 60 * 24);
|
|
56
|
+
return diffDays > cache.cacheTTLDays;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function touchBuildCache(cache, key) {
|
|
60
|
+
cache.timestamps[key] = cache.now.toISOString();
|
|
61
|
+
saveBuildCacheTimestamps(cache.timestampFile, cache.timestamps);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function resolveBuildCacheArch() {
|
|
65
|
+
const arch = process.arch === 'x64' ? 'amd64' : process.arch === 'arm64' ? 'arm64' : process.arch;
|
|
66
|
+
return { arch, archNode: arch === 'amd64' ? 'x64' : 'arm64' };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function prepareNodeBuildCache(ctx, cache, archNode) {
|
|
70
|
+
const { RED, GREEN, YELLOW, BLUE, NC } = ctx.colors;
|
|
71
|
+
const nodeCacheDir = path.join(cache.cacheDir, 'node');
|
|
72
|
+
const nodeVersion = 24;
|
|
73
|
+
const nodeKey = 'node/';
|
|
74
|
+
|
|
75
|
+
ensureDirectoryIfMissing(nodeCacheDir);
|
|
76
|
+
const hasNodeCache = fs.readdirSync(nodeCacheDir).some(fileName => (
|
|
77
|
+
fileName.startsWith('node-') && fileName.includes(`linux-${archNode}`)
|
|
78
|
+
));
|
|
79
|
+
if (hasNodeCache && !isBuildCacheExpired(cache, nodeKey)) {
|
|
80
|
+
ctx.log(`${GREEN}✓ Node.js 缓存已存在${NC}`);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
ctx.log(`${YELLOW}下载 Node.js ${nodeVersion} (${archNode})...${NC}`);
|
|
85
|
+
|
|
86
|
+
for (const mirror of cache.nodeMirrors) {
|
|
87
|
+
try {
|
|
88
|
+
ctx.log(`${BLUE}尝试镜像源: ${mirror}${NC}`);
|
|
89
|
+
const shasumUrl = `${mirror}/latest-v${nodeVersion}.x/SHASUMS256.txt`;
|
|
90
|
+
const shasumContent = ctx.runCmd('curl', ['-fsSL', shasumUrl], { stdio: 'pipe' });
|
|
91
|
+
const shasumLine = shasumContent.split('\n').find(line => line.includes(`linux-${archNode}.tar.gz`));
|
|
92
|
+
if (!shasumLine) continue;
|
|
93
|
+
|
|
94
|
+
const [expectedHash, fileName] = shasumLine.trim().split(/\s+/);
|
|
95
|
+
const nodeTargetPath = path.join(nodeCacheDir, fileName);
|
|
96
|
+
ctx.runCmd('curl', ['-fsSL', `${mirror}/latest-v${nodeVersion}.x/${fileName}`, '-o', nodeTargetPath], { stdio: 'inherit' });
|
|
97
|
+
|
|
98
|
+
if (getFileSha256(nodeTargetPath) !== expectedHash) {
|
|
99
|
+
ctx.log(`${RED}SHA256 校验失败,删除文件${NC}`);
|
|
100
|
+
fs.unlinkSync(nodeTargetPath);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
ctx.log(`${GREEN}✓ SHA256 校验通过${NC}`);
|
|
105
|
+
touchBuildCache(cache, nodeKey);
|
|
106
|
+
ctx.log(`${GREEN}✓ Node.js 下载完成${NC}`);
|
|
107
|
+
return;
|
|
108
|
+
} catch (e) {
|
|
109
|
+
ctx.log(`${YELLOW}镜像源 ${mirror} 失败,尝试下一个...${NC}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
ctx.error(`${RED}错误: Node.js 下载失败(所有镜像源均不可用)${NC}`);
|
|
114
|
+
throw new Error('Node.js download failed');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function prepareJdtlsBuildCache(ctx, cache, imageTool) {
|
|
118
|
+
const { RED, GREEN, YELLOW, NC } = ctx.colors;
|
|
119
|
+
if (!(imageTool === 'full' || imageTool.includes('java'))) return;
|
|
120
|
+
|
|
121
|
+
const jdtlsCacheDir = path.join(cache.cacheDir, 'jdtls');
|
|
122
|
+
const jdtlsKey = 'jdtls/jdt-language-server-latest.tar.gz';
|
|
123
|
+
const jdtlsPath = path.join(cache.cacheDir, jdtlsKey);
|
|
124
|
+
|
|
125
|
+
ensureDirectoryIfMissing(jdtlsCacheDir);
|
|
126
|
+
if (fs.existsSync(jdtlsPath) && !isBuildCacheExpired(cache, jdtlsKey)) {
|
|
127
|
+
ctx.log(`${GREEN}✓ JDT LSP 缓存已存在${NC}`);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const tmpDir = path.join(jdtlsCacheDir, '.tmp-apk');
|
|
132
|
+
const apkPath = path.join(tmpDir, 'jdtls.apk');
|
|
133
|
+
ctx.log(`${YELLOW}下载 JDT Language Server...${NC}`);
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
ensureDirectoryIfMissing(tmpDir);
|
|
137
|
+
ctx.runCmd('curl', ['-fsSL', 'https://mirrors.tencent.com/alpine/latest-stable/community/x86_64/jdtls-1.53.0-r0.apk', '-o', apkPath], { stdio: 'inherit' });
|
|
138
|
+
ctx.runCmd('tar', ['-xzf', apkPath, '-C', tmpDir], { stdio: 'inherit' });
|
|
139
|
+
ctx.runCmd('tar', ['-czf', jdtlsPath, '-C', path.join(tmpDir, 'usr', 'share', 'jdtls'), '.'], { stdio: 'inherit' });
|
|
140
|
+
touchBuildCache(cache, jdtlsKey);
|
|
141
|
+
ctx.log(`${GREEN}✓ JDT LSP 下载完成${NC}`);
|
|
142
|
+
} catch (e) {
|
|
143
|
+
ctx.error(`${RED}错误: JDT LSP 下载失败${NC}`);
|
|
144
|
+
throw e;
|
|
145
|
+
} finally {
|
|
146
|
+
try { ctx.runCmd('rm', ['-rf', tmpDir], { stdio: 'inherit', ignoreError: true }); } catch (e) {}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function cleanupGoTmpPath(ctx, tmpGoPath, warnOnError) {
|
|
151
|
+
const { YELLOW, NC } = ctx.colors;
|
|
152
|
+
if (!fs.existsSync(tmpGoPath)) return;
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
ctx.runCmd('go', ['clean', '-modcache'], {
|
|
156
|
+
stdio: 'inherit',
|
|
157
|
+
ignoreError: true,
|
|
158
|
+
env: { ...process.env, GOPATH: tmpGoPath }
|
|
159
|
+
});
|
|
160
|
+
ctx.runCmd('chmod', ['-R', 'u+w', tmpGoPath], { stdio: 'inherit', ignoreError: true });
|
|
161
|
+
ctx.runCmd('rm', ['-rf', tmpGoPath], { stdio: 'inherit', ignoreError: true });
|
|
162
|
+
} catch (e) {
|
|
163
|
+
if (warnOnError) {
|
|
164
|
+
ctx.log(`${YELLOW}提示: 临时目录清理失败,可手动删除 ${tmpGoPath}${NC}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function resolveGoplsSource(tmpGoPath, arch) {
|
|
170
|
+
const primary = path.join(tmpGoPath, 'bin', `linux_${arch}`, 'gopls');
|
|
171
|
+
if (fs.existsSync(primary)) return primary;
|
|
172
|
+
const fallback = path.join(tmpGoPath, 'bin', 'gopls');
|
|
173
|
+
return fs.existsSync(fallback) ? fallback : '';
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function prepareGoplsBuildCache(ctx, cache, imageTool, arch) {
|
|
177
|
+
const { RED, GREEN, YELLOW, NC } = ctx.colors;
|
|
178
|
+
if (!(imageTool === 'full' || imageTool.includes('go'))) return;
|
|
179
|
+
|
|
180
|
+
const goplsCacheDir = path.join(cache.cacheDir, 'gopls');
|
|
181
|
+
const goplsKey = `gopls/gopls-linux-${arch}`;
|
|
182
|
+
const goplsPath = path.join(cache.cacheDir, goplsKey);
|
|
183
|
+
|
|
184
|
+
ensureDirectoryIfMissing(goplsCacheDir);
|
|
185
|
+
if (fs.existsSync(goplsPath) && !isBuildCacheExpired(cache, goplsKey)) {
|
|
186
|
+
ctx.log(`${GREEN}✓ gopls 缓存已存在${NC}`);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const tmpGoPath = path.join(cache.cacheDir, '.tmp-go');
|
|
191
|
+
ctx.log(`${YELLOW}下载 gopls (${arch})...${NC}`);
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
cleanupGoTmpPath(ctx, tmpGoPath, false);
|
|
195
|
+
ensureDirectoryIfMissing(tmpGoPath);
|
|
196
|
+
|
|
197
|
+
ctx.runCmd('go', ['install', 'golang.org/x/tools/gopls@latest'], {
|
|
198
|
+
stdio: 'inherit',
|
|
199
|
+
env: { ...process.env, GOPATH: tmpGoPath, GOOS: 'linux', GOARCH: arch }
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const sourcePath = resolveGoplsSource(tmpGoPath, arch);
|
|
203
|
+
if (!sourcePath) throw new Error(`gopls binary not found in ${tmpGoPath}`);
|
|
204
|
+
fs.copyFileSync(sourcePath, goplsPath);
|
|
205
|
+
ctx.runCmd('chmod', ['+x', goplsPath], { stdio: 'inherit' });
|
|
206
|
+
|
|
207
|
+
touchBuildCache(cache, goplsKey);
|
|
208
|
+
ctx.log(`${GREEN}✓ gopls 下载完成${NC}`);
|
|
209
|
+
cleanupGoTmpPath(ctx, tmpGoPath, true);
|
|
210
|
+
} catch (e) {
|
|
211
|
+
ctx.error(`${RED}错误: gopls 下载失败${NC}`);
|
|
212
|
+
throw e;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function prepareBuildCache(ctx, imageTool) {
|
|
217
|
+
const { CYAN, GREEN, NC } = ctx.colors;
|
|
218
|
+
const cache = createBuildCacheContext(ctx);
|
|
219
|
+
const { arch, archNode } = resolveBuildCacheArch();
|
|
220
|
+
|
|
221
|
+
ctx.log(`\n${CYAN}准备构建缓存...${NC}`);
|
|
222
|
+
prepareNodeBuildCache(ctx, cache, archNode);
|
|
223
|
+
prepareJdtlsBuildCache(ctx, cache, imageTool);
|
|
224
|
+
prepareGoplsBuildCache(ctx, cache, imageTool, arch);
|
|
225
|
+
saveBuildCacheTimestamps(cache.timestampFile, cache.timestamps);
|
|
226
|
+
ctx.log(`${GREEN}✅ 构建缓存准备完成${NC}\n`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function resolveToolFromBuildArgs(args) {
|
|
230
|
+
for (const value of args) {
|
|
231
|
+
if (typeof value === 'string' && value.startsWith('TOOL=')) return value.slice(5);
|
|
232
|
+
}
|
|
233
|
+
return '';
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function buildImage(options = {}) {
|
|
237
|
+
const ctx = {
|
|
238
|
+
imageBuildArgs: Array.isArray(options.imageBuildArgs) ? [...options.imageBuildArgs] : [],
|
|
239
|
+
imageName: options.imageName,
|
|
240
|
+
imageVersionTag: options.imageVersionTag,
|
|
241
|
+
imageVersionDefault: options.imageVersionDefault || '',
|
|
242
|
+
imageVersionBase: options.imageVersionBase || '1.0.0',
|
|
243
|
+
parseImageVersionTag: options.parseImageVersionTag,
|
|
244
|
+
manyoyoName: options.manyoyoName || 'manyoyo',
|
|
245
|
+
yesMode: Boolean(options.yesMode),
|
|
246
|
+
dockerCmd: options.dockerCmd || 'docker',
|
|
247
|
+
rootDir: options.rootDir || process.cwd(),
|
|
248
|
+
loadConfig: options.loadConfig || (() => ({})),
|
|
249
|
+
runCmd: options.runCmd,
|
|
250
|
+
askQuestion: options.askQuestion || (async () => ''),
|
|
251
|
+
pruneDanglingImages: options.pruneDanglingImages || (() => {}),
|
|
252
|
+
cacheTtlDays: options.cacheTtlDays || 2,
|
|
253
|
+
log: options.log || console.log,
|
|
254
|
+
error: options.error || console.error,
|
|
255
|
+
exit: options.exit || (code => process.exit(code)),
|
|
256
|
+
colors: options.colors || { RED: '', GREEN: '', YELLOW: '', BLUE: '', CYAN: '', NC: '' }
|
|
257
|
+
};
|
|
258
|
+
const { RED, GREEN, YELLOW, BLUE, CYAN, NC } = ctx.colors;
|
|
259
|
+
|
|
260
|
+
const versionTag = ctx.imageVersionTag || ctx.imageVersionDefault || `${ctx.imageVersionBase}-common`;
|
|
261
|
+
const parsedVersion = ctx.parseImageVersionTag(versionTag);
|
|
262
|
+
if (!parsedVersion) {
|
|
263
|
+
ctx.error(`${RED}错误: 镜像版本格式错误,必须为 <x.y.z-后缀>,例如 1.7.4-common: ${versionTag}${NC}`);
|
|
264
|
+
ctx.exit(1);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const version = parsedVersion.baseVersion;
|
|
269
|
+
let imageTool = parsedVersion.tool;
|
|
270
|
+
const imageBuildArgs = [...ctx.imageBuildArgs];
|
|
271
|
+
const toolFromArgs = resolveToolFromBuildArgs(imageBuildArgs);
|
|
272
|
+
if (!toolFromArgs) {
|
|
273
|
+
imageBuildArgs.push('--build-arg', `TOOL=${imageTool}`);
|
|
274
|
+
} else {
|
|
275
|
+
imageTool = toolFromArgs;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const fullImageTag = `${ctx.imageName}:${version}-${imageTool}`;
|
|
279
|
+
ctx.log(`${CYAN}🔨 正在构建镜像: ${YELLOW}${fullImageTag}${NC}`);
|
|
280
|
+
ctx.log(`${BLUE}构建组件类型: ${imageTool}${NC}\n`);
|
|
281
|
+
|
|
282
|
+
await prepareBuildCache(ctx, imageTool);
|
|
283
|
+
|
|
284
|
+
const dockerfilePath = path.join(ctx.rootDir, 'docker', 'manyoyo.Dockerfile');
|
|
285
|
+
if (!fs.existsSync(dockerfilePath)) {
|
|
286
|
+
ctx.error(`${RED}错误: 找不到 Dockerfile: ${dockerfilePath}${NC}`);
|
|
287
|
+
ctx.exit(1);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const buildArgs = [
|
|
292
|
+
'build', '-t', fullImageTag,
|
|
293
|
+
'-f', dockerfilePath,
|
|
294
|
+
ctx.rootDir,
|
|
295
|
+
...imageBuildArgs,
|
|
296
|
+
'--load',
|
|
297
|
+
'--progress=plain',
|
|
298
|
+
'--no-cache'
|
|
299
|
+
];
|
|
300
|
+
|
|
301
|
+
ctx.log(`${BLUE}准备执行命令:${NC}`);
|
|
302
|
+
ctx.log(`${ctx.dockerCmd} ${buildArgs.map(quoteShellArg).join(' ')}\n`);
|
|
303
|
+
|
|
304
|
+
if (!ctx.yesMode) {
|
|
305
|
+
await ctx.askQuestion('❔ 是否继续构建? [ 直接回车=继续, ctrl+c=取消 ]: ');
|
|
306
|
+
ctx.log('');
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
try {
|
|
310
|
+
ctx.runCmd(ctx.dockerCmd, buildArgs, { stdio: 'inherit' });
|
|
311
|
+
ctx.log(`\n${GREEN}✅ 镜像构建成功: ${fullImageTag}${NC}`);
|
|
312
|
+
ctx.log(`${BLUE}使用镜像:${NC}`);
|
|
313
|
+
ctx.log(` ${ctx.manyoyoName} -n test --in ${ctx.imageName} --iv ${version}-${imageTool} -y c`);
|
|
314
|
+
ctx.pruneDanglingImages();
|
|
315
|
+
} catch (e) {
|
|
316
|
+
ctx.error(`${RED}错误: 镜像构建失败${NC}`);
|
|
317
|
+
ctx.exit(1);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
module.exports = {
|
|
322
|
+
buildImage
|
|
323
|
+
};
|