coding-tool-x 3.3.3 → 3.3.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/dist/web/assets/{Analytics-DtR00OYP.js → Analytics-B6CWdkhx.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-DWiSFOp5.js → ConfigTemplates-BW6LEgd8.js} +1 -1
- package/dist/web/assets/{Home-DUu2mGb6.js → Home-B2B2gS2-.js} +1 -1
- package/dist/web/assets/{PluginManager-DsJ1KtNr.js → PluginManager-Bqc7ldY-.js} +1 -1
- package/dist/web/assets/{ProjectList-CzTJaBJb.js → ProjectList-BFdZZm_8.js} +1 -1
- package/dist/web/assets/{SessionList-D1ovPZ0I.js → SessionList-B_Tp37kM.js} +1 -1
- package/dist/web/assets/{SkillManager-DqpDTc2c.js → SkillManager-ul2rcS3o.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-Dj28-3G5.js → WorkspaceManager-Dp5Jvdtu.js} +1 -1
- package/dist/web/assets/index-CSBDZxYn.js +2 -0
- package/dist/web/assets/index-DxRneGyu.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +2 -1
- package/src/commands/doctor.js +3 -3
- package/src/commands/export-config.js +2 -2
- package/src/commands/logs.js +42 -9
- package/src/commands/port-config.js +2 -2
- package/src/commands/switch.js +2 -2
- package/src/config/default.js +4 -1
- package/src/config/loader.js +5 -2
- package/src/config/paths.js +25 -21
- package/src/reset-config.js +3 -5
- package/src/server/api/agents.js +2 -3
- package/src/server/api/claude-hooks.js +92 -12
- package/src/server/api/codex-sessions.js +6 -5
- package/src/server/api/opencode-sessions.js +2 -2
- package/src/server/api/pm2-autostart.js +20 -8
- package/src/server/api/proxy.js +2 -3
- package/src/server/api/sessions.js +42 -12
- package/src/server/index.js +5 -9
- package/src/server/opencode-proxy-server.js +11 -1
- package/src/server/services/agents-service.js +6 -3
- package/src/server/services/channels.js +6 -7
- package/src/server/services/codex-channels.js +4 -1
- package/src/server/services/codex-config.js +4 -1
- package/src/server/services/codex-parser.js +31 -4
- package/src/server/services/codex-settings-manager.js +18 -9
- package/src/server/services/commands-service.js +2 -2
- package/src/server/services/config-export-service.js +7 -6
- package/src/server/services/config-registry-service.js +7 -6
- package/src/server/services/config-sync-manager.js +2 -2
- package/src/server/services/config-sync-service.js +2 -2
- package/src/server/services/env-checker.js +2 -2
- package/src/server/services/favorites.js +3 -4
- package/src/server/services/gemini-channels.js +4 -4
- package/src/server/services/gemini-config.js +2 -2
- package/src/server/services/gemini-sessions.js +3 -3
- package/src/server/services/gemini-settings-manager.js +5 -5
- package/src/server/services/mcp-service.js +7 -4
- package/src/server/services/model-detector.js +2 -2
- package/src/server/services/opencode-channels.js +5 -5
- package/src/server/services/opencode-sessions.js +28 -3
- package/src/server/services/plugins-service.js +3 -4
- package/src/server/services/prompts-service.js +7 -4
- package/src/server/services/proxy-runtime.js +2 -2
- package/src/server/services/repo-scanner-base.js +2 -2
- package/src/server/services/request-logger.js +2 -2
- package/src/server/services/security-config.js +2 -2
- package/src/server/services/session-cache.js +2 -2
- package/src/server/services/session-converter.js +9 -4
- package/src/server/services/sessions.js +8 -5
- package/src/server/services/settings-manager.js +3 -4
- package/src/server/services/skill-service.js +5 -5
- package/src/server/services/statistics-service.js +2 -2
- package/src/server/services/ui-config.js +3 -4
- package/src/server/websocket-server.js +2 -2
- package/src/utils/home-dir.js +82 -0
- package/src/utils/port-helper.js +34 -12
- package/dist/web/assets/index-CaKktouI.js +0 -2
- package/dist/web/assets/index-DZjDFGqR.css +0 -1
package/src/config/paths.js
CHANGED
|
@@ -3,16 +3,19 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const os = require('os');
|
|
6
|
+
const { resolvePreferredHomeDir } = require('../utils/home-dir');
|
|
7
|
+
|
|
8
|
+
const HOME_DIR = resolvePreferredHomeDir(process.platform, process.env, os.homedir());
|
|
6
9
|
|
|
7
10
|
// 基础目录
|
|
8
|
-
const CC_TOOL_BASE_DIR = path.join(
|
|
11
|
+
const CC_TOOL_BASE_DIR = path.join(HOME_DIR, '.cc-tool');
|
|
9
12
|
// 兼容旧变量名,避免外部调用方断裂
|
|
10
13
|
const CTX_BASE_DIR = CC_TOOL_BASE_DIR;
|
|
11
14
|
|
|
12
15
|
// 旧目录(升级时自动合并到 ~/.cc-tool)
|
|
13
16
|
const LEGACY_BASE_DIRS = [
|
|
14
|
-
path.join(
|
|
15
|
-
path.join(
|
|
17
|
+
path.join(HOME_DIR, '.claude', 'ctx'),
|
|
18
|
+
path.join(HOME_DIR, '.claude', 'cc-tool')
|
|
16
19
|
];
|
|
17
20
|
|
|
18
21
|
let migrationChecked = false;
|
|
@@ -127,7 +130,7 @@ const PATHS = {
|
|
|
127
130
|
notifyHook: path.join(CC_TOOL_BASE_DIR, 'notify-hook.js'),
|
|
128
131
|
|
|
129
132
|
// Skills 安装目录(注意:这个仍使用 Claude 原生路径)
|
|
130
|
-
skills: path.join(
|
|
133
|
+
skills: path.join(HOME_DIR, '.claude', 'skills'),
|
|
131
134
|
|
|
132
135
|
// MCP 配置(注意:这个仍使用 Claude 原生路径)
|
|
133
136
|
mcpConfig: path.join(CC_TOOL_BASE_DIR, 'mcp-config.json'),
|
|
@@ -148,41 +151,42 @@ const PATHS = {
|
|
|
148
151
|
const NATIVE_PATHS = {
|
|
149
152
|
// Claude Code 原生配置
|
|
150
153
|
claude: {
|
|
151
|
-
settings: path.join(
|
|
152
|
-
settingsBackup: path.join(
|
|
153
|
-
projects: path.join(
|
|
154
|
+
settings: path.join(HOME_DIR, '.claude', 'settings.json'),
|
|
155
|
+
settingsBackup: path.join(HOME_DIR, '.claude', 'settings.json.cc-tool-backup'),
|
|
156
|
+
projects: path.join(HOME_DIR, '.claude', 'projects')
|
|
154
157
|
},
|
|
155
158
|
|
|
156
159
|
// Codex 原生配置
|
|
157
160
|
codex: {
|
|
158
|
-
config: path.join(
|
|
159
|
-
configBackup: path.join(
|
|
160
|
-
auth: path.join(
|
|
161
|
-
authBackup: path.join(
|
|
162
|
-
sessions: path.join(
|
|
161
|
+
config: path.join(HOME_DIR, '.codex', 'config.toml'),
|
|
162
|
+
configBackup: path.join(HOME_DIR, '.codex', 'config.toml.cc-tool-backup'),
|
|
163
|
+
auth: path.join(HOME_DIR, '.codex', 'auth.json'),
|
|
164
|
+
authBackup: path.join(HOME_DIR, '.codex', 'auth.json.cc-tool-backup'),
|
|
165
|
+
sessions: path.join(HOME_DIR, '.codex', 'sessions')
|
|
163
166
|
},
|
|
164
167
|
|
|
165
168
|
// Gemini 原生配置
|
|
166
169
|
gemini: {
|
|
167
|
-
env: path.join(
|
|
168
|
-
envBackup: path.join(
|
|
169
|
-
tmp: path.join(
|
|
170
|
+
env: path.join(HOME_DIR, '.gemini', '.env'),
|
|
171
|
+
envBackup: path.join(HOME_DIR, '.gemini', '.env.cc-tool-backup'),
|
|
172
|
+
tmp: path.join(HOME_DIR, '.gemini', 'tmp')
|
|
170
173
|
},
|
|
171
174
|
|
|
172
175
|
// OpenCode 原生配置
|
|
173
176
|
opencode: {
|
|
174
|
-
data: path.join(
|
|
175
|
-
config: path.join(
|
|
176
|
-
sessions: path.join(
|
|
177
|
-
projects: path.join(
|
|
178
|
-
messages: path.join(
|
|
179
|
-
log: path.join(
|
|
177
|
+
data: path.join(HOME_DIR, '.local', 'share', 'opencode'),
|
|
178
|
+
config: path.join(HOME_DIR, '.config', 'opencode'),
|
|
179
|
+
sessions: path.join(HOME_DIR, '.local', 'share', 'opencode', 'storage', 'session'),
|
|
180
|
+
projects: path.join(HOME_DIR, '.local', 'share', 'opencode', 'storage', 'project'),
|
|
181
|
+
messages: path.join(HOME_DIR, '.local', 'share', 'opencode', 'storage', 'message'),
|
|
182
|
+
log: path.join(HOME_DIR, '.local', 'share', 'opencode', 'log')
|
|
180
183
|
}
|
|
181
184
|
};
|
|
182
185
|
|
|
183
186
|
module.exports = {
|
|
184
187
|
PATHS,
|
|
185
188
|
NATIVE_PATHS,
|
|
189
|
+
HOME_DIR,
|
|
186
190
|
CTX_BASE_DIR,
|
|
187
191
|
CC_TOOL_BASE_DIR,
|
|
188
192
|
LEGACY_BASE_DIRS,
|
package/src/reset-config.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
|
-
const
|
|
3
|
-
const os = require('os');
|
|
4
|
-
const { PATHS, ensureStorageDirMigrated } = require('./config/paths');
|
|
2
|
+
const { PATHS, NATIVE_PATHS, ensureStorageDirMigrated } = require('./config/paths');
|
|
5
3
|
|
|
6
4
|
// 恢复配置到默认状态
|
|
7
5
|
async function resetConfig() {
|
|
@@ -25,8 +23,8 @@ async function resetConfig() {
|
|
|
25
23
|
}
|
|
26
24
|
|
|
27
25
|
// 2. 检查并恢复 settings.json
|
|
28
|
-
const settingsPath =
|
|
29
|
-
const backupPath =
|
|
26
|
+
const settingsPath = NATIVE_PATHS.claude.settings;
|
|
27
|
+
const backupPath = NATIVE_PATHS.claude.settingsBackup;
|
|
30
28
|
|
|
31
29
|
if (fs.existsSync(backupPath)) {
|
|
32
30
|
console.log('发现备份文件,正在恢复...');
|
package/src/server/api/agents.js
CHANGED
|
@@ -6,15 +6,14 @@
|
|
|
6
6
|
|
|
7
7
|
const express = require('express');
|
|
8
8
|
const fs = require('fs');
|
|
9
|
-
const os = require('os');
|
|
10
9
|
const path = require('path');
|
|
11
10
|
const { AgentsService } = require('../services/agents-service');
|
|
12
|
-
const { PATHS } = require('../../config/paths');
|
|
11
|
+
const { PATHS, HOME_DIR } = require('../../config/paths');
|
|
13
12
|
|
|
14
13
|
const router = express.Router();
|
|
15
14
|
const SUPPORTED_PLATFORMS = ['claude', 'codex', 'opencode'];
|
|
16
15
|
const agentServices = new Map();
|
|
17
|
-
const DEFAULT_PROJECT_ALLOWED_ROOTS = [
|
|
16
|
+
const DEFAULT_PROJECT_ALLOWED_ROOTS = [HOME_DIR, process.cwd()];
|
|
18
17
|
|
|
19
18
|
function isSupportedPlatform(platform) {
|
|
20
19
|
return SUPPORTED_PLATFORMS.includes(platform);
|
|
@@ -5,18 +5,21 @@ const path = require('path');
|
|
|
5
5
|
const os = require('os');
|
|
6
6
|
const https = require('https');
|
|
7
7
|
const http = require('http');
|
|
8
|
+
const { resolvePreferredHomeDir, normalizeWindowsHomePath } = require('../../utils/home-dir');
|
|
9
|
+
|
|
10
|
+
// 检测操作系统
|
|
11
|
+
const platform = os.platform(); // 'darwin' | 'win32' | 'linux'
|
|
12
|
+
|
|
13
|
+
const HOME_DIR = resolvePreferredHomeDir(platform, process.env, os.homedir());
|
|
8
14
|
|
|
9
15
|
// Claude settings.json 路径
|
|
10
|
-
const CLAUDE_SETTINGS_PATH = path.join(
|
|
16
|
+
const CLAUDE_SETTINGS_PATH = path.join(HOME_DIR, '.claude', 'settings.json');
|
|
11
17
|
|
|
12
18
|
// UI 配置路径(记录用户是否主动关闭过、飞书配置等)
|
|
13
|
-
const UI_CONFIG_PATH = path.join(
|
|
19
|
+
const UI_CONFIG_PATH = path.join(HOME_DIR, '.cc-tool', 'ui-config.json');
|
|
14
20
|
|
|
15
21
|
// 通知脚本路径(用于飞书通知)
|
|
16
|
-
const NOTIFY_SCRIPT_PATH = path.join(
|
|
17
|
-
|
|
18
|
-
// 检测操作系统
|
|
19
|
-
const platform = os.platform(); // 'darwin' | 'win32' | 'linux'
|
|
22
|
+
const NOTIFY_SCRIPT_PATH = path.join(HOME_DIR, '.cc-tool', 'notify-hook.js');
|
|
20
23
|
|
|
21
24
|
// 读取 Claude settings.json
|
|
22
25
|
function readClaudeSettings() {
|
|
@@ -91,7 +94,7 @@ function generateSystemNotificationCommand(type) {
|
|
|
91
94
|
if (type === 'dialog') {
|
|
92
95
|
return `powershell -Command "Add-Type -AssemblyName PresentationFramework; [System.Windows.MessageBox]::Show('Claude Code 任务已完成 | 等待交互', 'Coding Tool', 'OK', 'Information')"`;
|
|
93
96
|
} else {
|
|
94
|
-
return `powershell -Command "$wshell = New-Object -ComObject Wscript.Shell; $wshell.Popup('任务已完成 | 等待交互', 5, 'Coding Tool', 0x40)"`;
|
|
97
|
+
return `powershell -NoProfile -Command "try { [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] > $null; [Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] > $null; $xml = New-Object Windows.Data.Xml.Dom.XmlDocument; $xml.LoadXml('<toast><visual><binding template=\\"ToastGeneric\\"><text>Coding Tool</text><text>任务已完成 | 等待交互</text></binding></visual><audio src=\\"ms-winsoundevent:Notification.Default\\"/></toast>'); $toast = [Windows.UI.Notifications.ToastNotification]::new($xml); [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('Coding Tool').Show($toast) } catch { $wshell = New-Object -ComObject Wscript.Shell; $wshell.Popup('任务已完成 | 等待交互', 5, 'Coding Tool', 0x40) }"`;
|
|
95
98
|
}
|
|
96
99
|
} else {
|
|
97
100
|
// Linux
|
|
@@ -192,6 +195,52 @@ try {
|
|
|
192
195
|
return script;
|
|
193
196
|
}
|
|
194
197
|
|
|
198
|
+
function parseNotifyTypeMarker(command) {
|
|
199
|
+
const marker = command.match(/--cc-notify-type=(['"])?(dialog|notification)\1/i);
|
|
200
|
+
return marker ? marker[2].toLowerCase() : null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function getStopHookCommand(settings) {
|
|
204
|
+
const hooks = settings?.hooks?.Stop;
|
|
205
|
+
if (!Array.isArray(hooks) || hooks.length === 0) {
|
|
206
|
+
return '';
|
|
207
|
+
}
|
|
208
|
+
const firstHook = hooks[0]?.hooks;
|
|
209
|
+
if (!Array.isArray(firstHook) || firstHook.length === 0) {
|
|
210
|
+
return '';
|
|
211
|
+
}
|
|
212
|
+
return firstHook[0]?.command || '';
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function normalizePathForCompare(rawPath) {
|
|
216
|
+
return String(rawPath || '').replace(/\\/g, '/');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function shouldRepairStopHook(settings, expectedScriptPath = NOTIFY_SCRIPT_PATH, fileExists = fs.existsSync) {
|
|
220
|
+
const command = getStopHookCommand(settings);
|
|
221
|
+
if (!command || !command.includes('notify-hook.js')) {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const markerType = parseNotifyTypeMarker(command);
|
|
226
|
+
if (!markerType) {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const normalizedCommand = normalizePathForCompare(command);
|
|
231
|
+
const normalizedExpected = normalizePathForCompare(expectedScriptPath);
|
|
232
|
+
if (!normalizedCommand.includes(normalizedExpected)) {
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return !fileExists(expectedScriptPath);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function buildStopHookCommand(type) {
|
|
240
|
+
const notifyType = type === 'dialog' ? 'dialog' : 'notification';
|
|
241
|
+
return `node "${NOTIFY_SCRIPT_PATH}" --cc-notify-type=${notifyType}`;
|
|
242
|
+
}
|
|
243
|
+
|
|
195
244
|
// 写入通知脚本
|
|
196
245
|
function writeNotifyScript(config) {
|
|
197
246
|
try {
|
|
@@ -222,6 +271,11 @@ function parseStopHookStatus(settings) {
|
|
|
222
271
|
}
|
|
223
272
|
|
|
224
273
|
const command = stopHook.hooks[0].command || '';
|
|
274
|
+
const markerType = parseNotifyTypeMarker(command);
|
|
275
|
+
|
|
276
|
+
if (markerType) {
|
|
277
|
+
return { enabled: true, type: markerType };
|
|
278
|
+
}
|
|
225
279
|
|
|
226
280
|
// 判断通知类型(跨平台检测)
|
|
227
281
|
const isDialog = command.includes('display dialog') ||
|
|
@@ -229,7 +283,9 @@ function parseStopHookStatus(settings) {
|
|
|
229
283
|
command.includes('zenity --info');
|
|
230
284
|
const isNotification = command.includes('display notification') ||
|
|
231
285
|
command.includes('Popup') ||
|
|
232
|
-
command.includes('notify-send')
|
|
286
|
+
command.includes('notify-send') ||
|
|
287
|
+
command.includes('ToastNotificationManager') ||
|
|
288
|
+
command.includes('CreateToastNotifier');
|
|
233
289
|
|
|
234
290
|
// 检查是否是我们的通知脚本
|
|
235
291
|
const isOurScript = command.includes('notify-hook.js');
|
|
@@ -285,7 +341,9 @@ function updateStopHook(systemNotification, feishu) {
|
|
|
285
341
|
}
|
|
286
342
|
} else {
|
|
287
343
|
// 生成并写入通知脚本
|
|
288
|
-
writeNotifyScript({ systemNotification, feishu })
|
|
344
|
+
if (!writeNotifyScript({ systemNotification, feishu })) {
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
289
347
|
|
|
290
348
|
// 更新 Stop hook 指向通知脚本
|
|
291
349
|
settings.hooks = settings.hooks || {};
|
|
@@ -294,7 +352,7 @@ function updateStopHook(systemNotification, feishu) {
|
|
|
294
352
|
hooks: [
|
|
295
353
|
{
|
|
296
354
|
type: 'command',
|
|
297
|
-
command:
|
|
355
|
+
command: buildStopHookCommand(systemNotification?.type)
|
|
298
356
|
}
|
|
299
357
|
]
|
|
300
358
|
}
|
|
@@ -318,9 +376,22 @@ function initDefaultHooks() {
|
|
|
318
376
|
const settings = readClaudeSettings();
|
|
319
377
|
const currentStatus = parseStopHookStatus(settings);
|
|
320
378
|
|
|
321
|
-
// 如果已经有 Stop hook
|
|
379
|
+
// 如果已经有 Stop hook 配置,优先尝试自愈旧路径,再决定是否跳过
|
|
322
380
|
if (currentStatus.enabled) {
|
|
323
|
-
|
|
381
|
+
if (shouldRepairStopHook(settings)) {
|
|
382
|
+
const systemNotification = {
|
|
383
|
+
enabled: true,
|
|
384
|
+
type: currentStatus.type || 'notification'
|
|
385
|
+
};
|
|
386
|
+
const feishu = getFeishuConfig();
|
|
387
|
+
if (updateStopHook(systemNotification, feishu)) {
|
|
388
|
+
console.log('[Claude Hooks] 检测到旧版 Stop hook 路径,已自动修复');
|
|
389
|
+
} else {
|
|
390
|
+
console.warn('[Claude Hooks] Stop hook 路径修复失败,保留原配置');
|
|
391
|
+
}
|
|
392
|
+
} else {
|
|
393
|
+
console.log('[Claude Hooks] 已存在 Stop hook 配置,跳过初始化');
|
|
394
|
+
}
|
|
324
395
|
return;
|
|
325
396
|
}
|
|
326
397
|
|
|
@@ -478,3 +549,12 @@ router.post('/test', (req, res) => {
|
|
|
478
549
|
// 导出初始化函数供服务启动时调用
|
|
479
550
|
module.exports = router;
|
|
480
551
|
module.exports.initDefaultHooks = initDefaultHooks;
|
|
552
|
+
module.exports._test = {
|
|
553
|
+
generateSystemNotificationCommand,
|
|
554
|
+
parseStopHookStatus,
|
|
555
|
+
parseNotifyTypeMarker,
|
|
556
|
+
buildStopHookCommand,
|
|
557
|
+
normalizeWindowsHomePath,
|
|
558
|
+
resolvePreferredHomeDir,
|
|
559
|
+
shouldRepairStopHook
|
|
560
|
+
};
|
|
@@ -243,7 +243,7 @@ module.exports = (config) => {
|
|
|
243
243
|
type: 'assistant',
|
|
244
244
|
content: msg.content || '[空消息]',
|
|
245
245
|
timestamp: msg.timestamp,
|
|
246
|
-
model: session.provider || 'codex'
|
|
246
|
+
model: msg.model || session.provider || 'codex'
|
|
247
247
|
});
|
|
248
248
|
}
|
|
249
249
|
// 推理内容
|
|
@@ -252,7 +252,7 @@ module.exports = (config) => {
|
|
|
252
252
|
type: 'assistant',
|
|
253
253
|
content: `**[推理]**\n${msg.content || '[空推理]'}`,
|
|
254
254
|
timestamp: msg.timestamp,
|
|
255
|
-
model: session.provider || 'codex'
|
|
255
|
+
model: msg.model || session.provider || 'codex'
|
|
256
256
|
});
|
|
257
257
|
}
|
|
258
258
|
// 工具调用
|
|
@@ -264,7 +264,7 @@ module.exports = (config) => {
|
|
|
264
264
|
type: 'assistant',
|
|
265
265
|
content: `**[调用工具: ${msg.name}]**\n\`\`\`json\n${argsStr}\n\`\`\``,
|
|
266
266
|
timestamp: msg.timestamp,
|
|
267
|
-
model: session.provider || 'codex'
|
|
267
|
+
model: msg.model || session.provider || 'codex'
|
|
268
268
|
});
|
|
269
269
|
}
|
|
270
270
|
// 工具输出
|
|
@@ -284,10 +284,11 @@ module.exports = (config) => {
|
|
|
284
284
|
}
|
|
285
285
|
|
|
286
286
|
convertedMessages.push({
|
|
287
|
-
type: '
|
|
287
|
+
type: 'assistant',
|
|
288
|
+
subtype: 'tool_result',
|
|
288
289
|
content: `**[工具结果]**\n\`\`\`\n${outputStr}\n\`\`\``,
|
|
289
290
|
timestamp: msg.timestamp,
|
|
290
|
-
model:
|
|
291
|
+
model: msg.model || session.provider || 'codex'
|
|
291
292
|
});
|
|
292
293
|
}
|
|
293
294
|
}
|
|
@@ -14,7 +14,7 @@ const {
|
|
|
14
14
|
} = require('../services/opencode-sessions');
|
|
15
15
|
const { loadAliases } = require('../services/alias');
|
|
16
16
|
const { broadcastLog } = require('../websocket-server');
|
|
17
|
-
const
|
|
17
|
+
const { HOME_DIR } = require('../../config/paths');
|
|
18
18
|
|
|
19
19
|
function isNotFoundError(error) {
|
|
20
20
|
if (!error || !error.message) {
|
|
@@ -295,7 +295,7 @@ module.exports = (config) => {
|
|
|
295
295
|
|
|
296
296
|
const projects = getProjects();
|
|
297
297
|
const project = projects.find(p => p.name === projectName);
|
|
298
|
-
const cwd = session.directory || project?.fullPath ||
|
|
298
|
+
const cwd = session.directory || project?.fullPath || HOME_DIR;
|
|
299
299
|
const command = `opencode -r ${sessionId}`;
|
|
300
300
|
const quotedCwd = `"${String(cwd).replace(/"/g, '\\"')}"`;
|
|
301
301
|
const copyCommand = `cd ${quotedCwd} && ${command}`;
|
|
@@ -3,11 +3,18 @@ const { exec } = require('child_process');
|
|
|
3
3
|
const { promisify } = require('util');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const fs = require('fs');
|
|
6
|
-
const os = require('os');
|
|
7
6
|
const pm2 = require('pm2');
|
|
7
|
+
const { HOME_DIR } = require('../../config/paths');
|
|
8
8
|
|
|
9
9
|
const execAsync = promisify(exec);
|
|
10
10
|
|
|
11
|
+
function getExecOptions(timeout = 30000, runtimePlatform = process.platform) {
|
|
12
|
+
if (runtimePlatform === 'win32') {
|
|
13
|
+
return { timeout };
|
|
14
|
+
}
|
|
15
|
+
return { shell: '/bin/bash', timeout };
|
|
16
|
+
}
|
|
17
|
+
|
|
11
18
|
/**
|
|
12
19
|
* Check if PM2 autostart is enabled
|
|
13
20
|
* by looking for PM2 startup script in system
|
|
@@ -18,7 +25,7 @@ async function checkAutoStartStatus() {
|
|
|
18
25
|
|
|
19
26
|
if (platform === 'darwin') {
|
|
20
27
|
// macOS - check for LaunchDaemon
|
|
21
|
-
const launchDaemonsPath = path.join(
|
|
28
|
+
const launchDaemonsPath = path.join(HOME_DIR, 'Library/LaunchDaemons');
|
|
22
29
|
const pm2Files = fs.existsSync(launchDaemonsPath)
|
|
23
30
|
? fs.readdirSync(launchDaemonsPath).filter(f => f.includes('pm2'))
|
|
24
31
|
: [];
|
|
@@ -27,11 +34,11 @@ async function checkAutoStartStatus() {
|
|
|
27
34
|
} else if (platform === 'linux') {
|
|
28
35
|
// Linux - check for systemd service
|
|
29
36
|
const systemdPath = '/etc/systemd/system/pm2-root.service';
|
|
30
|
-
const userSystemdPath = path.join(
|
|
37
|
+
const userSystemdPath = path.join(HOME_DIR, '.config/systemd/user/pm2-*.service');
|
|
31
38
|
|
|
32
39
|
const rootExists = fs.existsSync(systemdPath);
|
|
33
|
-
const userExists = fs.existsSync(path.join(
|
|
34
|
-
fs.readdirSync(path.join(
|
|
40
|
+
const userExists = fs.existsSync(path.join(HOME_DIR, '.config/systemd/user')) &&
|
|
41
|
+
fs.readdirSync(path.join(HOME_DIR, '.config/systemd/user')).some(f => f.includes('pm2'));
|
|
35
42
|
|
|
36
43
|
return { enabled: rootExists || userExists, platform: 'linux' };
|
|
37
44
|
} else if (platform === 'win32') {
|
|
@@ -104,7 +111,7 @@ async function enableAutoStart() {
|
|
|
104
111
|
|
|
105
112
|
console.log(`Running startup command: ${command}`);
|
|
106
113
|
|
|
107
|
-
exec(command,
|
|
114
|
+
exec(command, getExecOptions(30000), (execErr, stdout, stderr) => {
|
|
108
115
|
pm2.disconnect();
|
|
109
116
|
|
|
110
117
|
if (execErr) {
|
|
@@ -163,7 +170,7 @@ async function disableAutoStart() {
|
|
|
163
170
|
|
|
164
171
|
console.log(`Running unstartup command: ${command}`);
|
|
165
172
|
|
|
166
|
-
exec(command,
|
|
173
|
+
exec(command, getExecOptions(30000), (execErr, stdout, stderr) => {
|
|
167
174
|
pm2.disconnect();
|
|
168
175
|
|
|
169
176
|
if (execErr) {
|
|
@@ -195,7 +202,7 @@ async function disableAutoStart() {
|
|
|
195
202
|
});
|
|
196
203
|
}
|
|
197
204
|
|
|
198
|
-
|
|
205
|
+
function createPm2AutostartRouter() {
|
|
199
206
|
const router = express.Router();
|
|
200
207
|
|
|
201
208
|
/**
|
|
@@ -266,4 +273,9 @@ module.exports = () => {
|
|
|
266
273
|
});
|
|
267
274
|
|
|
268
275
|
return router;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
module.exports = createPm2AutostartRouter;
|
|
279
|
+
module.exports._test = {
|
|
280
|
+
getExecOptions
|
|
269
281
|
};
|
package/src/server/api/proxy.js
CHANGED
|
@@ -12,10 +12,9 @@ const {
|
|
|
12
12
|
} = require('../services/settings-manager');
|
|
13
13
|
const { getAllChannels } = require('../services/channels');
|
|
14
14
|
const { clearAllLogs } = require('../websocket-server');
|
|
15
|
-
const { PATHS, ensureStorageDirMigrated } = require('../../config/paths');
|
|
15
|
+
const { PATHS, NATIVE_PATHS, ensureStorageDirMigrated } = require('../../config/paths');
|
|
16
16
|
const fs = require('fs');
|
|
17
17
|
const path = require('path');
|
|
18
|
-
const os = require('os');
|
|
19
18
|
|
|
20
19
|
function sanitizeChannelForResponse(channel) {
|
|
21
20
|
if (!channel) return null;
|
|
@@ -285,7 +284,7 @@ router.post('/stop', async (req, res) => {
|
|
|
285
284
|
|
|
286
285
|
// 3. 删除备份文件和active-channel.json
|
|
287
286
|
if (hasBackup()) {
|
|
288
|
-
const backupPath =
|
|
287
|
+
const backupPath = NATIVE_PATHS.claude.settingsBackup;
|
|
289
288
|
if (fs.existsSync(backupPath)) {
|
|
290
289
|
fs.unlinkSync(backupPath);
|
|
291
290
|
console.log('✅ Removed backup file');
|
|
@@ -2,11 +2,12 @@ const express = require('express');
|
|
|
2
2
|
const router = express.Router();
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const fs = require('fs');
|
|
5
|
-
const os = require('os');
|
|
6
5
|
const readline = require('readline');
|
|
7
6
|
const { getSessionsForProject, deleteSession, forkSession, saveSessionOrder, parseRealProjectPath, searchSessions, getRecentSessions, searchSessionsAcrossProjects, hasActualMessages } = require('../services/sessions');
|
|
8
7
|
const { loadAliases } = require('../services/alias');
|
|
9
8
|
const { broadcastLog } = require('../websocket-server');
|
|
9
|
+
const { NATIVE_PATHS } = require('../../config/paths');
|
|
10
|
+
const CLAUDE_PROJECTS_DIR = NATIVE_PATHS.claude.projects;
|
|
10
11
|
|
|
11
12
|
module.exports = (config) => {
|
|
12
13
|
// GET /api/sessions/search/global - Search sessions across all projects
|
|
@@ -120,12 +121,12 @@ module.exports = (config) => {
|
|
|
120
121
|
const year = now.getFullYear();
|
|
121
122
|
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
122
123
|
const day = String(now.getDate()).padStart(2, '0');
|
|
123
|
-
sessionDir = path.join(
|
|
124
|
+
sessionDir = path.join(NATIVE_PATHS.codex.sessions, String(year), month, day);
|
|
124
125
|
sessionFile = path.join(sessionDir, `${newSessionId}.jsonl`);
|
|
125
126
|
} else if (toolType === 'gemini') {
|
|
126
127
|
// Gemini: ~/.gemini/tmp/{hash}/chats/{sessionId}.json
|
|
127
128
|
const pathHash = crypto.createHash('sha256').update(fullPath).digest('hex');
|
|
128
|
-
sessionDir = path.join(
|
|
129
|
+
sessionDir = path.join(NATIVE_PATHS.gemini.tmp, pathHash, 'chats');
|
|
129
130
|
sessionFile = path.join(sessionDir, `${newSessionId}.json`);
|
|
130
131
|
} else {
|
|
131
132
|
return res.status(400).json({ error: 'Invalid toolType. Must be claude, codex, or gemini' });
|
|
@@ -243,7 +244,7 @@ module.exports = (config) => {
|
|
|
243
244
|
let sessionFile = null;
|
|
244
245
|
const possiblePaths = [
|
|
245
246
|
path.join(fullPath, '.claude', 'sessions', sessionId + '.jsonl'),
|
|
246
|
-
path.join(
|
|
247
|
+
path.join(CLAUDE_PROJECTS_DIR, projectName, sessionId + '.jsonl')
|
|
247
248
|
];
|
|
248
249
|
|
|
249
250
|
console.log(`[Messages API] Trying paths:`, possiblePaths);
|
|
@@ -279,6 +280,7 @@ module.exports = (config) => {
|
|
|
279
280
|
|
|
280
281
|
const stream = fs.createReadStream(sessionFile, { encoding: 'utf8' });
|
|
281
282
|
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
283
|
+
let lastAssistantModel = null;
|
|
282
284
|
|
|
283
285
|
try {
|
|
284
286
|
for await (const line of rl) {
|
|
@@ -297,31 +299,45 @@ module.exports = (config) => {
|
|
|
297
299
|
}
|
|
298
300
|
|
|
299
301
|
if (json.type === 'user' || json.type === 'assistant') {
|
|
302
|
+
const resolvedModel = json.message?.model || json.model || lastAssistantModel || null;
|
|
303
|
+
const messageId = json.message?.id || json.uuid || null;
|
|
300
304
|
const message = {
|
|
301
305
|
type: json.type,
|
|
302
306
|
content: null,
|
|
303
307
|
timestamp: json.timestamp || null,
|
|
304
|
-
model:
|
|
308
|
+
model: resolvedModel,
|
|
309
|
+
messageId,
|
|
310
|
+
subtype: null
|
|
305
311
|
};
|
|
312
|
+
let deferredToolResultContent = '';
|
|
306
313
|
|
|
307
314
|
if (json.type === 'user') {
|
|
308
315
|
if (typeof json.message?.content === 'string') {
|
|
309
316
|
message.content = json.message.content;
|
|
310
317
|
} else if (Array.isArray(json.message?.content)) {
|
|
311
|
-
const
|
|
318
|
+
const userParts = [];
|
|
319
|
+
const toolResultParts = [];
|
|
312
320
|
for (const item of json.message.content) {
|
|
313
321
|
if (item.type === 'text' && item.text) {
|
|
314
|
-
|
|
322
|
+
userParts.push(item.text);
|
|
315
323
|
} else if (item.type === 'tool_result') {
|
|
316
324
|
const resultContent = typeof item.content === 'string'
|
|
317
325
|
? item.content
|
|
318
326
|
: JSON.stringify(item.content, null, 2);
|
|
319
|
-
|
|
327
|
+
toolResultParts.push(`**[工具结果]**\n\`\`\`\n${resultContent}\n\`\`\``);
|
|
320
328
|
} else if (item.type === 'image') {
|
|
321
|
-
|
|
329
|
+
userParts.push('[图片]');
|
|
322
330
|
}
|
|
323
331
|
}
|
|
324
|
-
|
|
332
|
+
|
|
333
|
+
if (userParts.length > 0) {
|
|
334
|
+
message.content = userParts.join('\n\n');
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Claude tool_result is carried in a "user" envelope, but should be rendered as AI tool output.
|
|
338
|
+
if (toolResultParts.length > 0) {
|
|
339
|
+
deferredToolResultContent = toolResultParts.join('\n\n');
|
|
340
|
+
}
|
|
325
341
|
}
|
|
326
342
|
} else if (json.type === 'assistant') {
|
|
327
343
|
if (Array.isArray(json.message?.content)) {
|
|
@@ -340,11 +356,26 @@ module.exports = (config) => {
|
|
|
340
356
|
} else if (typeof json.message?.content === 'string') {
|
|
341
357
|
message.content = json.message.content;
|
|
342
358
|
}
|
|
359
|
+
|
|
360
|
+
if (message.model) {
|
|
361
|
+
lastAssistantModel = message.model;
|
|
362
|
+
}
|
|
343
363
|
}
|
|
344
364
|
|
|
345
365
|
if (message.content && message.content !== 'Warmup') {
|
|
346
366
|
allMessages.push(message);
|
|
347
367
|
}
|
|
368
|
+
|
|
369
|
+
if (deferredToolResultContent) {
|
|
370
|
+
allMessages.push({
|
|
371
|
+
type: 'assistant',
|
|
372
|
+
subtype: 'tool_result',
|
|
373
|
+
content: deferredToolResultContent,
|
|
374
|
+
timestamp: json.timestamp || null,
|
|
375
|
+
model: resolvedModel,
|
|
376
|
+
messageId: messageId ? `${messageId}-tool-result` : null
|
|
377
|
+
});
|
|
378
|
+
}
|
|
348
379
|
}
|
|
349
380
|
} catch (err) {
|
|
350
381
|
// Skip invalid lines
|
|
@@ -393,7 +424,6 @@ module.exports = (config) => {
|
|
|
393
424
|
const { projectName, sessionId } = req.params;
|
|
394
425
|
const path = require('path');
|
|
395
426
|
const fs = require('fs');
|
|
396
|
-
const os = require('os');
|
|
397
427
|
|
|
398
428
|
// Parse real project path (important for cross-project sessions)
|
|
399
429
|
const { fullPath } = parseRealProjectPath(projectName);
|
|
@@ -406,7 +436,7 @@ module.exports = (config) => {
|
|
|
406
436
|
const possiblePaths = [
|
|
407
437
|
projectSessionFile,
|
|
408
438
|
// Location 2: User's .claude/projects directory (ClaudeCode default)
|
|
409
|
-
path.join(
|
|
439
|
+
path.join(CLAUDE_PROJECTS_DIR, projectName, sessionId + '.jsonl')
|
|
410
440
|
];
|
|
411
441
|
|
|
412
442
|
for (const testPath of possiblePaths) {
|
package/src/server/index.js
CHANGED
|
@@ -3,7 +3,7 @@ const path = require('path');
|
|
|
3
3
|
const chalk = require('chalk');
|
|
4
4
|
const inquirer = require('inquirer');
|
|
5
5
|
const { loadConfig } = require('../config/loader');
|
|
6
|
-
const { ensureStorageDirMigrated } = require('../config/paths');
|
|
6
|
+
const { PATHS, ensureStorageDirMigrated } = require('../config/paths');
|
|
7
7
|
const { startWebSocketServer: attachWebSocketServer } = require('./websocket-server');
|
|
8
8
|
const { isPortInUse, killProcessByPort, waitForPortRelease } = require('../utils/port-helper');
|
|
9
9
|
const { isProxyConfig } = require('./services/settings-manager');
|
|
@@ -266,14 +266,10 @@ async function startServer(port, host = '127.0.0.1', options = {}) {
|
|
|
266
266
|
// 自动恢复代理状态
|
|
267
267
|
function autoRestoreProxies() {
|
|
268
268
|
const config = loadConfig();
|
|
269
|
-
const os = require('os');
|
|
270
269
|
const fs = require('fs');
|
|
271
|
-
const path = require('path');
|
|
272
|
-
|
|
273
|
-
const ccToolDir = path.join(os.homedir(), '.cc-tool');
|
|
274
270
|
|
|
275
271
|
// 检查 Claude 代理状态文件
|
|
276
|
-
const claudeActiveFile =
|
|
272
|
+
const claudeActiveFile = PATHS.activeChannel.claude;
|
|
277
273
|
if (fs.existsSync(claudeActiveFile)) {
|
|
278
274
|
console.log(chalk.cyan('\n🔄 检测到 Claude 代理状态文件,正在自动启动...'));
|
|
279
275
|
const proxyPort = config.ports?.proxy || 20088;
|
|
@@ -287,7 +283,7 @@ function autoRestoreProxies() {
|
|
|
287
283
|
}
|
|
288
284
|
|
|
289
285
|
// 检查 Codex 代理状态文件
|
|
290
|
-
const codexActiveFile =
|
|
286
|
+
const codexActiveFile = PATHS.activeChannel.codex;
|
|
291
287
|
if (fs.existsSync(codexActiveFile)) {
|
|
292
288
|
console.log(chalk.cyan('\n🔄 检测到 Codex 代理状态文件,正在自动启动...'));
|
|
293
289
|
const codexProxyPort = config.ports?.codexProxy || 20089;
|
|
@@ -312,7 +308,7 @@ function autoRestoreProxies() {
|
|
|
312
308
|
}
|
|
313
309
|
|
|
314
310
|
// 检查 Gemini 代理状态文件
|
|
315
|
-
const geminiActiveFile =
|
|
311
|
+
const geminiActiveFile = PATHS.activeChannel.gemini;
|
|
316
312
|
if (fs.existsSync(geminiActiveFile)) {
|
|
317
313
|
console.log(chalk.cyan('\n🔄 检测到 Gemini 代理状态文件,正在自动启动...'));
|
|
318
314
|
const geminiProxyPort = config.ports?.geminiProxy || 20090;
|
|
@@ -332,7 +328,7 @@ function autoRestoreProxies() {
|
|
|
332
328
|
}
|
|
333
329
|
|
|
334
330
|
// 检查 OpenCode 代理状态文件
|
|
335
|
-
const opencodeActiveFile =
|
|
331
|
+
const opencodeActiveFile = PATHS.activeChannel.opencode;
|
|
336
332
|
if (fs.existsSync(opencodeActiveFile)) {
|
|
337
333
|
console.log(chalk.cyan('\n🔄 检测到 OpenCode 代理状态文件,正在自动启动...'));
|
|
338
334
|
const opencodeProxyPort = config.ports?.opencodeProxy || 20091;
|