koishi-plugin-docker-control 0.0.4 → 0.0.5
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/lib/commands/compose.d.ts +11 -0
- package/lib/commands/compose.js +138 -0
- package/lib/commands/index.js +2 -0
- package/lib/index.d.ts +3 -0
- package/lib/index.js +1 -1
- package/lib/service/connector.d.ts +19 -0
- package/lib/service/connector.js +59 -2
- package/lib/service/node.d.ts +15 -1
- package/lib/service/node.js +83 -3
- package/lib/types.d.ts +24 -0
- package/lib/utils/render.d.ts +4 -0
- package/lib/utils/render.js +78 -0
- package/package.json +1 -1
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Docker Compose 命令 - 展示和发送 docker compose 配置
|
|
3
|
+
*/
|
|
4
|
+
import { Context } from 'koishi';
|
|
5
|
+
import type { DockerControlConfig } from '../types';
|
|
6
|
+
type GetService = () => any;
|
|
7
|
+
/**
|
|
8
|
+
* 注册 docker.compose 命令
|
|
9
|
+
*/
|
|
10
|
+
export declare function registerComposeCommand(ctx: Context, getService: GetService, config?: DockerControlConfig): void;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerComposeCommand = registerComposeCommand;
|
|
4
|
+
/**
|
|
5
|
+
* Docker Compose 命令 - 展示和发送 docker compose 配置
|
|
6
|
+
*/
|
|
7
|
+
const koishi_1 = require("koishi");
|
|
8
|
+
const render_1 = require("../utils/render");
|
|
9
|
+
const logger_1 = require("../utils/logger");
|
|
10
|
+
/**
|
|
11
|
+
* 注册 docker.compose 命令
|
|
12
|
+
*/
|
|
13
|
+
function registerComposeCommand(ctx, getService, config) {
|
|
14
|
+
const useImageOutput = config?.imageOutput === true;
|
|
15
|
+
ctx
|
|
16
|
+
.command('docker.compose <node> <container>', '展示 Docker Compose 配置')
|
|
17
|
+
.alias('dockercompose', 'compose', 'docker-compose')
|
|
18
|
+
.option('download', '-d 直接发送 compose 文件(base64编码)')
|
|
19
|
+
.option('image', '-i 强制使用图片展示')
|
|
20
|
+
.option('text', '-t 强制使用文字展示')
|
|
21
|
+
.action(async ({ options, session }, nodeSelector, container) => {
|
|
22
|
+
const service = getService();
|
|
23
|
+
if (!service) {
|
|
24
|
+
return 'Docker 服务未初始化';
|
|
25
|
+
}
|
|
26
|
+
if (!nodeSelector || !container) {
|
|
27
|
+
return '请指定节点和容器: docker.compose <节点> <容器>';
|
|
28
|
+
}
|
|
29
|
+
// 获取节点
|
|
30
|
+
const nodes = service.getNodesBySelector(nodeSelector);
|
|
31
|
+
if (nodes.length === 0) {
|
|
32
|
+
return `未找到节点: ${nodeSelector}`;
|
|
33
|
+
}
|
|
34
|
+
const node = nodes[0];
|
|
35
|
+
if (node.status !== 'connected') {
|
|
36
|
+
return `节点 ${node.name} 未连接`;
|
|
37
|
+
}
|
|
38
|
+
// 查找容器
|
|
39
|
+
let containers;
|
|
40
|
+
try {
|
|
41
|
+
containers = await node.listContainers(true);
|
|
42
|
+
}
|
|
43
|
+
catch (e) {
|
|
44
|
+
return `获取容器列表失败: ${e.message}`;
|
|
45
|
+
}
|
|
46
|
+
// 支持容器名称或 ID 前缀匹配
|
|
47
|
+
const targetContainer = containers.find(c => c.Names[0]?.replace('/', '') === container ||
|
|
48
|
+
c.Id.startsWith(container));
|
|
49
|
+
if (!targetContainer) {
|
|
50
|
+
return `未找到容器: ${container}`;
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
// 获取 compose 文件信息
|
|
54
|
+
const composeInfo = await node.getComposeFileInfo(targetContainer.Id);
|
|
55
|
+
if (!composeInfo) {
|
|
56
|
+
return `容器 ${container} 不是由 Docker Compose 启动,或无法找到 compose 文件`;
|
|
57
|
+
}
|
|
58
|
+
// 如果指定了 -d 参数,作为文件发送
|
|
59
|
+
if (options.download) {
|
|
60
|
+
try {
|
|
61
|
+
const filename = `${composeInfo.projectName}-docker-compose.yaml`;
|
|
62
|
+
const buffer = Buffer.from(composeInfo.content, 'utf-8');
|
|
63
|
+
// 1. 构建 Data URI 字符串
|
|
64
|
+
// 使用 application/octet-stream 以避免适配器自作聪明改文件名
|
|
65
|
+
const dataUri = 'data:application/octet-stream;base64,' + buffer.toString('base64');
|
|
66
|
+
// === 方案 A: 优先尝试使用 assets 服务上传 ===
|
|
67
|
+
if (ctx.assets) {
|
|
68
|
+
try {
|
|
69
|
+
logger_1.connectorLogger.debug(`[compose] 正在通过 assets 上传文件: ${filename}`);
|
|
70
|
+
// 传入 Data URI 字符串
|
|
71
|
+
const url = await ctx.assets.upload(dataUri, filename);
|
|
72
|
+
logger_1.connectorLogger.info(`[compose] Assets 上传成功: ${url}`);
|
|
73
|
+
// 直接发送 URL,文件名由 assets 插件的 URL 决定
|
|
74
|
+
return koishi_1.h.file(url);
|
|
75
|
+
}
|
|
76
|
+
catch (e) {
|
|
77
|
+
logger_1.connectorLogger.warn(`[compose] Assets 上传失败,尝试降级: ${e.message}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// === 方案 B: 降级方案 ===
|
|
81
|
+
// 失败后直接发送 Data URI
|
|
82
|
+
logger_1.connectorLogger.debug(`[compose] 发送 DataURI (降级): filename=${filename}`);
|
|
83
|
+
return koishi_1.h.file(dataUri, {
|
|
84
|
+
filename: filename,
|
|
85
|
+
name: filename,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
catch (e) {
|
|
89
|
+
logger_1.connectorLogger.error(`[compose] 生成文件失败: ${e.message}`);
|
|
90
|
+
return `生成文件失败: ${e.message}`;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// 判断使用图片还是文字展示
|
|
94
|
+
// -i 强制图片,-t 强制文字,都没指定则根据配置
|
|
95
|
+
const forceImage = options.image;
|
|
96
|
+
const forceText = options.text;
|
|
97
|
+
const shouldUseImage = forceImage || (!forceText && useImageOutput && ctx.puppeteer);
|
|
98
|
+
if (shouldUseImage) {
|
|
99
|
+
try {
|
|
100
|
+
const html = (0, render_1.generateComposeHtml)(node.name, container, composeInfo.projectName, composeInfo.originalPath, composeInfo.serviceCount, composeInfo.content);
|
|
101
|
+
return await (0, render_1.renderToImage)(ctx, html);
|
|
102
|
+
}
|
|
103
|
+
catch (e) {
|
|
104
|
+
// 如果图片渲染失败,回退到文字
|
|
105
|
+
if (e.message?.includes('puppeteer')) {
|
|
106
|
+
return [
|
|
107
|
+
`=== Docker Compose: ${composeInfo.projectName} ===`,
|
|
108
|
+
`节点: ${node.name}`,
|
|
109
|
+
`容器: ${container}`,
|
|
110
|
+
`文件路径: ${composeInfo.originalPath}`,
|
|
111
|
+
`服务数量: ${composeInfo.serviceCount}`,
|
|
112
|
+
'',
|
|
113
|
+
'--- compose.yaml ---',
|
|
114
|
+
'',
|
|
115
|
+
composeInfo.content,
|
|
116
|
+
].join('\n');
|
|
117
|
+
}
|
|
118
|
+
throw e;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// 文字展示
|
|
122
|
+
return [
|
|
123
|
+
`=== Docker Compose: ${composeInfo.projectName} ===`,
|
|
124
|
+
`节点: ${node.name}`,
|
|
125
|
+
`容器: ${container}`,
|
|
126
|
+
`文件路径: ${composeInfo.originalPath}`,
|
|
127
|
+
`服务数量: ${composeInfo.serviceCount}`,
|
|
128
|
+
'',
|
|
129
|
+
'--- compose.yaml ---',
|
|
130
|
+
'',
|
|
131
|
+
composeInfo.content,
|
|
132
|
+
].join('\n');
|
|
133
|
+
}
|
|
134
|
+
catch (e) {
|
|
135
|
+
return `获取 compose 配置失败: ${e.message}`;
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
}
|
package/lib/commands/index.js
CHANGED
|
@@ -4,6 +4,7 @@ exports.registerCommands = registerCommands;
|
|
|
4
4
|
const list_1 = require("./list");
|
|
5
5
|
const control_1 = require("./control");
|
|
6
6
|
const logs_1 = require("./logs");
|
|
7
|
+
const compose_1 = require("./compose");
|
|
7
8
|
const render_1 = require("../utils/render");
|
|
8
9
|
/**
|
|
9
10
|
* 注册所有指令
|
|
@@ -13,6 +14,7 @@ function registerCommands(ctx, getService, config) {
|
|
|
13
14
|
(0, list_1.registerListCommand)(ctx, getService, config);
|
|
14
15
|
(0, control_1.registerControlCommands)(ctx, getService, config);
|
|
15
16
|
(0, logs_1.registerLogsCommand)(ctx, getService, config);
|
|
17
|
+
(0, compose_1.registerComposeCommand)(ctx, getService, config);
|
|
16
18
|
// 注册辅助指令
|
|
17
19
|
registerHelperCommands(ctx, getService, config);
|
|
18
20
|
}
|
package/lib/index.d.ts
CHANGED
|
@@ -23,6 +23,9 @@ declare module 'koishi' {
|
|
|
23
23
|
puppeteer?: {
|
|
24
24
|
render: (html: string, callback?: (page: any, next: (handle?: any) => Promise<string>) => Promise<string>) => Promise<string>;
|
|
25
25
|
};
|
|
26
|
+
assets?: {
|
|
27
|
+
upload: (data: string | Buffer, filename: string) => Promise<string>;
|
|
28
|
+
};
|
|
26
29
|
}
|
|
27
30
|
interface Tables {
|
|
28
31
|
'docker_control_subscriptions': DockerControlSubscription;
|
package/lib/index.js
CHANGED
|
@@ -13,7 +13,7 @@ const commands_1 = require("./commands");
|
|
|
13
13
|
exports.name = 'docker-control';
|
|
14
14
|
exports.inject = {
|
|
15
15
|
required: ['database'],
|
|
16
|
-
optional: ['puppeteer'],
|
|
16
|
+
optional: ['puppeteer', 'assets'],
|
|
17
17
|
};
|
|
18
18
|
// 导出配置 Schema
|
|
19
19
|
exports.Config = koishi_1.Schema.object({
|
|
@@ -74,4 +74,23 @@ export declare class DockerConnector {
|
|
|
74
74
|
* 获取凭证配置
|
|
75
75
|
*/
|
|
76
76
|
private getCredential;
|
|
77
|
+
/**
|
|
78
|
+
* 读取文件内容 (支持 Windows 路径和 WSL 路径自动转换)
|
|
79
|
+
* @param filePath 文件路径 (可能是 Windows 路径如 C:\xxx 或 WSL 路径如 /mnt/c/xxx)
|
|
80
|
+
* @returns 文件内容
|
|
81
|
+
*/
|
|
82
|
+
readFile(filePath: string): Promise<string>;
|
|
83
|
+
/**
|
|
84
|
+
* 内部文件读取方法
|
|
85
|
+
*/
|
|
86
|
+
private readFileInternal;
|
|
87
|
+
/**
|
|
88
|
+
* 将 Windows 路径转换为 WSL 路径
|
|
89
|
+
* 例如: C:\Users\anyul\anyulapp\RSSHub\docker-compose.yml -> /mnt/c/Users/anyul/anyulapp/RSSHub/docker-compose.yml
|
|
90
|
+
*/
|
|
91
|
+
convertWindowsToWslPath(windowsPath: string): string;
|
|
92
|
+
/**
|
|
93
|
+
* 检查文件是否存在
|
|
94
|
+
*/
|
|
95
|
+
fileExists(filePath: string): Promise<boolean>;
|
|
77
96
|
}
|
package/lib/service/connector.js
CHANGED
|
@@ -61,8 +61,10 @@ class DockerConnector {
|
|
|
61
61
|
catch (e) {
|
|
62
62
|
// 可能已经关闭,忽略错误
|
|
63
63
|
}
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
// 非零退出码或包含错误信息时抛出异常
|
|
65
|
+
if (code !== 0 || stderr.includes('Error') || stderr.includes('error') || stderr.includes('No such file')) {
|
|
66
|
+
const errorMsg = stderr.trim() || `命令执行失败,退出码: ${code}`;
|
|
67
|
+
reject(new Error(errorMsg));
|
|
66
68
|
}
|
|
67
69
|
else {
|
|
68
70
|
resolve(stdout.trim());
|
|
@@ -300,5 +302,60 @@ class DockerConnector {
|
|
|
300
302
|
getCredential() {
|
|
301
303
|
return (0, config_1.getCredentialById)(this.fullConfig, this.config.credentialId);
|
|
302
304
|
}
|
|
305
|
+
/**
|
|
306
|
+
* 读取文件内容 (支持 Windows 路径和 WSL 路径自动转换)
|
|
307
|
+
* @param filePath 文件路径 (可能是 Windows 路径如 C:\xxx 或 WSL 路径如 /mnt/c/xxx)
|
|
308
|
+
* @returns 文件内容
|
|
309
|
+
*/
|
|
310
|
+
async readFile(filePath) {
|
|
311
|
+
// 检测是否是 Windows 路径 (包含盘符如 C:\)
|
|
312
|
+
const isWindowsPath = /^[A-Za-z]:/.test(filePath);
|
|
313
|
+
if (isWindowsPath) {
|
|
314
|
+
// 第一次尝试使用原始路径
|
|
315
|
+
try {
|
|
316
|
+
return await this.readFileInternal(filePath);
|
|
317
|
+
}
|
|
318
|
+
catch (originalError) {
|
|
319
|
+
// 如果失败,尝试转换为 WSL 路径
|
|
320
|
+
const wslPath = this.convertWindowsToWslPath(filePath);
|
|
321
|
+
logger_1.connectorLogger.debug(`[${this.config.name}] 原始路径失败 (${filePath}),尝试 WSL 路径: ${wslPath}`);
|
|
322
|
+
return await this.readFileInternal(wslPath);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
// 非 Windows 路径直接读取
|
|
326
|
+
return await this.readFileInternal(filePath);
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* 内部文件读取方法
|
|
330
|
+
*/
|
|
331
|
+
async readFileInternal(filePath) {
|
|
332
|
+
// 使用 cat 命令读取文件
|
|
333
|
+
// 对路径进行引号处理以支持包含空格的路径
|
|
334
|
+
const escapedPath = filePath.replace(/"/g, '\\"');
|
|
335
|
+
return this.exec(`cat "${escapedPath}"`);
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* 将 Windows 路径转换为 WSL 路径
|
|
339
|
+
* 例如: C:\Users\anyul\anyulapp\RSSHub\docker-compose.yml -> /mnt/c/Users/anyul/anyulapp/RSSHub/docker-compose.yml
|
|
340
|
+
*/
|
|
341
|
+
convertWindowsToWslPath(windowsPath) {
|
|
342
|
+
// 匹配 Windows 盘符路径 (如 C:\xxx 或 C:/xxx)
|
|
343
|
+
const match = windowsPath.match(/^([A-Za-z]):[\\/](.*)$/);
|
|
344
|
+
if (!match) {
|
|
345
|
+
// 如果不是有效的 Windows 路径,返回原路径
|
|
346
|
+
return windowsPath;
|
|
347
|
+
}
|
|
348
|
+
const driveLetter = match[1].toLowerCase();
|
|
349
|
+
const restPath = match[2].replace(/\\/g, '/');
|
|
350
|
+
return `/mnt/${driveLetter}/${restPath}`;
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* 检查文件是否存在
|
|
354
|
+
*/
|
|
355
|
+
async fileExists(filePath) {
|
|
356
|
+
const escapedPath = filePath.replace(/"/g, '\\"');
|
|
357
|
+
const result = await this.execWithExitCode(`test -f "${escapedPath}" && echo "exists" || echo "not exists"`);
|
|
358
|
+
return result.output.trim() === 'exists';
|
|
359
|
+
}
|
|
303
360
|
}
|
|
304
361
|
exports.DockerConnector = DockerConnector;
|
package/lib/service/node.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { NodeConfig, ContainerInfo, DockerEvent, NodeStatusType, CredentialConfig } from '../types';
|
|
1
|
+
import type { NodeConfig, ContainerInfo, DockerEvent, NodeStatusType, CredentialConfig, ComposeFileInfo, ContainerComposeInfo } from '../types';
|
|
2
2
|
export declare class DockerNode {
|
|
3
3
|
/** 节点配置 */
|
|
4
4
|
readonly config: NodeConfig;
|
|
@@ -94,6 +94,20 @@ export declare class DockerNode {
|
|
|
94
94
|
* 获取镜像数量
|
|
95
95
|
*/
|
|
96
96
|
getImageCount(): Promise<number>;
|
|
97
|
+
/**
|
|
98
|
+
* 获取容器的 Docker Compose 信息
|
|
99
|
+
* 通过标签 com.docker.compose.project.config_files 获取 compose 文件路径
|
|
100
|
+
*/
|
|
101
|
+
getContainerComposeInfo(containerId: string): Promise<ContainerComposeInfo | null>;
|
|
102
|
+
/**
|
|
103
|
+
* 获取容器的 Docker Compose 文件信息
|
|
104
|
+
* 读取并解析 compose 文件
|
|
105
|
+
*/
|
|
106
|
+
getComposeFileInfo(containerId: string): Promise<ComposeFileInfo | null>;
|
|
107
|
+
/**
|
|
108
|
+
* 统计 compose 文件中的服务数量
|
|
109
|
+
*/
|
|
110
|
+
private countServices;
|
|
97
111
|
/**
|
|
98
112
|
* 获取容器详细信息 (docker inspect)
|
|
99
113
|
*/
|
package/lib/service/node.js
CHANGED
|
@@ -185,9 +185,15 @@ class DockerNode {
|
|
|
185
185
|
if (!this.connector)
|
|
186
186
|
return null;
|
|
187
187
|
try {
|
|
188
|
-
//
|
|
189
|
-
const
|
|
190
|
-
|
|
188
|
+
// 使用 execWithExitCode 避免非零退出码抛出异常
|
|
189
|
+
const result = await this.connector.execWithExitCode('docker info --format "{{.NCPU}} {{.MemTotal}} {{.MemAvailable}}"');
|
|
190
|
+
logger_1.nodeLogger.debug(`[${this.name}] docker info 输出: "${result.output}", 退出码: ${result.exitCode}`);
|
|
191
|
+
// docker info 可能返回退出码 1 但仍有输出(权限问题),只要有输出就解析
|
|
192
|
+
if (!result.output.trim()) {
|
|
193
|
+
logger_1.nodeLogger.warn(`[${this.name}] docker info 输出为空`);
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
const parts = result.output.trim().split(/\s+/);
|
|
191
197
|
if (parts.length >= 2) {
|
|
192
198
|
return {
|
|
193
199
|
NCPU: parseInt(parts[0]) || 0,
|
|
@@ -198,6 +204,7 @@ class DockerNode {
|
|
|
198
204
|
return null;
|
|
199
205
|
}
|
|
200
206
|
catch (e) {
|
|
207
|
+
logger_1.nodeLogger.warn(`[${this.name}] 获取系统信息异常: ${e}`);
|
|
201
208
|
return null;
|
|
202
209
|
}
|
|
203
210
|
}
|
|
@@ -233,6 +240,79 @@ class DockerNode {
|
|
|
233
240
|
return 0;
|
|
234
241
|
}
|
|
235
242
|
}
|
|
243
|
+
/**
|
|
244
|
+
* 获取容器的 Docker Compose 信息
|
|
245
|
+
* 通过标签 com.docker.compose.project.config_files 获取 compose 文件路径
|
|
246
|
+
*/
|
|
247
|
+
async getContainerComposeInfo(containerId) {
|
|
248
|
+
if (!this.connector)
|
|
249
|
+
throw new Error('未连接');
|
|
250
|
+
try {
|
|
251
|
+
// 使用 docker inspect 获取容器标签
|
|
252
|
+
const output = await this.connector.exec(`docker inspect ${containerId} --format "{{json .Config.Labels}}"`);
|
|
253
|
+
if (!output.trim()) {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
const labels = JSON.parse(output);
|
|
257
|
+
// 获取 compose 项目名称和配置文件路径
|
|
258
|
+
const projectName = labels['com.docker.compose.project'] || '';
|
|
259
|
+
const configFiles = labels['com.docker.compose.project.config_files'] || '';
|
|
260
|
+
if (!projectName || !configFiles) {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
return {
|
|
264
|
+
containerId,
|
|
265
|
+
containerName: labels['com.docker.compose.container-number'] || '',
|
|
266
|
+
projectName,
|
|
267
|
+
composeFilePath: configFiles,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
catch (e) {
|
|
271
|
+
logger_1.nodeLogger.warn(`[${this.name}] 获取容器 ${containerId} 的 compose 信息失败: ${e}`);
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* 获取容器的 Docker Compose 文件信息
|
|
277
|
+
* 读取并解析 compose 文件
|
|
278
|
+
*/
|
|
279
|
+
async getComposeFileInfo(containerId) {
|
|
280
|
+
if (!this.connector)
|
|
281
|
+
throw new Error('未连接');
|
|
282
|
+
try {
|
|
283
|
+
const composeInfo = await this.getContainerComposeInfo(containerId);
|
|
284
|
+
if (!composeInfo) {
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
const filePath = composeInfo.composeFilePath;
|
|
288
|
+
const originalPath = filePath;
|
|
289
|
+
// 尝试读取文件 (支持 Windows 路径自动转换)
|
|
290
|
+
const content = await this.connector.readFile(filePath);
|
|
291
|
+
// 统计服务数量 (简单的 yaml 解析)
|
|
292
|
+
const serviceCount = this.countServices(content);
|
|
293
|
+
return {
|
|
294
|
+
originalPath,
|
|
295
|
+
effectivePath: filePath,
|
|
296
|
+
usedWslPath: false, // 实际是否使用了 WSL 路径在内部处理
|
|
297
|
+
content,
|
|
298
|
+
projectName: composeInfo.projectName,
|
|
299
|
+
serviceCount,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
catch (e) {
|
|
303
|
+
logger_1.nodeLogger.warn(`[${this.name}] 获取 compose 文件信息失败: ${e.message}`);
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* 统计 compose 文件中的服务数量
|
|
309
|
+
*/
|
|
310
|
+
countServices(content) {
|
|
311
|
+
// 简单的正则匹配 services: 下面的服务名
|
|
312
|
+
const servicePattern = /^[a-zA-Z0-9_-]+:\s*$/gm;
|
|
313
|
+
const matches = content.match(servicePattern);
|
|
314
|
+
return matches ? matches.length : 0;
|
|
315
|
+
}
|
|
236
316
|
/**
|
|
237
317
|
* 获取容器详细信息 (docker inspect)
|
|
238
318
|
*/
|
package/lib/types.d.ts
CHANGED
|
@@ -98,3 +98,27 @@ export interface SubscriptionConfig {
|
|
|
98
98
|
/** 创建时间 */
|
|
99
99
|
createdAt?: number;
|
|
100
100
|
}
|
|
101
|
+
export interface ComposeFileInfo {
|
|
102
|
+
/** 原始路径 (Windows 路径或 WSL 路径) */
|
|
103
|
+
originalPath: string;
|
|
104
|
+
/** 实际使用的路径 (尝试转换后的路径) */
|
|
105
|
+
effectivePath: string;
|
|
106
|
+
/** 是否使用了 WSL 路径转换 */
|
|
107
|
+
usedWslPath: boolean;
|
|
108
|
+
/** Compose 文件内容 */
|
|
109
|
+
content: string;
|
|
110
|
+
/** 所属项目名称 */
|
|
111
|
+
projectName: string;
|
|
112
|
+
/** 容器数量 */
|
|
113
|
+
serviceCount: number;
|
|
114
|
+
}
|
|
115
|
+
export interface ContainerComposeInfo {
|
|
116
|
+
/** 容器 ID */
|
|
117
|
+
containerId: string;
|
|
118
|
+
/** 容器名称 */
|
|
119
|
+
containerName: string;
|
|
120
|
+
/** 所属 Docker Compose 项目 */
|
|
121
|
+
projectName: string;
|
|
122
|
+
/** Compose 文件路径 */
|
|
123
|
+
composeFilePath: string;
|
|
124
|
+
}
|
package/lib/utils/render.d.ts
CHANGED
|
@@ -45,4 +45,8 @@ export declare function generateLogsHtml(nodeName: string, containerName: string
|
|
|
45
45
|
* 生成执行结果 HTML
|
|
46
46
|
*/
|
|
47
47
|
export declare function generateExecHtml(nodeName: string, containerName: string, command: string, output: string, exitCode: number): string;
|
|
48
|
+
/**
|
|
49
|
+
* 生成 Docker Compose 配置 HTML
|
|
50
|
+
*/
|
|
51
|
+
export declare function generateComposeHtml(nodeName: string, containerName: string, projectName: string, filePath: string, serviceCount: number, composeContent: string): string;
|
|
48
52
|
export {};
|
package/lib/utils/render.js
CHANGED
|
@@ -8,6 +8,7 @@ exports.generateNodesHtml = generateNodesHtml;
|
|
|
8
8
|
exports.generateNodeDetailHtml = generateNodeDetailHtml;
|
|
9
9
|
exports.generateLogsHtml = generateLogsHtml;
|
|
10
10
|
exports.generateExecHtml = generateExecHtml;
|
|
11
|
+
exports.generateComposeHtml = generateComposeHtml;
|
|
11
12
|
const koishi_1 = require("koishi");
|
|
12
13
|
// 基础样式
|
|
13
14
|
const STYLE = `
|
|
@@ -680,3 +681,80 @@ function generateExecHtml(nodeName, containerName, command, output, exitCode) {
|
|
|
680
681
|
`;
|
|
681
682
|
return wrapHtml(header + body);
|
|
682
683
|
}
|
|
684
|
+
/**
|
|
685
|
+
* 生成 Docker Compose 配置 HTML
|
|
686
|
+
*/
|
|
687
|
+
function generateComposeHtml(nodeName, containerName, projectName, filePath, serviceCount, composeContent) {
|
|
688
|
+
// 对内容进行语法高亮
|
|
689
|
+
const highlightedContent = highlightYaml(composeContent);
|
|
690
|
+
const header = `
|
|
691
|
+
<div class="header">
|
|
692
|
+
<div class="header-title">Docker Compose</div>
|
|
693
|
+
<div class="header-badge">${nodeName}/${containerName}</div>
|
|
694
|
+
</div>
|
|
695
|
+
`;
|
|
696
|
+
const body = `
|
|
697
|
+
<div class="content">
|
|
698
|
+
<div class="detail-card" style="margin-bottom: 20px;">
|
|
699
|
+
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px;">
|
|
700
|
+
<div class="detail-item">
|
|
701
|
+
<div class="detail-label">项目名称</div>
|
|
702
|
+
<div class="detail-value highlight">${projectName}</div>
|
|
703
|
+
</div>
|
|
704
|
+
<div class="detail-item">
|
|
705
|
+
<div class="detail-label">服务数量</div>
|
|
706
|
+
<div class="detail-value">${serviceCount} 个</div>
|
|
707
|
+
</div>
|
|
708
|
+
<div class="detail-item" style="grid-column: 1 / -1;">
|
|
709
|
+
<div class="detail-label">文件路径</div>
|
|
710
|
+
<div class="detail-value" style="font-size: 13px;">${filePath}</div>
|
|
711
|
+
</div>
|
|
712
|
+
</div>
|
|
713
|
+
</div>
|
|
714
|
+
|
|
715
|
+
<div style="
|
|
716
|
+
background: rgba(0, 0, 0, 0.3);
|
|
717
|
+
border-radius: 8px;
|
|
718
|
+
padding: 16px;
|
|
719
|
+
font-family: 'SF Mono', Monaco, 'Courier New', monospace;
|
|
720
|
+
font-size: 12px;
|
|
721
|
+
line-height: 1.6;
|
|
722
|
+
white-space: pre-wrap;
|
|
723
|
+
word-break: break-all;
|
|
724
|
+
">${highlightedContent}</div>
|
|
725
|
+
</div>
|
|
726
|
+
`;
|
|
727
|
+
// 添加 YAML 高亮样式
|
|
728
|
+
const yamlStyle = `
|
|
729
|
+
.yaml-key { color: #60a5fa; }
|
|
730
|
+
.yaml-string { color: #a5f3fc; }
|
|
731
|
+
.yaml-number { color: #f472b6; }
|
|
732
|
+
.yaml-boolean { color: #fbbf24; }
|
|
733
|
+
.yaml-null { color: #94a3b8; }
|
|
734
|
+
.yaml-comment { color: #64748b; font-style: italic; }
|
|
735
|
+
.yaml-bracket { color: #f87171; }
|
|
736
|
+
`;
|
|
737
|
+
return wrapHtml(header + body, STYLE + yamlStyle);
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* 简单的 YAML 语法高亮
|
|
741
|
+
*/
|
|
742
|
+
function highlightYaml(content) {
|
|
743
|
+
// HTML 转义
|
|
744
|
+
let html = escapeHtml(content);
|
|
745
|
+
// 高亮键名 (冒号前的单词)
|
|
746
|
+
html = html.replace(/^([a-zA-Z0-9_-]+):(\s*)$/gm, '<span class="yaml-key">$1</span>:<br>');
|
|
747
|
+
// 高亮带引号的字符串
|
|
748
|
+
html = html.replace(/("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')/g, '<span class="yaml-string">$1</span>');
|
|
749
|
+
// 高亮数字
|
|
750
|
+
html = html.replace(/\b(\d+\.?\d*)\b/g, '<span class="yaml-number">$1</span>');
|
|
751
|
+
// 高亮布尔值
|
|
752
|
+
html = html.replace(/\b(true|false|yes|no|on|off)\b/gi, '<span class="yaml-boolean">$1</span>');
|
|
753
|
+
// 高亮 null
|
|
754
|
+
html = html.replace(/\bnull\b/gi, '<span class="yaml-null">null</span>');
|
|
755
|
+
// 高亮注释
|
|
756
|
+
html = html.replace(/#.*$/gm, '<span class="yaml-comment">$&</span>');
|
|
757
|
+
// 高亮括号
|
|
758
|
+
html = html.replace(/([\[\]{}()])/g, '<span class="yaml-bracket">$1</span>');
|
|
759
|
+
return html;
|
|
760
|
+
}
|