@adversity/coding-tool-x 2.3.0 → 2.4.1
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/CHANGELOG.md +41 -0
- package/README.md +8 -0
- package/dist/web/assets/icons-Dom8a0SN.js +1 -0
- package/dist/web/assets/index-CQeUIH7E.css +41 -0
- package/dist/web/assets/index-YrjlFzC4.js +14 -0
- package/dist/web/assets/naive-ui-BjMHakwv.js +1 -0
- package/dist/web/assets/vendors-DtJKdpSj.js +7 -0
- package/dist/web/assets/vue-vendor-VFuFB5f4.js +44 -0
- package/dist/web/index.html +6 -2
- package/package.json +2 -2
- package/src/commands/export-config.js +205 -0
- package/src/config/default.js +1 -1
- package/src/server/api/config-export.js +122 -0
- package/src/server/api/config-sync.js +155 -0
- package/src/server/api/config-templates.js +12 -6
- package/src/server/api/health-check.js +1 -89
- package/src/server/api/permissions.js +92 -69
- package/src/server/api/projects.js +2 -2
- package/src/server/api/sessions.js +70 -70
- package/src/server/api/skills.js +206 -0
- package/src/server/api/terminal.js +26 -0
- package/src/server/index.js +7 -11
- package/src/server/services/config-export-service.js +415 -0
- package/src/server/services/config-sync-service.js +515 -0
- package/src/server/services/config-templates-service.js +61 -38
- package/src/server/services/enhanced-cache.js +196 -0
- package/src/server/services/health-check.js +1 -315
- package/src/server/services/permission-templates-service.js +339 -0
- package/src/server/services/pty-manager.js +35 -1
- package/src/server/services/sessions.js +122 -44
- package/src/server/services/skill-service.js +252 -2
- package/src/server/services/workspace-service.js +44 -84
- package/src/server/websocket-server.js +4 -1
- package/dist/web/assets/index-dhun1bYQ.js +0 -3555
- package/dist/web/assets/index-hHb7DAda.css +0 -41
|
@@ -847,14 +847,14 @@ class SkillService {
|
|
|
847
847
|
|
|
848
848
|
request.on('error', (err) => {
|
|
849
849
|
file.close();
|
|
850
|
-
fs.unlink(dest, () => {});
|
|
850
|
+
fs.unlink(dest, () => { });
|
|
851
851
|
reject(err);
|
|
852
852
|
});
|
|
853
853
|
|
|
854
854
|
request.on('timeout', () => {
|
|
855
855
|
request.destroy();
|
|
856
856
|
file.close();
|
|
857
|
-
fs.unlink(dest, () => {});
|
|
857
|
+
fs.unlink(dest, () => { });
|
|
858
858
|
reject(new Error('Download timeout'));
|
|
859
859
|
});
|
|
860
860
|
});
|
|
@@ -912,6 +912,256 @@ ${content}
|
|
|
912
912
|
return { success: true, message: '技能创建成功', directory };
|
|
913
913
|
}
|
|
914
914
|
|
|
915
|
+
/**
|
|
916
|
+
* 创建带多文件的技能
|
|
917
|
+
* @param {string} directory - 技能目录名
|
|
918
|
+
* @param {Array<{path: string, content: string}>} files - 文件数组
|
|
919
|
+
* @returns {Object} 创建结果
|
|
920
|
+
*/
|
|
921
|
+
createSkillWithFiles({ directory, files }) {
|
|
922
|
+
const dest = path.join(this.installDir, directory);
|
|
923
|
+
|
|
924
|
+
// 检查是否已存在
|
|
925
|
+
if (fs.existsSync(dest)) {
|
|
926
|
+
throw new Error(`技能目录 "${directory}" 已存在`);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// 验证必须包含 SKILL.md
|
|
930
|
+
const hasSkillMd = files.some(f =>
|
|
931
|
+
f.path === 'SKILL.md' || f.path.endsWith('/SKILL.md')
|
|
932
|
+
);
|
|
933
|
+
if (!hasSkillMd) {
|
|
934
|
+
throw new Error('技能必须包含 SKILL.md 文件');
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// 创建目录
|
|
938
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
939
|
+
|
|
940
|
+
// 写入所有文件
|
|
941
|
+
for (const file of files) {
|
|
942
|
+
const filePath = path.join(dest, file.path);
|
|
943
|
+
const fileDir = path.dirname(filePath);
|
|
944
|
+
|
|
945
|
+
// 确保父目录存在
|
|
946
|
+
if (!fs.existsSync(fileDir)) {
|
|
947
|
+
fs.mkdirSync(fileDir, { recursive: true });
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// 写入文件内容
|
|
951
|
+
if (file.isBase64) {
|
|
952
|
+
// 二进制文件使用 base64 编码
|
|
953
|
+
fs.writeFileSync(filePath, Buffer.from(file.content, 'base64'));
|
|
954
|
+
} else {
|
|
955
|
+
fs.writeFileSync(filePath, file.content, 'utf-8');
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// 清除缓存
|
|
960
|
+
this.skillsCache = null;
|
|
961
|
+
this.cacheTime = 0;
|
|
962
|
+
|
|
963
|
+
return {
|
|
964
|
+
success: true,
|
|
965
|
+
message: '技能创建成功',
|
|
966
|
+
directory,
|
|
967
|
+
fileCount: files.length
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
/**
|
|
972
|
+
* 获取技能目录下所有文件列表
|
|
973
|
+
* @param {string} directory - 技能目录名
|
|
974
|
+
* @returns {Array<{path: string, size: number, isDirectory: boolean}>}
|
|
975
|
+
*/
|
|
976
|
+
getSkillFiles(directory) {
|
|
977
|
+
const skillPath = path.join(this.installDir, directory);
|
|
978
|
+
|
|
979
|
+
if (!fs.existsSync(skillPath)) {
|
|
980
|
+
throw new Error(`技能 "${directory}" 不存在`);
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
const files = [];
|
|
984
|
+
this._scanFilesRecursive(skillPath, skillPath, files);
|
|
985
|
+
return files;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
/**
|
|
989
|
+
* 递归扫描目录获取文件列表
|
|
990
|
+
*/
|
|
991
|
+
_scanFilesRecursive(currentDir, baseDir, files) {
|
|
992
|
+
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
993
|
+
|
|
994
|
+
for (const entry of entries) {
|
|
995
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
996
|
+
const relativePath = path.relative(baseDir, fullPath);
|
|
997
|
+
|
|
998
|
+
if (entry.isDirectory()) {
|
|
999
|
+
files.push({
|
|
1000
|
+
path: relativePath,
|
|
1001
|
+
size: 0,
|
|
1002
|
+
isDirectory: true
|
|
1003
|
+
});
|
|
1004
|
+
this._scanFilesRecursive(fullPath, baseDir, files);
|
|
1005
|
+
} else {
|
|
1006
|
+
const stats = fs.statSync(fullPath);
|
|
1007
|
+
files.push({
|
|
1008
|
+
path: relativePath,
|
|
1009
|
+
size: stats.size,
|
|
1010
|
+
isDirectory: false
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
/**
|
|
1017
|
+
* 获取技能文件内容
|
|
1018
|
+
* @param {string} directory - 技能目录名
|
|
1019
|
+
* @param {string} filePath - 文件相对路径
|
|
1020
|
+
* @returns {Object} 文件内容
|
|
1021
|
+
*/
|
|
1022
|
+
getSkillFileContent(directory, filePath) {
|
|
1023
|
+
const fullPath = path.join(this.installDir, directory, filePath);
|
|
1024
|
+
|
|
1025
|
+
if (!fs.existsSync(fullPath)) {
|
|
1026
|
+
throw new Error(`文件 "${filePath}" 不存在`);
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
const stats = fs.statSync(fullPath);
|
|
1030
|
+
if (stats.isDirectory()) {
|
|
1031
|
+
throw new Error(`"${filePath}" 是目录,不是文件`);
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// 判断是否是文本文件
|
|
1035
|
+
const textExtensions = ['.md', '.txt', '.json', '.js', '.ts', '.py', '.sh', '.yaml', '.yml', '.toml', '.xml', '.html', '.css'];
|
|
1036
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
1037
|
+
const isText = textExtensions.includes(ext);
|
|
1038
|
+
|
|
1039
|
+
if (isText) {
|
|
1040
|
+
return {
|
|
1041
|
+
path: filePath,
|
|
1042
|
+
content: fs.readFileSync(fullPath, 'utf-8'),
|
|
1043
|
+
isBase64: false,
|
|
1044
|
+
size: stats.size
|
|
1045
|
+
};
|
|
1046
|
+
} else {
|
|
1047
|
+
return {
|
|
1048
|
+
path: filePath,
|
|
1049
|
+
content: fs.readFileSync(fullPath).toString('base64'),
|
|
1050
|
+
isBase64: true,
|
|
1051
|
+
size: stats.size
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
/**
|
|
1057
|
+
* 添加文件到现有技能
|
|
1058
|
+
* @param {string} directory - 技能目录名
|
|
1059
|
+
* @param {Array<{path: string, content: string, isBase64?: boolean}>} files - 文件数组
|
|
1060
|
+
*/
|
|
1061
|
+
addSkillFiles(directory, files) {
|
|
1062
|
+
const skillPath = path.join(this.installDir, directory);
|
|
1063
|
+
|
|
1064
|
+
if (!fs.existsSync(skillPath)) {
|
|
1065
|
+
throw new Error(`技能 "${directory}" 不存在`);
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
const added = [];
|
|
1069
|
+
for (const file of files) {
|
|
1070
|
+
const filePath = path.join(skillPath, file.path);
|
|
1071
|
+
const fileDir = path.dirname(filePath);
|
|
1072
|
+
|
|
1073
|
+
// 确保父目录存在
|
|
1074
|
+
if (!fs.existsSync(fileDir)) {
|
|
1075
|
+
fs.mkdirSync(fileDir, { recursive: true });
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
// 写入文件
|
|
1079
|
+
if (file.isBase64) {
|
|
1080
|
+
fs.writeFileSync(filePath, Buffer.from(file.content, 'base64'));
|
|
1081
|
+
} else {
|
|
1082
|
+
fs.writeFileSync(filePath, file.content, 'utf-8');
|
|
1083
|
+
}
|
|
1084
|
+
added.push(file.path);
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
// 清除缓存
|
|
1088
|
+
this.skillsCache = null;
|
|
1089
|
+
this.cacheTime = 0;
|
|
1090
|
+
|
|
1091
|
+
return { success: true, added };
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
/**
|
|
1095
|
+
* 删除技能中的文件
|
|
1096
|
+
* @param {string} directory - 技能目录名
|
|
1097
|
+
* @param {string} filePath - 文件相对路径
|
|
1098
|
+
*/
|
|
1099
|
+
deleteSkillFile(directory, filePath) {
|
|
1100
|
+
const skillPath = path.join(this.installDir, directory);
|
|
1101
|
+
|
|
1102
|
+
if (!fs.existsSync(skillPath)) {
|
|
1103
|
+
throw new Error(`技能 "${directory}" 不存在`);
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
// 不允许删除 SKILL.md
|
|
1107
|
+
if (filePath === 'SKILL.md') {
|
|
1108
|
+
throw new Error('不能删除 SKILL.md 文件');
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
const fullPath = path.join(skillPath, filePath);
|
|
1112
|
+
|
|
1113
|
+
if (!fs.existsSync(fullPath)) {
|
|
1114
|
+
throw new Error(`文件 "${filePath}" 不存在`);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
const stats = fs.statSync(fullPath);
|
|
1118
|
+
if (stats.isDirectory()) {
|
|
1119
|
+
fs.rmSync(fullPath, { recursive: true, force: true });
|
|
1120
|
+
} else {
|
|
1121
|
+
fs.unlinkSync(fullPath);
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// 清除缓存
|
|
1125
|
+
this.skillsCache = null;
|
|
1126
|
+
this.cacheTime = 0;
|
|
1127
|
+
|
|
1128
|
+
return { success: true, deleted: filePath };
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
/**
|
|
1132
|
+
* 更新技能文件内容
|
|
1133
|
+
* @param {string} directory - 技能目录名
|
|
1134
|
+
* @param {string} filePath - 文件相对路径
|
|
1135
|
+
* @param {string} content - 新内容
|
|
1136
|
+
* @param {boolean} isBase64 - 是否为 base64 编码
|
|
1137
|
+
*/
|
|
1138
|
+
updateSkillFile(directory, filePath, content, isBase64 = false) {
|
|
1139
|
+
const skillPath = path.join(this.installDir, directory);
|
|
1140
|
+
|
|
1141
|
+
if (!fs.existsSync(skillPath)) {
|
|
1142
|
+
throw new Error(`技能 "${directory}" 不存在`);
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
const fullPath = path.join(skillPath, filePath);
|
|
1146
|
+
|
|
1147
|
+
if (!fs.existsSync(fullPath)) {
|
|
1148
|
+
throw new Error(`文件 "${filePath}" 不存在`);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
if (isBase64) {
|
|
1152
|
+
fs.writeFileSync(fullPath, Buffer.from(content, 'base64'));
|
|
1153
|
+
} else {
|
|
1154
|
+
fs.writeFileSync(fullPath, content, 'utf-8');
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
// 清除缓存
|
|
1158
|
+
this.skillsCache = null;
|
|
1159
|
+
this.cacheTime = 0;
|
|
1160
|
+
|
|
1161
|
+
return { success: true, updated: filePath };
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
|
|
915
1165
|
/**
|
|
916
1166
|
* 卸载技能
|
|
917
1167
|
*/
|
|
@@ -4,6 +4,7 @@ const path = require('path');
|
|
|
4
4
|
const { execSync } = require('child_process');
|
|
5
5
|
const { PATHS } = require('../../config/paths');
|
|
6
6
|
const configTemplatesService = require('./config-templates-service');
|
|
7
|
+
const permissionTemplatesService = require('./permission-templates-service');
|
|
7
8
|
|
|
8
9
|
// 工作区配置文件路径
|
|
9
10
|
const WORKSPACES_CONFIG = path.join(PATHS.base, 'workspaces.json');
|
|
@@ -299,73 +300,14 @@ function createWorkspace(options) {
|
|
|
299
300
|
}
|
|
300
301
|
}
|
|
301
302
|
|
|
302
|
-
//
|
|
303
|
+
// 应用权限模板(如果指定)
|
|
303
304
|
let permissionInfo = null;
|
|
304
|
-
if (permissionTemplate
|
|
305
|
+
if (permissionTemplate) {
|
|
305
306
|
try {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
allow: [
|
|
309
|
-
'Bash(cat:*)',
|
|
310
|
-
'Bash(ls:*)',
|
|
311
|
-
'Bash(pwd)',
|
|
312
|
-
'Bash(echo:*)',
|
|
313
|
-
'Bash(head:*)',
|
|
314
|
-
'Bash(tail:*)',
|
|
315
|
-
'Bash(grep:*)',
|
|
316
|
-
'Read(*)'
|
|
317
|
-
],
|
|
318
|
-
deny: [
|
|
319
|
-
'Bash(rm:*)',
|
|
320
|
-
'Bash(sudo:*)',
|
|
321
|
-
'Bash(git push:*)',
|
|
322
|
-
'Bash(git reset --hard:*)',
|
|
323
|
-
'Bash(chmod:*)',
|
|
324
|
-
'Bash(chown:*)',
|
|
325
|
-
'Edit(*)'
|
|
326
|
-
]
|
|
327
|
-
},
|
|
328
|
-
balanced: {
|
|
329
|
-
allow: [
|
|
330
|
-
'Bash(cat:*)',
|
|
331
|
-
'Bash(ls:*)',
|
|
332
|
-
'Bash(pwd)',
|
|
333
|
-
'Bash(echo:*)',
|
|
334
|
-
'Bash(head:*)',
|
|
335
|
-
'Bash(tail:*)',
|
|
336
|
-
'Bash(grep:*)',
|
|
337
|
-
'Bash(find:*)',
|
|
338
|
-
'Bash(git status)',
|
|
339
|
-
'Bash(git diff:*)',
|
|
340
|
-
'Bash(git log:*)',
|
|
341
|
-
'Bash(npm run:*)',
|
|
342
|
-
'Bash(pnpm:*)',
|
|
343
|
-
'Bash(yarn:*)',
|
|
344
|
-
'Read(*)',
|
|
345
|
-
'Edit(*)'
|
|
346
|
-
],
|
|
347
|
-
deny: [
|
|
348
|
-
'Bash(rm -rf:*)',
|
|
349
|
-
'Bash(sudo:*)',
|
|
350
|
-
'Bash(git push --force:*)',
|
|
351
|
-
'Bash(git reset --hard:*)'
|
|
352
|
-
]
|
|
353
|
-
},
|
|
354
|
-
permissive: {
|
|
355
|
-
allow: [
|
|
356
|
-
'Bash(*)',
|
|
357
|
-
'Read(*)',
|
|
358
|
-
'Edit(*)'
|
|
359
|
-
],
|
|
360
|
-
deny: [
|
|
361
|
-
'Bash(rm -rf /*)',
|
|
362
|
-
'Bash(sudo rm -rf:*)'
|
|
363
|
-
]
|
|
364
|
-
}
|
|
365
|
-
};
|
|
307
|
+
// 从权限模板服务获取模板
|
|
308
|
+
const template = permissionTemplatesService.getTemplateById(permissionTemplate);
|
|
366
309
|
|
|
367
|
-
|
|
368
|
-
if (permSettings) {
|
|
310
|
+
if (template && template.permissions) {
|
|
369
311
|
// 为工作区中的每个项目应用权限设置
|
|
370
312
|
for (const proj of workspaceProjects) {
|
|
371
313
|
const projSettingsDir = path.join(proj.targetPath, '.claude');
|
|
@@ -388,8 +330,8 @@ function createWorkspace(options) {
|
|
|
388
330
|
|
|
389
331
|
// 更新权限设置
|
|
390
332
|
settings.permissions = {
|
|
391
|
-
allow:
|
|
392
|
-
deny:
|
|
333
|
+
allow: template.permissions.allow || [],
|
|
334
|
+
deny: template.permissions.deny || []
|
|
393
335
|
};
|
|
394
336
|
|
|
395
337
|
// 保存设置
|
|
@@ -454,27 +396,44 @@ function deleteWorkspace(id, removeFiles = false) {
|
|
|
454
396
|
|
|
455
397
|
const workspace = data.workspaces[index];
|
|
456
398
|
|
|
457
|
-
//
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
399
|
+
// 清理 worktrees (无论是否删除工作区目录,都应该清理 worktree)
|
|
400
|
+
for (const proj of workspace.projects) {
|
|
401
|
+
if (proj.isGitRepo && proj.sourcePath && fs.existsSync(proj.sourcePath)) {
|
|
402
|
+
try {
|
|
403
|
+
// 重新扫描实际的 worktrees,确保获取最新状态
|
|
404
|
+
const actualWorktrees = getGitWorktrees(proj.sourcePath);
|
|
405
|
+
for (const wt of actualWorktrees) {
|
|
406
|
+
// 只删除属于这个工作区的 worktree (通过 -ws- 标识符识别)
|
|
407
|
+
if (wt.path && wt.path.includes('-ws-')) {
|
|
408
|
+
try {
|
|
409
|
+
console.log(`清理 worktree: ${wt.path}`);
|
|
410
|
+
execSync(`git worktree remove "${wt.path}" --force`, {
|
|
411
|
+
cwd: proj.sourcePath,
|
|
412
|
+
stdio: 'pipe'
|
|
413
|
+
});
|
|
414
|
+
} catch (error) {
|
|
415
|
+
console.error(`删除 worktree 失败: ${wt.path}`, error.message);
|
|
416
|
+
// 如果 git worktree remove 失败,尝试手动删除目录
|
|
417
|
+
if (fs.existsSync(wt.path)) {
|
|
418
|
+
try {
|
|
419
|
+
fs.rmSync(wt.path, { recursive: true, force: true });
|
|
420
|
+
console.log(`手动删除 worktree 目录: ${wt.path}`);
|
|
421
|
+
} catch (rmError) {
|
|
422
|
+
console.error(`手动删除 worktree 目录失败: ${wt.path}`, rmError.message);
|
|
423
|
+
}
|
|
472
424
|
}
|
|
473
425
|
}
|
|
474
426
|
}
|
|
475
427
|
}
|
|
428
|
+
} catch (error) {
|
|
429
|
+
console.error(`扫描 worktree 失败: ${proj.sourcePath}`, error.message);
|
|
476
430
|
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
477
433
|
|
|
434
|
+
// 如果需要删除物理文件
|
|
435
|
+
if (removeFiles && fs.existsSync(workspace.path)) {
|
|
436
|
+
try {
|
|
478
437
|
// 删除工作区目录
|
|
479
438
|
fs.rmSync(workspace.path, { recursive: true, force: true });
|
|
480
439
|
} catch (error) {
|
|
@@ -678,10 +637,11 @@ function getAllAvailableProjects() {
|
|
|
678
637
|
const projects = sessionsService.getProjectsWithStats(config, { force: true });
|
|
679
638
|
|
|
680
639
|
for (const proj of projects) {
|
|
681
|
-
// 使用 fullPath
|
|
640
|
+
// 使用 fullPath + channel 组合去重,允许同一项目在不同渠道显示
|
|
682
641
|
const projectPath = proj.fullPath;
|
|
683
|
-
|
|
684
|
-
|
|
642
|
+
const projectKey = `${projectPath}:${channel.name}`;
|
|
643
|
+
if (projectPath && !seenPaths.has(projectKey)) {
|
|
644
|
+
seenPaths.add(projectKey);
|
|
685
645
|
allProjects.push({
|
|
686
646
|
name: proj.name,
|
|
687
647
|
displayName: proj.displayName,
|
|
@@ -175,7 +175,10 @@ function startWebSocketServer(httpServer) {
|
|
|
175
175
|
console.log(`✅ WebSocket server started on ws://127.0.0.1:${port}/ws`);
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
-
wss.on('connection', (ws) => {
|
|
178
|
+
wss.on('connection', (ws, req) => {
|
|
179
|
+
const clientIp = req.socket.remoteAddress;
|
|
180
|
+
console.log(`[WebSocket] New connection from ${clientIp}`);
|
|
181
|
+
|
|
179
182
|
wsClients.add(ws);
|
|
180
183
|
|
|
181
184
|
// 标记客户端存活
|