@hangox/mg-cli 1.0.0 → 1.0.2
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/dist/cli.js +1580 -0
- package/dist/cli.js.map +1 -0
- package/dist/daemon-runner.js +794 -0
- package/dist/daemon-runner.js.map +1 -0
- package/dist/index-DNrszrq9.d.ts +568 -0
- package/dist/index.d.ts +129 -0
- package/dist/index.js +950 -0
- package/dist/index.js.map +1 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +689 -0
- package/dist/server.js.map +1 -0
- package/package.json +5 -1
- package/.eslintrc.cjs +0 -26
- package/CLAUDE.md +0 -43
- package/src/cli/client.ts +0 -266
- package/src/cli/commands/execute-code.ts +0 -59
- package/src/cli/commands/export-image.ts +0 -193
- package/src/cli/commands/get-all-nodes.ts +0 -81
- package/src/cli/commands/get-all-pages.ts +0 -118
- package/src/cli/commands/get-node-by-id.ts +0 -83
- package/src/cli/commands/get-node-by-link.ts +0 -105
- package/src/cli/commands/server.ts +0 -130
- package/src/cli/index.ts +0 -33
- package/src/index.ts +0 -9
- package/src/server/connection-manager.ts +0 -211
- package/src/server/daemon-runner.ts +0 -22
- package/src/server/daemon.ts +0 -211
- package/src/server/index.ts +0 -8
- package/src/server/logger.ts +0 -117
- package/src/server/request-handler.ts +0 -192
- package/src/server/websocket-server.ts +0 -297
- package/src/shared/constants.ts +0 -90
- package/src/shared/errors.ts +0 -131
- package/src/shared/index.ts +0 -8
- package/src/shared/types.ts +0 -227
- package/src/shared/utils.ts +0 -352
- package/tests/unit/shared/constants.test.ts +0 -66
- package/tests/unit/shared/errors.test.ts +0 -82
- package/tests/unit/shared/utils.test.ts +0 -208
- package/tsconfig.json +0 -22
- package/tsup.config.ts +0 -33
- package/vitest.config.ts +0 -22
package/src/shared/errors.ts
DELETED
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MG Plugin 错误码定义
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
/** 错误码枚举 */
|
|
6
|
-
export enum ErrorCode {
|
|
7
|
-
/** 无法连接到 MG Server */
|
|
8
|
-
CONNECTION_FAILED = 'E001',
|
|
9
|
-
/** 连接超时 */
|
|
10
|
-
CONNECTION_TIMEOUT = 'E002',
|
|
11
|
-
/** 没有 MasterGo 页面连接到 Server */
|
|
12
|
-
NO_PAGE_CONNECTED = 'E003',
|
|
13
|
-
/** 未找到匹配的页面 */
|
|
14
|
-
PAGE_NOT_FOUND = 'E004',
|
|
15
|
-
/** 节点不存在 */
|
|
16
|
-
NODE_NOT_FOUND = 'E005',
|
|
17
|
-
/** 没有选中任何节点 */
|
|
18
|
-
NO_SELECTION = 'E006',
|
|
19
|
-
/** mg 对象不可用 */
|
|
20
|
-
MG_UNAVAILABLE = 'E007',
|
|
21
|
-
/** 导出图片失败 */
|
|
22
|
-
EXPORT_FAILED = 'E008',
|
|
23
|
-
/** 文件写入失败 */
|
|
24
|
-
FILE_WRITE_FAILED = 'E009',
|
|
25
|
-
/** 无效的 mgp:// 链接格式 */
|
|
26
|
-
INVALID_LINK = 'E010',
|
|
27
|
-
/** 参数校验失败 */
|
|
28
|
-
INVALID_PARAMS = 'E011',
|
|
29
|
-
/** 请求超时 */
|
|
30
|
-
REQUEST_TIMEOUT = 'E012',
|
|
31
|
-
/** 所有备选端口均被占用 */
|
|
32
|
-
PORT_EXHAUSTED = 'E013',
|
|
33
|
-
/** 无法发现 Server (端口扫描失败) */
|
|
34
|
-
SERVER_DISCOVERY_FAILED = 'E014',
|
|
35
|
-
/** 自动启动 Server 失败 */
|
|
36
|
-
SERVER_START_FAILED = 'E015',
|
|
37
|
-
/** Server 已在运行中 */
|
|
38
|
-
SERVER_ALREADY_RUNNING = 'E016',
|
|
39
|
-
/** 连接断开 */
|
|
40
|
-
CONNECTION_LOST = 'E017',
|
|
41
|
-
/** 未知错误 */
|
|
42
|
-
UNKNOWN_ERROR = 'E099',
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** 错误码对应的错误名称 */
|
|
46
|
-
export const ErrorNames: Record<ErrorCode, string> = {
|
|
47
|
-
[ErrorCode.CONNECTION_FAILED]: 'CONNECTION_FAILED',
|
|
48
|
-
[ErrorCode.CONNECTION_TIMEOUT]: 'CONNECTION_TIMEOUT',
|
|
49
|
-
[ErrorCode.NO_PAGE_CONNECTED]: 'NO_PAGE_CONNECTED',
|
|
50
|
-
[ErrorCode.PAGE_NOT_FOUND]: 'PAGE_NOT_FOUND',
|
|
51
|
-
[ErrorCode.NODE_NOT_FOUND]: 'NODE_NOT_FOUND',
|
|
52
|
-
[ErrorCode.NO_SELECTION]: 'NO_SELECTION',
|
|
53
|
-
[ErrorCode.MG_UNAVAILABLE]: 'MG_UNAVAILABLE',
|
|
54
|
-
[ErrorCode.EXPORT_FAILED]: 'EXPORT_FAILED',
|
|
55
|
-
[ErrorCode.FILE_WRITE_FAILED]: 'FILE_WRITE_FAILED',
|
|
56
|
-
[ErrorCode.INVALID_LINK]: 'INVALID_LINK',
|
|
57
|
-
[ErrorCode.INVALID_PARAMS]: 'INVALID_PARAMS',
|
|
58
|
-
[ErrorCode.REQUEST_TIMEOUT]: 'REQUEST_TIMEOUT',
|
|
59
|
-
[ErrorCode.PORT_EXHAUSTED]: 'PORT_EXHAUSTED',
|
|
60
|
-
[ErrorCode.SERVER_DISCOVERY_FAILED]: 'SERVER_DISCOVERY_FAILED',
|
|
61
|
-
[ErrorCode.SERVER_START_FAILED]: 'SERVER_START_FAILED',
|
|
62
|
-
[ErrorCode.SERVER_ALREADY_RUNNING]: 'SERVER_ALREADY_RUNNING',
|
|
63
|
-
[ErrorCode.CONNECTION_LOST]: 'CONNECTION_LOST',
|
|
64
|
-
[ErrorCode.UNKNOWN_ERROR]: 'UNKNOWN_ERROR',
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** 错误码对应的默认消息 */
|
|
68
|
-
export const ErrorMessages: Record<ErrorCode, string> = {
|
|
69
|
-
[ErrorCode.CONNECTION_FAILED]: '无法连接到 MG Server',
|
|
70
|
-
[ErrorCode.CONNECTION_TIMEOUT]: '连接超时',
|
|
71
|
-
[ErrorCode.NO_PAGE_CONNECTED]: '没有 MasterGo 页面连接到 Server',
|
|
72
|
-
[ErrorCode.PAGE_NOT_FOUND]: '未找到匹配的页面',
|
|
73
|
-
[ErrorCode.NODE_NOT_FOUND]: '节点不存在',
|
|
74
|
-
[ErrorCode.NO_SELECTION]: '没有选中任何节点',
|
|
75
|
-
[ErrorCode.MG_UNAVAILABLE]: 'mg 对象不可用',
|
|
76
|
-
[ErrorCode.EXPORT_FAILED]: '导出图片失败',
|
|
77
|
-
[ErrorCode.FILE_WRITE_FAILED]: '文件写入失败',
|
|
78
|
-
[ErrorCode.INVALID_LINK]: '无效的 mgp:// 链接格式',
|
|
79
|
-
[ErrorCode.INVALID_PARAMS]: '参数校验失败',
|
|
80
|
-
[ErrorCode.REQUEST_TIMEOUT]: '请求超时',
|
|
81
|
-
[ErrorCode.PORT_EXHAUSTED]: '所有备选端口均被占用',
|
|
82
|
-
[ErrorCode.SERVER_DISCOVERY_FAILED]: '无法发现 Server (端口扫描失败)',
|
|
83
|
-
[ErrorCode.SERVER_START_FAILED]: '自动启动 Server 失败',
|
|
84
|
-
[ErrorCode.SERVER_ALREADY_RUNNING]: 'Server 已在运行中',
|
|
85
|
-
[ErrorCode.CONNECTION_LOST]: '连接断开',
|
|
86
|
-
[ErrorCode.UNKNOWN_ERROR]: '未知错误',
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/** MG Plugin 错误类 */
|
|
90
|
-
export class MGError extends Error {
|
|
91
|
-
/** 错误码 */
|
|
92
|
-
code: ErrorCode
|
|
93
|
-
/** 错误名称 */
|
|
94
|
-
errorName: string
|
|
95
|
-
/** 额外详情 */
|
|
96
|
-
details?: Record<string, unknown>
|
|
97
|
-
|
|
98
|
-
constructor(code: ErrorCode, message?: string, details?: Record<string, unknown>) {
|
|
99
|
-
super(message || ErrorMessages[code])
|
|
100
|
-
this.name = 'MGError'
|
|
101
|
-
this.code = code
|
|
102
|
-
this.errorName = ErrorNames[code]
|
|
103
|
-
this.details = details
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/** 转换为 JSON 格式 */
|
|
107
|
-
toJSON() {
|
|
108
|
-
return {
|
|
109
|
-
code: this.code,
|
|
110
|
-
name: this.errorName,
|
|
111
|
-
message: this.message,
|
|
112
|
-
details: this.details,
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/** 格式化输出 */
|
|
117
|
-
toString() {
|
|
118
|
-
return `错误 [${this.code}]: ${this.message}`
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* 创建错误
|
|
124
|
-
*/
|
|
125
|
-
export function createError(
|
|
126
|
-
code: ErrorCode,
|
|
127
|
-
message?: string,
|
|
128
|
-
details?: Record<string, unknown>
|
|
129
|
-
): MGError {
|
|
130
|
-
return new MGError(code, message, details)
|
|
131
|
-
}
|
package/src/shared/index.ts
DELETED
package/src/shared/types.ts
DELETED
|
@@ -1,227 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MG Plugin 类型定义
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { ConnectionType, MessageType } from './constants.js'
|
|
6
|
-
import { ErrorCode } from './errors.js'
|
|
7
|
-
|
|
8
|
-
// ==================== Server 信息 ====================
|
|
9
|
-
|
|
10
|
-
/** Server 运行信息 */
|
|
11
|
-
export interface ServerInfo {
|
|
12
|
-
/** 监听端口 */
|
|
13
|
-
port: number
|
|
14
|
-
/** 进程 ID */
|
|
15
|
-
pid: number
|
|
16
|
-
/** 启动时间 (ISO 8601) */
|
|
17
|
-
startedAt: string
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// ==================== WebSocket 消息 ====================
|
|
21
|
-
|
|
22
|
-
/** 基础消息结构 */
|
|
23
|
-
export interface BaseMessage {
|
|
24
|
-
/** 消息 ID */
|
|
25
|
-
id?: string
|
|
26
|
-
/** 消息类型 */
|
|
27
|
-
type: MessageType | string
|
|
28
|
-
/** 时间戳 */
|
|
29
|
-
timestamp?: number
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** 请求消息 */
|
|
33
|
-
export interface RequestMessage extends BaseMessage {
|
|
34
|
-
/** 目标页面 URL(标准化后的) */
|
|
35
|
-
pageUrl?: string
|
|
36
|
-
/** 请求参数 */
|
|
37
|
-
params?: Record<string, unknown>
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** 响应消息 */
|
|
41
|
-
export interface ResponseMessage extends BaseMessage {
|
|
42
|
-
/** 对应请求的 ID */
|
|
43
|
-
id: string
|
|
44
|
-
/** 是否成功 */
|
|
45
|
-
success: boolean
|
|
46
|
-
/** 成功时的数据 */
|
|
47
|
-
data?: unknown
|
|
48
|
-
/** 失败时的错误信息 */
|
|
49
|
-
error?: ErrorInfo | null
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** 错误信息 */
|
|
53
|
-
export interface ErrorInfo {
|
|
54
|
-
/** 错误码 */
|
|
55
|
-
code: ErrorCode
|
|
56
|
-
/** 错误名称 */
|
|
57
|
-
name: string
|
|
58
|
-
/** 错误消息 */
|
|
59
|
-
message: string
|
|
60
|
-
/** 额外详情 */
|
|
61
|
-
details?: Record<string, unknown>
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** 注册消息 */
|
|
65
|
-
export interface RegisterMessage extends BaseMessage {
|
|
66
|
-
type: 'register'
|
|
67
|
-
data: {
|
|
68
|
-
/** 连接类型 */
|
|
69
|
-
connectionType: ConnectionType
|
|
70
|
-
/** 页面 URL(Provider 必需) */
|
|
71
|
-
pageUrl?: string
|
|
72
|
-
/** 页面唯一 ID */
|
|
73
|
-
pageId?: string
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** 心跳消息 */
|
|
78
|
-
export interface PingMessage extends BaseMessage {
|
|
79
|
-
type: 'ping'
|
|
80
|
-
timestamp: number
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export interface PongMessage extends BaseMessage {
|
|
84
|
-
type: 'pong'
|
|
85
|
-
timestamp: number
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// ==================== 连接管理 ====================
|
|
89
|
-
|
|
90
|
-
/** 连接信息 */
|
|
91
|
-
export interface ConnectionInfo {
|
|
92
|
-
/** 连接 ID */
|
|
93
|
-
id: string
|
|
94
|
-
/** 连接类型 */
|
|
95
|
-
type: ConnectionType
|
|
96
|
-
/** 页面 URL(仅 Provider) */
|
|
97
|
-
pageUrl?: string
|
|
98
|
-
/** 页面 ID(仅 Provider) */
|
|
99
|
-
pageId?: string
|
|
100
|
-
/** 连接时间 */
|
|
101
|
-
connectedAt: Date
|
|
102
|
-
/** 最后活跃时间 */
|
|
103
|
-
lastActiveAt: Date
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// ==================== 节点相关 ====================
|
|
107
|
-
|
|
108
|
-
/** 节点信息(简化版) */
|
|
109
|
-
export interface NodeInfo {
|
|
110
|
-
/** 节点 ID */
|
|
111
|
-
id: string
|
|
112
|
-
/** 节点名称 */
|
|
113
|
-
name: string
|
|
114
|
-
/** 节点类型 */
|
|
115
|
-
type: string
|
|
116
|
-
/** 是否可见 */
|
|
117
|
-
visible: boolean
|
|
118
|
-
/** X 坐标 */
|
|
119
|
-
x?: number
|
|
120
|
-
/** Y 坐标 */
|
|
121
|
-
y?: number
|
|
122
|
-
/** 宽度 */
|
|
123
|
-
width?: number
|
|
124
|
-
/** 高度 */
|
|
125
|
-
height?: number
|
|
126
|
-
/** 子节点 */
|
|
127
|
-
children?: NodeInfo[]
|
|
128
|
-
/** 其他属性 */
|
|
129
|
-
[key: string]: unknown
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/** 获取节点参数 */
|
|
133
|
-
export interface GetNodeParams {
|
|
134
|
-
/** 节点 ID */
|
|
135
|
-
nodeId: string
|
|
136
|
-
/** 遍历深度 */
|
|
137
|
-
maxDepth?: number
|
|
138
|
-
/** 是否包含不可见节点 */
|
|
139
|
-
includeInvisible?: boolean
|
|
140
|
-
/** 索引签名 */
|
|
141
|
-
[key: string]: unknown
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/** 获取所有节点参数 */
|
|
145
|
-
export interface GetAllNodesParams {
|
|
146
|
-
/** 遍历深度 */
|
|
147
|
-
maxDepth?: number
|
|
148
|
-
/** 是否包含不可见节点 */
|
|
149
|
-
includeInvisible?: boolean
|
|
150
|
-
/** 索引签名 */
|
|
151
|
-
[key: string]: unknown
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/** 导出图片参数 */
|
|
155
|
-
export interface ExportImageParams {
|
|
156
|
-
/** 节点 ID(可选,默认第一个选中节点) */
|
|
157
|
-
nodeId?: string
|
|
158
|
-
/** 导出格式 */
|
|
159
|
-
format?: 'PNG' | 'JPG' | 'SVG' | 'PDF' | 'WEBP'
|
|
160
|
-
/** 缩放倍率 */
|
|
161
|
-
scale?: number
|
|
162
|
-
/** 固定宽度 */
|
|
163
|
-
width?: number
|
|
164
|
-
/** 固定高度 */
|
|
165
|
-
height?: number
|
|
166
|
-
/** 是否使用完整尺寸 */
|
|
167
|
-
useAbsoluteBounds?: boolean
|
|
168
|
-
/** 是否包含特效和外描边 */
|
|
169
|
-
useRenderBounds?: boolean
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// ==================== CLI 相关 ====================
|
|
173
|
-
|
|
174
|
-
/** CLI 输出格式化函数类型 */
|
|
175
|
-
export type OutputFormatter = (data: Record<string, unknown>) => void
|
|
176
|
-
|
|
177
|
-
/** CLI 命令选项 */
|
|
178
|
-
export interface CliOptions {
|
|
179
|
-
/** 输出文件路径 */
|
|
180
|
-
output?: string
|
|
181
|
-
/** 遍历深度 */
|
|
182
|
-
maxDepth?: number
|
|
183
|
-
/** 包含不可见节点 */
|
|
184
|
-
includeInvisible?: boolean
|
|
185
|
-
/** 禁用自动重试 */
|
|
186
|
-
noRetry?: boolean
|
|
187
|
-
/** 禁用自动启动 Server */
|
|
188
|
-
noAutoStart?: boolean
|
|
189
|
-
/** Server 端口 */
|
|
190
|
-
port?: number
|
|
191
|
-
/** 前台模式运行 */
|
|
192
|
-
foreground?: boolean
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// ==================== 页面信息 ====================
|
|
196
|
-
|
|
197
|
-
/** RGBA 颜色 */
|
|
198
|
-
export interface RGBA {
|
|
199
|
-
r: number
|
|
200
|
-
g: number
|
|
201
|
-
b: number
|
|
202
|
-
a: number
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/** 单个页面信息 */
|
|
206
|
-
export interface MgPageInfo {
|
|
207
|
-
/** 页面 ID */
|
|
208
|
-
id: string
|
|
209
|
-
/** 页面名称 */
|
|
210
|
-
name: string
|
|
211
|
-
/** 页面背景色 */
|
|
212
|
-
bgColor?: RGBA
|
|
213
|
-
/** 直接子节点数量 */
|
|
214
|
-
childCount: number
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/** 所有页面信息 */
|
|
218
|
-
export interface AllPagesInfo {
|
|
219
|
-
/** 文档 ID */
|
|
220
|
-
documentId: string
|
|
221
|
-
/** 文档名称 */
|
|
222
|
-
documentName: string
|
|
223
|
-
/** 页面列表 */
|
|
224
|
-
pages: MgPageInfo[]
|
|
225
|
-
/** 页面总数 */
|
|
226
|
-
totalCount: number
|
|
227
|
-
}
|
package/src/shared/utils.ts
DELETED
|
@@ -1,352 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MG Plugin 工具函数
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from 'node:fs'
|
|
6
|
-
import { dirname, resolve, isAbsolute } from 'node:path'
|
|
7
|
-
import { CONFIG_DIR, SERVER_INFO_FILE, LOG_DIR } from './constants.js'
|
|
8
|
-
import type { ServerInfo } from './types.js'
|
|
9
|
-
|
|
10
|
-
// ==================== 文件操作 ====================
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* 确保目录存在
|
|
14
|
-
*/
|
|
15
|
-
export function ensureDir(dir: string): void {
|
|
16
|
-
if (!existsSync(dir)) {
|
|
17
|
-
mkdirSync(dir, { recursive: true })
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* 确保配置目录存在
|
|
23
|
-
*/
|
|
24
|
-
export function ensureConfigDir(): void {
|
|
25
|
-
ensureDir(CONFIG_DIR)
|
|
26
|
-
ensureDir(LOG_DIR)
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* 读取 Server 信息文件
|
|
31
|
-
*/
|
|
32
|
-
export function readServerInfo(): ServerInfo | null {
|
|
33
|
-
try {
|
|
34
|
-
if (!existsSync(SERVER_INFO_FILE)) {
|
|
35
|
-
return null
|
|
36
|
-
}
|
|
37
|
-
const content = readFileSync(SERVER_INFO_FILE, 'utf-8')
|
|
38
|
-
return JSON.parse(content) as ServerInfo
|
|
39
|
-
} catch {
|
|
40
|
-
return null
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* 写入 Server 信息文件
|
|
46
|
-
*/
|
|
47
|
-
export function writeServerInfo(info: ServerInfo): void {
|
|
48
|
-
ensureConfigDir()
|
|
49
|
-
writeFileSync(SERVER_INFO_FILE, JSON.stringify(info, null, 2), 'utf-8')
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* 删除 Server 信息文件
|
|
54
|
-
*/
|
|
55
|
-
export function deleteServerInfo(): void {
|
|
56
|
-
try {
|
|
57
|
-
if (existsSync(SERVER_INFO_FILE)) {
|
|
58
|
-
unlinkSync(SERVER_INFO_FILE)
|
|
59
|
-
}
|
|
60
|
-
} catch {
|
|
61
|
-
// 忽略删除错误
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* 解析输出路径(支持相对路径和绝对路径)
|
|
67
|
-
*/
|
|
68
|
-
export function resolveOutputPath(outputPath: string): string {
|
|
69
|
-
if (isAbsolute(outputPath)) {
|
|
70
|
-
return outputPath
|
|
71
|
-
}
|
|
72
|
-
return resolve(process.cwd(), outputPath)
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* 确保输出路径的目录存在
|
|
77
|
-
*/
|
|
78
|
-
export function ensureOutputDir(outputPath: string): void {
|
|
79
|
-
const dir = dirname(outputPath)
|
|
80
|
-
ensureDir(dir)
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// ==================== 进程管理 ====================
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* 检查进程是否存在
|
|
87
|
-
*/
|
|
88
|
-
export function isProcessRunning(pid: number): boolean {
|
|
89
|
-
try {
|
|
90
|
-
// 发送信号 0 不会终止进程,但如果进程不存在会抛出错误
|
|
91
|
-
process.kill(pid, 0)
|
|
92
|
-
return true
|
|
93
|
-
} catch {
|
|
94
|
-
return false
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* 终止进程
|
|
100
|
-
*/
|
|
101
|
-
export function killProcess(pid: number): boolean {
|
|
102
|
-
try {
|
|
103
|
-
process.kill(pid, 'SIGTERM')
|
|
104
|
-
return true
|
|
105
|
-
} catch {
|
|
106
|
-
return false
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// ==================== URL 处理 ====================
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* 标准化页面 URL
|
|
114
|
-
*
|
|
115
|
-
* 输入: https://mastergo.netease.com/file/174135798361888?fileOpenFrom=home&page_id=0%3A8347
|
|
116
|
-
* 输出: mastergo.netease.com/file/174135798361888
|
|
117
|
-
*/
|
|
118
|
-
export function normalizePageUrl(url: string): string {
|
|
119
|
-
try {
|
|
120
|
-
const urlObj = new URL(url)
|
|
121
|
-
// 返回 host + pathname,去掉 https:// 前缀和查询参数
|
|
122
|
-
return urlObj.host + urlObj.pathname
|
|
123
|
-
} catch {
|
|
124
|
-
// 如果已经是标准化的格式,直接返回
|
|
125
|
-
return url
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* 检查是否是设计页面 URL
|
|
131
|
-
*/
|
|
132
|
-
export function isDesignPageUrl(url: string): boolean {
|
|
133
|
-
// 匹配 /file/数字 格式
|
|
134
|
-
return /\/file\/\d+/.test(url)
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* 解析 mgp:// 链接
|
|
139
|
-
*
|
|
140
|
-
* 支持的格式 (queryParams 格式,nodeId 需要 URL 编码):
|
|
141
|
-
* - mgp://mastergo.netease.com/file/174135798361888?nodeId=123%3A456 (单个节点)
|
|
142
|
-
* - mgp://mastergo.netease.com/file/174135798361888?nodeId=0%3A8633&nodePath=314%3A13190%2F0%3A8633 (带父节点路径)
|
|
143
|
-
*
|
|
144
|
-
* 输出: { pageUrl: 'mastergo.netease.com/file/174135798361888', nodeId: '0:8633', nodePath: ['314:13190', '0:8633'] }
|
|
145
|
-
*/
|
|
146
|
-
export function parseMgpLink(link: string): { pageUrl: string; nodeId: string; nodePath?: string[] } | null {
|
|
147
|
-
// 检查是否是 mgp:// 协议
|
|
148
|
-
if (!link.startsWith('mgp://')) {
|
|
149
|
-
return null
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
try {
|
|
153
|
-
// 移除 mgp:// 前缀,构造为可解析的 URL
|
|
154
|
-
const urlPart = link.slice(6) // 移除 'mgp://'
|
|
155
|
-
const questionMarkIndex = urlPart.indexOf('?')
|
|
156
|
-
|
|
157
|
-
if (questionMarkIndex === -1) {
|
|
158
|
-
// 没有查询参数,无效格式
|
|
159
|
-
return null
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const pageUrl = urlPart.slice(0, questionMarkIndex)
|
|
163
|
-
const queryString = urlPart.slice(questionMarkIndex + 1)
|
|
164
|
-
|
|
165
|
-
// 解析查询参数
|
|
166
|
-
const params = new URLSearchParams(queryString)
|
|
167
|
-
const encodedNodeId = params.get('nodeId')
|
|
168
|
-
|
|
169
|
-
if (!encodedNodeId) {
|
|
170
|
-
return null
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// 解码 nodeId
|
|
174
|
-
const nodeId = decodeURIComponent(encodedNodeId)
|
|
175
|
-
|
|
176
|
-
// 验证 nodeId 格式:支持单个节点 (数字:数字) 或带路径的格式 (数字:数字/数字:数字/...)
|
|
177
|
-
if (!/^(\d+:\d+)(\/\d+:\d+)*$/.test(nodeId)) {
|
|
178
|
-
return null
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// 解析可选的 nodePath
|
|
182
|
-
const encodedNodePath = params.get('nodePath')
|
|
183
|
-
let nodePath: string[] | undefined
|
|
184
|
-
|
|
185
|
-
if (encodedNodePath) {
|
|
186
|
-
const decodedNodePath = decodeURIComponent(encodedNodePath)
|
|
187
|
-
nodePath = decodedNodePath.split('/').filter(Boolean)
|
|
188
|
-
// 验证每个路径段的格式
|
|
189
|
-
if (!nodePath.every(segment => /^\d+:\d+$/.test(segment))) {
|
|
190
|
-
return null
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
return {
|
|
195
|
-
pageUrl,
|
|
196
|
-
nodeId,
|
|
197
|
-
nodePath,
|
|
198
|
-
}
|
|
199
|
-
} catch {
|
|
200
|
-
return null
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* 生成 mgp:// 链接
|
|
206
|
-
*
|
|
207
|
-
* @param pageUrl 页面 URL(会被标准化)
|
|
208
|
-
* @param nodeId 节点 ID(会被 URL 编码)
|
|
209
|
-
* @param nodePath 可选的父节点路径(会被 URL 编码)
|
|
210
|
-
*/
|
|
211
|
-
export function generateMgpLink(pageUrl: string, nodeId: string, nodePath?: string[]): string {
|
|
212
|
-
const normalizedUrl = normalizePageUrl(pageUrl)
|
|
213
|
-
const encodedNodeId = encodeURIComponent(nodeId)
|
|
214
|
-
|
|
215
|
-
let link = `mgp://${normalizedUrl}?nodeId=${encodedNodeId}`
|
|
216
|
-
|
|
217
|
-
if (nodePath && nodePath.length > 0) {
|
|
218
|
-
const encodedNodePath = encodeURIComponent(nodePath.join('/'))
|
|
219
|
-
link += `&nodePath=${encodedNodePath}`
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
return link
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// ==================== 输出格式化 ====================
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* 格式化文件大小
|
|
229
|
-
*/
|
|
230
|
-
export function formatFileSize(bytes: number): string {
|
|
231
|
-
if (bytes < 1024) {
|
|
232
|
-
return `${bytes} 字节`
|
|
233
|
-
}
|
|
234
|
-
const kb = bytes / 1024
|
|
235
|
-
if (kb < 1024) {
|
|
236
|
-
return `${kb.toFixed(2)} KB`
|
|
237
|
-
}
|
|
238
|
-
const mb = kb / 1024
|
|
239
|
-
return `${mb.toFixed(2)} MB`
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
* 格式化时间间隔
|
|
244
|
-
*/
|
|
245
|
-
export function formatDuration(ms: number): string {
|
|
246
|
-
const seconds = Math.floor(ms / 1000)
|
|
247
|
-
const minutes = Math.floor(seconds / 60)
|
|
248
|
-
const hours = Math.floor(minutes / 60)
|
|
249
|
-
const days = Math.floor(hours / 24)
|
|
250
|
-
|
|
251
|
-
if (days > 0) {
|
|
252
|
-
return `${days} 天 ${hours % 24} 小时`
|
|
253
|
-
}
|
|
254
|
-
if (hours > 0) {
|
|
255
|
-
return `${hours} 小时 ${minutes % 60} 分钟`
|
|
256
|
-
}
|
|
257
|
-
if (minutes > 0) {
|
|
258
|
-
return `${minutes} 分钟 ${seconds % 60} 秒`
|
|
259
|
-
}
|
|
260
|
-
return `${seconds} 秒`
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
* 生成唯一 ID
|
|
265
|
-
*/
|
|
266
|
-
export function generateId(): string {
|
|
267
|
-
return `${Date.now()}_${Math.random().toString(36).substring(2, 9)}`
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* 获取当前时间的 ISO 格式字符串
|
|
272
|
-
*/
|
|
273
|
-
export function getCurrentISOTime(): string {
|
|
274
|
-
return new Date().toISOString()
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
/**
|
|
278
|
-
* 格式化日期时间用于日志
|
|
279
|
-
*/
|
|
280
|
-
export function formatLogTime(date: Date = new Date()): string {
|
|
281
|
-
const year = date.getFullYear()
|
|
282
|
-
const month = String(date.getMonth() + 1).padStart(2, '0')
|
|
283
|
-
const day = String(date.getDate()).padStart(2, '0')
|
|
284
|
-
const hours = String(date.getHours()).padStart(2, '0')
|
|
285
|
-
const minutes = String(date.getMinutes()).padStart(2, '0')
|
|
286
|
-
const seconds = String(date.getSeconds()).padStart(2, '0')
|
|
287
|
-
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// ==================== FileId 提取 ====================
|
|
291
|
-
|
|
292
|
-
/**
|
|
293
|
-
* 从完整 URL 提取 fileId
|
|
294
|
-
*
|
|
295
|
-
* 支持格式:
|
|
296
|
-
* - https://mastergo.netease.com/file/174875497054651?page_id=321%3A11979
|
|
297
|
-
* - mastergo.netease.com/file/174875497054651
|
|
298
|
-
*
|
|
299
|
-
* @returns fileId 或 null
|
|
300
|
-
*/
|
|
301
|
-
export function extractFileIdFromUrl(url: string): string | null {
|
|
302
|
-
// 匹配 /file/数字 格式
|
|
303
|
-
const match = url.match(/\/file\/(\d+)/)
|
|
304
|
-
return match ? match[1] : null
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
/**
|
|
308
|
-
* 从 mgp:// 链接提取 fileId
|
|
309
|
-
*
|
|
310
|
-
* 支持格式:
|
|
311
|
-
* - mgp://mastergo.netease.com/file/174875497054651?nodeId=xxx
|
|
312
|
-
*
|
|
313
|
-
* @returns fileId 或 null
|
|
314
|
-
*/
|
|
315
|
-
export function extractFileIdFromMgpLink(link: string): string | null {
|
|
316
|
-
if (!link.startsWith('mgp://')) {
|
|
317
|
-
return null
|
|
318
|
-
}
|
|
319
|
-
return extractFileIdFromUrl(link)
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
/**
|
|
323
|
-
* 统一处理输入,提取 fileId
|
|
324
|
-
*
|
|
325
|
-
* 支持三种输入格式:
|
|
326
|
-
* 1. 完整 URL: https://mastergo.netease.com/file/174875497054651?page_id=xxx
|
|
327
|
-
* 2. mgp 协议: mgp://mastergo.netease.com/file/174875497054651?nodeId=xxx
|
|
328
|
-
* 3. 纯 fileId: 174875497054651
|
|
329
|
-
*
|
|
330
|
-
* @returns fileId 或 null
|
|
331
|
-
*/
|
|
332
|
-
export function extractFileId(input: string): string | null {
|
|
333
|
-
const trimmed = input.trim()
|
|
334
|
-
|
|
335
|
-
// 1. 纯数字 fileId
|
|
336
|
-
if (/^\d+$/.test(trimmed)) {
|
|
337
|
-
return trimmed
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// 2. mgp:// 协议
|
|
341
|
-
if (trimmed.startsWith('mgp://')) {
|
|
342
|
-
return extractFileIdFromMgpLink(trimmed)
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// 3. 完整 URL (http:// 或 https://)
|
|
346
|
-
if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
|
|
347
|
-
return extractFileIdFromUrl(trimmed)
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
// 4. 尝试作为不带协议的 URL 解析
|
|
351
|
-
return extractFileIdFromUrl(trimmed)
|
|
352
|
-
}
|