coding-tool-x 3.2.2 → 3.3.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 +20 -0
- package/dist/web/assets/{Analytics-COVBIlMT.js → Analytics-BskCbia_.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-CwCbgetE.js → ConfigTemplates-B4X3rgfY.js} +1 -1
- package/dist/web/assets/{Home-CgMMTGxS.js → Home-DHYMMKOU.js} +1 -1
- package/dist/web/assets/{PluginManager-DQ4B002M.js → PluginManager-D_LoULGH.js} +1 -1
- package/dist/web/assets/{ProjectList-BT99XzrL.js → ProjectList-DiV4Qwa1.js} +1 -1
- package/dist/web/assets/{SessionList-ButOecT4.js → SessionList-B24o0wiX.js} +1 -1
- package/dist/web/assets/{SkillManager-e2C5kuhp.js → SkillManager-B9Rnuaig.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-Dh5Rzjkr.js → WorkspaceManager-BkL2l5J9.js} +1 -1
- package/dist/web/assets/icons-B29onFfZ.js +1 -0
- package/dist/web/assets/index-C5j22icm.css +1 -0
- package/dist/web/assets/index-ZttxvTKw.js +2 -0
- package/dist/web/assets/{naive-ui-DlpKk-8M.js → naive-ui-CxpuzdjU.js} +1 -1
- package/dist/web/index.html +4 -4
- package/package.json +1 -1
- package/src/server/api/opencode-channels.js +30 -2
- package/src/server/opencode-proxy-server.js +16 -116
- package/src/server/proxy-server.js +2 -10
- package/src/server/services/channels.js +7 -5
- package/src/server/services/codex-channels.js +7 -5
- package/src/server/services/codex-settings-manager.js +13 -0
- package/src/server/services/config-templates-service.js +28 -22
- package/src/server/services/gemini-channels.js +7 -5
- package/src/server/services/mcp-service.js +22 -1
- package/src/server/services/request-logger.js +190 -0
- package/src/server/services/speed-test.js +17 -108
- package/src/utils/port-helper.js +26 -5
- package/dist/web/assets/icons-DRrXwWZi.js +0 -1
- package/dist/web/assets/index-CwGg4bbn.css +0 -1
- package/dist/web/assets/index-j56-PHWL.js +0 -2
|
@@ -122,9 +122,199 @@ function createApiRequestLogger() {
|
|
|
122
122
|
};
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
const CLAUDE_TEMPLATE_PATH = path.join(CC_TOOL_DIR, 'claude-request-template.json');
|
|
126
|
+
const CLAUDE_TEMPLATE_MIN_SYSTEM_CHARS = 100;
|
|
127
|
+
|
|
128
|
+
const FALLBACK_CLAUDE_SYSTEM = Object.freeze([
|
|
129
|
+
{
|
|
130
|
+
type: 'text',
|
|
131
|
+
text: 'x-anthropic-billing-header: cc_version=2.1.59; cc_entrypoint=cli; cch=00000;'
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
type: 'text',
|
|
135
|
+
text: "You are Claude Code, Anthropic's official CLI for Claude."
|
|
136
|
+
}
|
|
137
|
+
]);
|
|
138
|
+
|
|
139
|
+
const FALLBACK_CLAUDE_TOOLS = Object.freeze([
|
|
140
|
+
{
|
|
141
|
+
name: 'Task',
|
|
142
|
+
description: 'Launch a new agent to handle complex, multi-step tasks autonomously.',
|
|
143
|
+
input_schema: {
|
|
144
|
+
type: 'object',
|
|
145
|
+
properties: {
|
|
146
|
+
description: { type: 'string' },
|
|
147
|
+
prompt: { type: 'string' },
|
|
148
|
+
subagent_type: { type: 'string' },
|
|
149
|
+
model: { type: 'string' },
|
|
150
|
+
resume: { type: 'string' },
|
|
151
|
+
run_in_background: { type: 'boolean' },
|
|
152
|
+
max_turns: { type: 'integer' },
|
|
153
|
+
isolation: { type: 'string' }
|
|
154
|
+
},
|
|
155
|
+
required: ['description', 'prompt', 'subagent_type']
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
name: 'Bash',
|
|
160
|
+
description: 'Executes a given bash command and returns its output.',
|
|
161
|
+
input_schema: {
|
|
162
|
+
type: 'object',
|
|
163
|
+
properties: {
|
|
164
|
+
command: { type: 'string' },
|
|
165
|
+
timeout: { type: 'number' },
|
|
166
|
+
description: { type: 'string' },
|
|
167
|
+
run_in_background: { type: 'boolean' },
|
|
168
|
+
dangerouslyDisableSandbox: { type: 'boolean' }
|
|
169
|
+
},
|
|
170
|
+
required: ['command']
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
name: 'Glob',
|
|
175
|
+
description: 'Fast file pattern matching tool that works with any codebase size.',
|
|
176
|
+
input_schema: {
|
|
177
|
+
type: 'object',
|
|
178
|
+
properties: {
|
|
179
|
+
pattern: { type: 'string' },
|
|
180
|
+
path: { type: 'string' }
|
|
181
|
+
},
|
|
182
|
+
required: ['pattern']
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
name: 'Grep',
|
|
187
|
+
description: 'A powerful search tool built on ripgrep.',
|
|
188
|
+
input_schema: {
|
|
189
|
+
type: 'object',
|
|
190
|
+
properties: {
|
|
191
|
+
pattern: { type: 'string' },
|
|
192
|
+
path: { type: 'string' },
|
|
193
|
+
glob: { type: 'string' },
|
|
194
|
+
output_mode: { type: 'string' },
|
|
195
|
+
'-B': { type: 'number' },
|
|
196
|
+
'-A': { type: 'number' },
|
|
197
|
+
'-C': { type: 'number' },
|
|
198
|
+
context: { type: 'number' },
|
|
199
|
+
'-n': { type: 'boolean' },
|
|
200
|
+
'-i': { type: 'boolean' },
|
|
201
|
+
type: { type: 'string' },
|
|
202
|
+
head_limit: { type: 'number' },
|
|
203
|
+
offset: { type: 'number' },
|
|
204
|
+
multiline: { type: 'boolean' }
|
|
205
|
+
},
|
|
206
|
+
required: ['pattern']
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
name: 'Read',
|
|
211
|
+
description: 'Reads a file from the local filesystem.',
|
|
212
|
+
input_schema: {
|
|
213
|
+
type: 'object',
|
|
214
|
+
properties: {
|
|
215
|
+
file_path: { type: 'string' },
|
|
216
|
+
offset: { type: 'number' },
|
|
217
|
+
limit: { type: 'number' },
|
|
218
|
+
pages: { type: 'string' }
|
|
219
|
+
},
|
|
220
|
+
required: ['file_path']
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
name: 'Edit',
|
|
225
|
+
description: 'Performs exact string replacements in files.',
|
|
226
|
+
input_schema: {
|
|
227
|
+
type: 'object',
|
|
228
|
+
properties: {
|
|
229
|
+
file_path: { type: 'string' },
|
|
230
|
+
old_string: { type: 'string' },
|
|
231
|
+
new_string: { type: 'string' },
|
|
232
|
+
replace_all: { type: 'boolean' }
|
|
233
|
+
},
|
|
234
|
+
required: ['file_path', 'old_string', 'new_string']
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
name: 'Write',
|
|
239
|
+
description: 'Writes a file to the local filesystem.',
|
|
240
|
+
input_schema: {
|
|
241
|
+
type: 'object',
|
|
242
|
+
properties: {
|
|
243
|
+
file_path: { type: 'string' },
|
|
244
|
+
content: { type: 'string' }
|
|
245
|
+
},
|
|
246
|
+
required: ['file_path', 'content']
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
name: 'ToolSearch',
|
|
251
|
+
description: 'Search for or select deferred tools to make them available for use.',
|
|
252
|
+
input_schema: {
|
|
253
|
+
type: 'object',
|
|
254
|
+
properties: {
|
|
255
|
+
query: { type: 'string' },
|
|
256
|
+
max_results: { type: 'number' }
|
|
257
|
+
},
|
|
258
|
+
required: ['query', 'max_results']
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
]);
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* 从 Claude 请求 body 提取模板(system/tools/userId),覆盖写入单一文件。
|
|
265
|
+
* 无需环境变量开关,默认始终执行(文件极小,仅保留最新一次有效模板)。
|
|
266
|
+
* @param {object} body - 请求 body 对象
|
|
267
|
+
*/
|
|
268
|
+
function persistClaudeRequestTemplate(body) {
|
|
269
|
+
if (!body || typeof body !== 'object') return;
|
|
270
|
+
|
|
271
|
+
const system = Array.isArray(body.system) ? body.system : [];
|
|
272
|
+
const tools = Array.isArray(body.tools) ? body.tools : [];
|
|
273
|
+
const userId = (body.metadata && typeof body.metadata.user_id === 'string')
|
|
274
|
+
? body.metadata.user_id
|
|
275
|
+
: '';
|
|
276
|
+
|
|
277
|
+
// 必须有 tools 且 system 有一定内容才算有效模板
|
|
278
|
+
if (tools.length === 0) return;
|
|
279
|
+
const systemCharCount = system.reduce((s, b) => s + (typeof b?.text === 'string' ? b.text.length : 0), 0);
|
|
280
|
+
if (systemCharCount < CLAUDE_TEMPLATE_MIN_SYSTEM_CHARS) return;
|
|
281
|
+
|
|
282
|
+
try {
|
|
283
|
+
ensureDir(CC_TOOL_DIR);
|
|
284
|
+
const template = { updatedAt: Date.now(), userId, system, tools };
|
|
285
|
+
fs.writeFile(CLAUDE_TEMPLATE_PATH, JSON.stringify(template), (err) => {
|
|
286
|
+
if (err) console.error('[request-logger] Failed to write claude-request-template.json:', err);
|
|
287
|
+
});
|
|
288
|
+
} catch (err) {
|
|
289
|
+
console.error('[request-logger] Failed to write claude-request-template.json:', err);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* 读取 Claude 请求模板(同步)。
|
|
295
|
+
* 若文件不存在或无效,返回内置 fallback(含 billing header 占位符和完整工具 schema)。
|
|
296
|
+
* @returns {{ userId: string, system: Array, tools: Array }}
|
|
297
|
+
*/
|
|
298
|
+
function loadClaudeRequestTemplate() {
|
|
299
|
+
try {
|
|
300
|
+
if (fs.existsSync(CLAUDE_TEMPLATE_PATH)) {
|
|
301
|
+
const raw = fs.readFileSync(CLAUDE_TEMPLATE_PATH, 'utf8');
|
|
302
|
+
const parsed = JSON.parse(raw);
|
|
303
|
+
if (Array.isArray(parsed.tools) && parsed.tools.length > 0 && Array.isArray(parsed.system)) {
|
|
304
|
+
return { userId: parsed.userId || '', system: parsed.system, tools: parsed.tools };
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
} catch {
|
|
308
|
+
// fall through to fallback
|
|
309
|
+
}
|
|
310
|
+
return { userId: '', system: [...FALLBACK_CLAUDE_SYSTEM], tools: [...FALLBACK_CLAUDE_TOOLS] };
|
|
311
|
+
}
|
|
312
|
+
|
|
125
313
|
module.exports = {
|
|
126
314
|
isProxyRequestLoggingEnabled,
|
|
127
315
|
isApiRequestLoggingEnabled,
|
|
128
316
|
persistProxyRequestSnapshot,
|
|
317
|
+
persistClaudeRequestTemplate,
|
|
318
|
+
loadClaudeRequestTemplate,
|
|
129
319
|
createApiRequestLogger
|
|
130
320
|
};
|
|
@@ -6,9 +6,6 @@
|
|
|
6
6
|
|
|
7
7
|
const https = require('https');
|
|
8
8
|
const http = require('http');
|
|
9
|
-
const fs = require('fs');
|
|
10
|
-
const os = require('os');
|
|
11
|
-
const path = require('path');
|
|
12
9
|
const crypto = require('crypto');
|
|
13
10
|
const { URL } = require('url');
|
|
14
11
|
const { probeModelAvailability } = require('./model-detector');
|
|
@@ -16,6 +13,7 @@ const { getEffectiveApiKey: getClaudeEffectiveApiKey } = require('./channels');
|
|
|
16
13
|
const { getEffectiveApiKey: getCodexEffectiveApiKey } = require('./codex-channels');
|
|
17
14
|
const { getEffectiveApiKey: getGeminiEffectiveApiKey } = require('./gemini-channels');
|
|
18
15
|
const { getEffectiveApiKey: getOpenCodeEffectiveApiKey } = require('./opencode-channels');
|
|
16
|
+
const { loadClaudeRequestTemplate } = require('./request-logger');
|
|
19
17
|
|
|
20
18
|
// 测试结果缓存
|
|
21
19
|
const testResultsCache = new Map();
|
|
@@ -35,16 +33,6 @@ const CLAUDE_ADVANCED_TOOL_USE_BETA = 'advanced-tool-use-2025-11-20';
|
|
|
35
33
|
const ROUTE_OR_METHOD_MISMATCH_STATUS = new Set([404, 405, 501]);
|
|
36
34
|
const CLAUDE_USER_ID_ACCOUNT_RE = /^user_([0-9a-f]{64})_account__session_[a-z0-9._-]+$/i;
|
|
37
35
|
const CLAUDE_USER_ID_FULL_RE = /^user_[0-9a-f]{64}_account__session_[a-z0-9._-]+$/i;
|
|
38
|
-
const DEFAULT_CLAUDE_CODE_TOOL_NAMES = Object.freeze([
|
|
39
|
-
'Task',
|
|
40
|
-
'Bash',
|
|
41
|
-
'Glob',
|
|
42
|
-
'Grep',
|
|
43
|
-
'Read',
|
|
44
|
-
'Edit',
|
|
45
|
-
'Write',
|
|
46
|
-
'ToolSearch'
|
|
47
|
-
]);
|
|
48
36
|
let cachedClaudeAccountId = '';
|
|
49
37
|
let cachedClaudeUserId = '';
|
|
50
38
|
|
|
@@ -195,96 +183,31 @@ function resolveClaudeAccountIdFromUserId(userId = '') {
|
|
|
195
183
|
}
|
|
196
184
|
|
|
197
185
|
function resolveClaudeAccountIdFromLogs() {
|
|
198
|
-
const logsPath = path.join(os.homedir(), '.cc-tool', 'claude-requests.jsonl');
|
|
199
|
-
if (!fs.existsSync(logsPath)) return '';
|
|
200
|
-
|
|
201
186
|
try {
|
|
202
|
-
const
|
|
203
|
-
const
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
207
|
-
const line = lines[index].trim();
|
|
208
|
-
if (!line) continue;
|
|
209
|
-
try {
|
|
210
|
-
const parsed = JSON.parse(line);
|
|
211
|
-
const userId = parsed?.request?.body?.metadata?.user_id;
|
|
212
|
-
const accountId = resolveClaudeAccountIdFromUserId(userId);
|
|
213
|
-
if (accountId) {
|
|
214
|
-
accountIdCount.set(accountId, (accountIdCount.get(accountId) || 0) + 1);
|
|
215
|
-
}
|
|
216
|
-
} catch {
|
|
217
|
-
// ignore malformed line
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
const ranked = Array.from(accountIdCount.entries())
|
|
222
|
-
.filter(([accountId]) => accountId !== '0'.repeat(64))
|
|
223
|
-
.sort((left, right) => right[1] - left[1]);
|
|
224
|
-
|
|
225
|
-
if (ranked.length > 0) {
|
|
226
|
-
return ranked[0][0];
|
|
227
|
-
}
|
|
187
|
+
const template = loadClaudeRequestTemplate();
|
|
188
|
+
const userId = template?.userId || '';
|
|
189
|
+
const accountId = resolveClaudeAccountIdFromUserId(userId);
|
|
190
|
+
return (accountId && accountId !== '0'.repeat(64)) ? accountId : '';
|
|
228
191
|
} catch {
|
|
229
|
-
|
|
192
|
+
return '';
|
|
230
193
|
}
|
|
231
|
-
|
|
232
|
-
return '';
|
|
233
194
|
}
|
|
234
195
|
|
|
235
196
|
function resolveClaudeUserIdFromLogs() {
|
|
236
|
-
const logsPath = path.join(os.homedir(), '.cc-tool', 'claude-requests.jsonl');
|
|
237
|
-
if (!fs.existsSync(logsPath)) return '';
|
|
238
|
-
|
|
239
197
|
try {
|
|
240
|
-
const
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
if (!line) continue;
|
|
247
|
-
try {
|
|
248
|
-
const parsed = JSON.parse(line);
|
|
249
|
-
const userId = normalizeNonEmptyString(parsed?.request?.body?.metadata?.user_id);
|
|
250
|
-
if (!userId || !CLAUDE_USER_ID_FULL_RE.test(userId)) continue;
|
|
251
|
-
const accountId = resolveClaudeAccountIdFromUserId(userId);
|
|
252
|
-
if (!accountId || accountId === '0'.repeat(64)) continue;
|
|
253
|
-
userIdCount.set(userId, (userIdCount.get(userId) || 0) + 1);
|
|
254
|
-
} catch {
|
|
255
|
-
// ignore malformed line
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
const ranked = Array.from(userIdCount.entries()).sort((left, right) => right[1] - left[1]);
|
|
260
|
-
return ranked.length > 0 ? ranked[0][0] : '';
|
|
198
|
+
const template = loadClaudeRequestTemplate();
|
|
199
|
+
const userId = normalizeNonEmptyString(template?.userId || '');
|
|
200
|
+
if (!userId || !CLAUDE_USER_ID_FULL_RE.test(userId)) return '';
|
|
201
|
+
const accountId = resolveClaudeAccountIdFromUserId(userId);
|
|
202
|
+
if (!accountId || accountId === '0'.repeat(64)) return '';
|
|
203
|
+
return userId;
|
|
261
204
|
} catch {
|
|
262
205
|
return '';
|
|
263
206
|
}
|
|
264
207
|
}
|
|
265
208
|
|
|
266
209
|
function resolveClaudeRequestTemplate() {
|
|
267
|
-
|
|
268
|
-
if (!fs.existsSync(logsPath)) return null;
|
|
269
|
-
try {
|
|
270
|
-
const content = fs.readFileSync(logsPath, 'utf8');
|
|
271
|
-
const lines = content.trim().split('\n');
|
|
272
|
-
for (let i = lines.length - 1; i >= 0; i--) {
|
|
273
|
-
const line = lines[i].trim();
|
|
274
|
-
if (!line) continue;
|
|
275
|
-
try {
|
|
276
|
-
const parsed = JSON.parse(line);
|
|
277
|
-
const body = parsed?.request?.body;
|
|
278
|
-
if (!body || typeof body !== 'object') continue;
|
|
279
|
-
const system = Array.isArray(body.system) ? body.system : [];
|
|
280
|
-
const tools = Array.isArray(body.tools) ? body.tools : [];
|
|
281
|
-
const hasBilling = system.some(b => typeof b?.text === 'string' && b.text.startsWith('x-anthropic-billing-header:'));
|
|
282
|
-
if (!hasBilling || tools.length === 0) continue;
|
|
283
|
-
return { system, tools };
|
|
284
|
-
} catch { }
|
|
285
|
-
}
|
|
286
|
-
} catch { }
|
|
287
|
-
return null;
|
|
210
|
+
return loadClaudeRequestTemplate();
|
|
288
211
|
}
|
|
289
212
|
|
|
290
213
|
function resolveClaudePreferredUserId() {
|
|
@@ -347,17 +270,6 @@ function buildClaudeCodeUserId() {
|
|
|
347
270
|
return `user_${accountId}_account__session_${sessionId}`;
|
|
348
271
|
}
|
|
349
272
|
|
|
350
|
-
function buildDefaultClaudeCodeTools() {
|
|
351
|
-
return DEFAULT_CLAUDE_CODE_TOOL_NAMES.map(name => ({
|
|
352
|
-
name,
|
|
353
|
-
description: `${name} tool`,
|
|
354
|
-
input_schema: {
|
|
355
|
-
type: 'object',
|
|
356
|
-
properties: {},
|
|
357
|
-
additionalProperties: true
|
|
358
|
-
}
|
|
359
|
-
}));
|
|
360
|
-
}
|
|
361
273
|
|
|
362
274
|
function buildGeminiNativeGeneratePath(parsedUrl, model) {
|
|
363
275
|
let pathname = parsedUrl.pathname.replace(/\/+$/, '');
|
|
@@ -755,12 +667,8 @@ async function testAPIFunctionality(baseUrl, apiKey, timeout, channelType = 'cla
|
|
|
755
667
|
: `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;
|
|
756
668
|
const userId = buildClaudeCodeUserId() || `user_${'0'.repeat(64)}_account__session_${sessionId}`;
|
|
757
669
|
const template = resolveClaudeRequestTemplate();
|
|
758
|
-
const systemBlocks = template
|
|
759
|
-
|
|
760
|
-
: [{ type: 'text', text: "You are Claude Code, Anthropic's official CLI for Claude." }];
|
|
761
|
-
const toolsToUse = template?.tools?.length > 0
|
|
762
|
-
? template.tools
|
|
763
|
-
: buildDefaultClaudeCodeTools();
|
|
670
|
+
const systemBlocks = template.system;
|
|
671
|
+
const toolsToUse = template.tools;
|
|
764
672
|
const requestPayload = {
|
|
765
673
|
model: testModel,
|
|
766
674
|
max_tokens: 10,
|
|
@@ -868,6 +776,7 @@ async function testAPIFunctionality(baseUrl, apiKey, timeout, channelType = 'cla
|
|
|
868
776
|
primaryRequestConfig = useCliFormat ? cliRequestConfig : nativeRequestConfig;
|
|
869
777
|
fallbackRequestConfig = useCliFormat ? nativeRequestConfig : cliRequestConfig;
|
|
870
778
|
} else {
|
|
779
|
+
testModel = modelProbe?.preferredTestModel || normalizeNonEmptyString(model) || 'gpt-4o-mini';
|
|
871
780
|
let apiPath = parsedUrl.pathname.replace(/\/$/, '');
|
|
872
781
|
if (!apiPath.endsWith('/chat/completions')) {
|
|
873
782
|
apiPath = apiPath + (apiPath.endsWith('/v1') ? '/chat/completions' : '/v1/chat/completions');
|
|
@@ -875,7 +784,7 @@ async function testAPIFunctionality(baseUrl, apiKey, timeout, channelType = 'cla
|
|
|
875
784
|
primaryRequestConfig = {
|
|
876
785
|
apiPath,
|
|
877
786
|
requestBody: JSON.stringify({
|
|
878
|
-
model:
|
|
787
|
+
model: testModel,
|
|
879
788
|
max_tokens: 1,
|
|
880
789
|
messages: [{ role: 'user', content: 'Hi' }]
|
|
881
790
|
}),
|
package/src/utils/port-helper.js
CHANGED
|
@@ -40,17 +40,33 @@ function isPortInUse(port, host = '127.0.0.1') {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
|
-
* 查找占用端口的进程PID
|
|
43
|
+
* 查找占用端口的进程PID(跨平台)
|
|
44
44
|
*/
|
|
45
45
|
function findProcessByPort(port) {
|
|
46
|
+
const isWindows = process.platform === 'win32';
|
|
47
|
+
if (isWindows) {
|
|
48
|
+
try {
|
|
49
|
+
// Windows: netstat -ano 列出所有连接,findstr 过滤端口
|
|
50
|
+
const result = execSync(`netstat -ano | findstr ":${port} "`, { encoding: 'utf-8' });
|
|
51
|
+
const pids = new Set();
|
|
52
|
+
result.split('\n').forEach(line => {
|
|
53
|
+
// 格式: " TCP 0.0.0.0:9999 0.0.0.0:0 LISTENING 1234"
|
|
54
|
+
const match = line.trim().match(/\s+(\d+)\s*$/);
|
|
55
|
+
if (match) pids.add(match[1]);
|
|
56
|
+
});
|
|
57
|
+
return Array.from(pids).filter(pid => pid && pid !== '0');
|
|
58
|
+
} catch (e) {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
46
63
|
try {
|
|
47
64
|
// macOS/Linux 使用 lsof
|
|
48
65
|
const result = execSync(`lsof -ti :${port}`, { encoding: 'utf-8' }).trim();
|
|
49
66
|
return result.split('\n').filter(pid => pid);
|
|
50
67
|
} catch (err) {
|
|
51
|
-
// 如果 lsof
|
|
68
|
+
// 如果 lsof 失败,尝试使用 fuser(某些 Linux 系统)
|
|
52
69
|
try {
|
|
53
|
-
// 适用于某些 Linux 系统
|
|
54
70
|
const result = execSync(`fuser ${port}/tcp 2>/dev/null`, { encoding: 'utf-8' }).trim();
|
|
55
71
|
return result.split(/\s+/).filter(pid => pid);
|
|
56
72
|
} catch (e) {
|
|
@@ -60,7 +76,7 @@ function findProcessByPort(port) {
|
|
|
60
76
|
}
|
|
61
77
|
|
|
62
78
|
/**
|
|
63
|
-
*
|
|
79
|
+
* 杀掉占用端口的进程(跨平台)
|
|
64
80
|
*/
|
|
65
81
|
function killProcessByPort(port) {
|
|
66
82
|
try {
|
|
@@ -69,9 +85,14 @@ function killProcessByPort(port) {
|
|
|
69
85
|
return false;
|
|
70
86
|
}
|
|
71
87
|
|
|
88
|
+
const isWindows = process.platform === 'win32';
|
|
72
89
|
pids.forEach(pid => {
|
|
73
90
|
try {
|
|
74
|
-
|
|
91
|
+
if (isWindows) {
|
|
92
|
+
execSync(`taskkill /F /PID ${pid}`, { stdio: 'ignore' });
|
|
93
|
+
} else {
|
|
94
|
+
execSync(`kill -9 ${pid}`, { stdio: 'ignore' });
|
|
95
|
+
}
|
|
75
96
|
} catch (err) {
|
|
76
97
|
// 忽略单个进程杀掉失败的错误
|
|
77
98
|
}
|