yingclaw 2.5.31 → 2.5.38
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/README.md +96 -2
- package/bin/cli.js +118 -14
- package/index.js +1 -0
- package/lib/config.js +44 -11
- package/lib/desktop.js +235 -3
- package/lib/doctor.js +200 -2
- package/lib/gateway.js +313 -10
- package/lib/google.js +138 -0
- package/lib/panel.js +18 -1
- package/package.json +1 -1
package/lib/desktop.js
CHANGED
|
@@ -3,7 +3,7 @@ const fs = require('fs');
|
|
|
3
3
|
const os = require('os');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const { spawnSync } = require('child_process');
|
|
6
|
-
const { normalizeAnthropicBaseUrl } = require('./config');
|
|
6
|
+
const { CLEAR_CLAUDE_ENV_KEYS, normalizeAnthropicBaseUrl } = require('./config');
|
|
7
7
|
const {
|
|
8
8
|
YINGCLAW_GATEWAY_PREFIX,
|
|
9
9
|
buildDesktopGatewayRoutes,
|
|
@@ -13,6 +13,7 @@ const {
|
|
|
13
13
|
const CLAUDE_DESKTOP_LABEL = 'Claude 桌面应用配置';
|
|
14
14
|
const YINGCLAW_ENTRY_NAME = 'yingclaw';
|
|
15
15
|
const MAC_POLICY_BUNDLE = 'com.anthropic.claudefordesktop';
|
|
16
|
+
const CLAUDE_CODE_SETTINGS_SCHEMA = 'https://json.schemastore.org/claude-code-settings.json';
|
|
16
17
|
const DESKTOP_GATEWAY_KEYS = [
|
|
17
18
|
'inferenceProvider',
|
|
18
19
|
'inferenceGatewayBaseUrl',
|
|
@@ -96,6 +97,11 @@ function readJsonFile(file) {
|
|
|
96
97
|
}
|
|
97
98
|
}
|
|
98
99
|
|
|
100
|
+
function writeJsonFile(file, value) {
|
|
101
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
102
|
+
fs.writeFileSync(file, JSON.stringify(value, null, 2) + '\n');
|
|
103
|
+
}
|
|
104
|
+
|
|
99
105
|
function toDesktopModelId(model) {
|
|
100
106
|
return model.startsWith('claude-') ? model : `claude-${model}`;
|
|
101
107
|
}
|
|
@@ -112,6 +118,215 @@ function buildGatewayBaseUrl(config) {
|
|
|
112
118
|
return `http://127.0.0.1:${port}${YINGCLAW_GATEWAY_PREFIX}`;
|
|
113
119
|
}
|
|
114
120
|
|
|
121
|
+
function getClaudeCodeSettingsPath(options = {}) {
|
|
122
|
+
const homeDir = options.homeDir || os.homedir();
|
|
123
|
+
return options.settingsFile || path.join(homeDir, '.claude', 'settings.json');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function getClaudePluginCacheDir(options = {}) {
|
|
127
|
+
const homeDir = options.homeDir || os.homedir();
|
|
128
|
+
return options.pluginCacheDir || path.join(homeDir, '.claude', 'plugins', 'cache');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function getProcessStart(pid, options = {}) {
|
|
132
|
+
const runner = options.processRunner || options.runner || spawnSync;
|
|
133
|
+
const platform = options.platform || process.platform;
|
|
134
|
+
const pidText = String(pid);
|
|
135
|
+
let result;
|
|
136
|
+
if (platform === 'win32') {
|
|
137
|
+
result = runner('powershell.exe', [
|
|
138
|
+
'-NoProfile',
|
|
139
|
+
'-Command',
|
|
140
|
+
`$p = Get-Process -Id ${pidText} -ErrorAction SilentlyContinue; if ($p) { $p.StartTime.ToUniversalTime().ToString('o') }`,
|
|
141
|
+
], { stdio: 'pipe', encoding: 'utf8', windowsHide: true });
|
|
142
|
+
} else {
|
|
143
|
+
result = runner('ps', ['-p', pidText, '-o', 'lstart='], { stdio: 'pipe', encoding: 'utf8' });
|
|
144
|
+
}
|
|
145
|
+
if (result.status !== 0) return null;
|
|
146
|
+
return (result.stdout || '').toString().trim() || null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function readLockProcStart(file) {
|
|
150
|
+
try {
|
|
151
|
+
const raw = fs.readFileSync(file, 'utf8');
|
|
152
|
+
const parsed = JSON.parse(raw);
|
|
153
|
+
return typeof parsed.procStart === 'string' ? parsed.procStart.trim() : null;
|
|
154
|
+
} catch {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function walkFiles(dir, visitor) {
|
|
160
|
+
let entries;
|
|
161
|
+
try {
|
|
162
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
163
|
+
} catch {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
for (const entry of entries) {
|
|
167
|
+
const file = path.join(dir, entry.name);
|
|
168
|
+
if (entry.isDirectory()) {
|
|
169
|
+
walkFiles(file, visitor);
|
|
170
|
+
} else if (entry.isFile()) {
|
|
171
|
+
visitor(file);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function cleanupStaleClaudePluginLocks(options = {}) {
|
|
177
|
+
const pluginCacheDir = getClaudePluginCacheDir(options);
|
|
178
|
+
if (!pluginCacheDir || !fs.existsSync(pluginCacheDir)) {
|
|
179
|
+
return { result: 'missing', checked: 0, removed: 0, kept: 0 };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
let checked = 0;
|
|
183
|
+
let removed = 0;
|
|
184
|
+
let kept = 0;
|
|
185
|
+
const processStartCache = new Map();
|
|
186
|
+
|
|
187
|
+
walkFiles(pluginCacheDir, (file) => {
|
|
188
|
+
if (path.basename(path.dirname(file)) !== '.in_use') return;
|
|
189
|
+
const pid = path.basename(file);
|
|
190
|
+
if (!/^\d+$/.test(pid)) return;
|
|
191
|
+
|
|
192
|
+
checked += 1;
|
|
193
|
+
if (!processStartCache.has(pid)) {
|
|
194
|
+
processStartCache.set(pid, getProcessStart(pid, options));
|
|
195
|
+
}
|
|
196
|
+
const currentProcStart = processStartCache.get(pid);
|
|
197
|
+
const lockProcStart = readLockProcStart(file);
|
|
198
|
+
const isStale = !currentProcStart || (lockProcStart && lockProcStart !== currentProcStart);
|
|
199
|
+
|
|
200
|
+
if (!isStale) {
|
|
201
|
+
kept += 1;
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
fs.unlinkSync(file);
|
|
207
|
+
removed += 1;
|
|
208
|
+
} catch {
|
|
209
|
+
kept += 1;
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
return { result: removed > 0 ? 'updated' : 'unchanged', checked, removed, kept };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function findDesktopRoute(routes, family) {
|
|
217
|
+
return routes.find((route) => route && route.id.includes(`claude-${family}-`))?.id;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function buildClaudeDesktopCodeEnv(config, options = {}) {
|
|
221
|
+
const gatewayConfig = ensureDesktopGatewayConfig(config, options);
|
|
222
|
+
const routes = buildDesktopGatewayRoutes(gatewayConfig);
|
|
223
|
+
const fallback = routes[0]?.id || 'claude-sonnet-4-6';
|
|
224
|
+
const sonnet = findDesktopRoute(routes, 'sonnet') || fallback;
|
|
225
|
+
const haiku = findDesktopRoute(routes, 'haiku') || routes[1]?.id || sonnet;
|
|
226
|
+
const opus = findDesktopRoute(routes, 'opus') || sonnet;
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
ANTHROPIC_BASE_URL: buildGatewayBaseUrl(gatewayConfig),
|
|
230
|
+
ANTHROPIC_AUTH_TOKEN: gatewayConfig.desktopGatewayKey,
|
|
231
|
+
ANTHROPIC_MODEL: sonnet,
|
|
232
|
+
ANTHROPIC_DEFAULT_OPUS_MODEL: opus,
|
|
233
|
+
ANTHROPIC_DEFAULT_SONNET_MODEL: sonnet,
|
|
234
|
+
ANTHROPIC_DEFAULT_HAIKU_MODEL: haiku,
|
|
235
|
+
CLAUDE_CODE_SUBAGENT_MODEL: haiku,
|
|
236
|
+
CLAUDE_CODE_EFFORT_LEVEL: 'low',
|
|
237
|
+
CLAUDE_CODE_SIMPLE: '1',
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function buildClaudeDesktopLaunchEnv(env = process.env, overrides = {}) {
|
|
242
|
+
const next = { ...env };
|
|
243
|
+
for (const key of CLEAR_CLAUDE_ENV_KEYS) {
|
|
244
|
+
delete next[key];
|
|
245
|
+
}
|
|
246
|
+
return { ...next, ...overrides };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function shouldWriteClaudeDesktopCodeSettings(options = {}) {
|
|
250
|
+
return options.withCode === true && options.direct !== true;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function shouldPromptClaudeDesktopCodeSettings(options = {}) {
|
|
254
|
+
return options.direct !== true && options.withCode !== true;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function writeClaudeDesktopCodeSettings(config, options = {}) {
|
|
258
|
+
const file = getClaudeCodeSettingsPath(options);
|
|
259
|
+
const current = readJsonFile(file);
|
|
260
|
+
const env = {
|
|
261
|
+
...(current.env && typeof current.env === 'object' ? current.env : {}),
|
|
262
|
+
...buildClaudeDesktopCodeEnv(config, options),
|
|
263
|
+
};
|
|
264
|
+
const next = {
|
|
265
|
+
...current,
|
|
266
|
+
$schema: current.$schema || CLAUDE_CODE_SETTINGS_SCHEMA,
|
|
267
|
+
env,
|
|
268
|
+
};
|
|
269
|
+
writeJsonFile(file, next);
|
|
270
|
+
try {
|
|
271
|
+
fs.chmodSync(file, 0o600);
|
|
272
|
+
} catch {}
|
|
273
|
+
return { result: 'updated', file };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function checkClaudeDesktopCodeSettingsEnv(config, options = {}) {
|
|
277
|
+
const file = getClaudeCodeSettingsPath(options);
|
|
278
|
+
const settings = readJsonFile(file);
|
|
279
|
+
const env = settings.env && typeof settings.env === 'object' ? settings.env : {};
|
|
280
|
+
const expected = buildClaudeDesktopCodeEnv(config, options);
|
|
281
|
+
const missing = [];
|
|
282
|
+
const mismatched = [];
|
|
283
|
+
const hasYingclawEnv = Object.keys(expected).some((key) => Object.prototype.hasOwnProperty.call(env, key));
|
|
284
|
+
|
|
285
|
+
for (const [key, value] of Object.entries(expected)) {
|
|
286
|
+
if (!Object.prototype.hasOwnProperty.call(env, key)) {
|
|
287
|
+
missing.push(key);
|
|
288
|
+
} else if (String(env[key]) !== String(value)) {
|
|
289
|
+
mismatched.push(key);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
file,
|
|
295
|
+
configured: missing.length === 0 && mismatched.length === 0,
|
|
296
|
+
hasYingclawEnv,
|
|
297
|
+
simpleEnabled: env.CLAUDE_CODE_SIMPLE === '1',
|
|
298
|
+
missing,
|
|
299
|
+
mismatched,
|
|
300
|
+
baseUrl: env.ANTHROPIC_BASE_URL || null,
|
|
301
|
+
model: env.ANTHROPIC_MODEL || null,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function clearClaudeDesktopCodeSettings(options = {}) {
|
|
306
|
+
const file = getClaudeCodeSettingsPath(options);
|
|
307
|
+
if (!fs.existsSync(file)) return { result: 'missing', file };
|
|
308
|
+
|
|
309
|
+
const current = readJsonFile(file);
|
|
310
|
+
const nextEnv = { ...(current.env && typeof current.env === 'object' ? current.env : {}) };
|
|
311
|
+
let changed = false;
|
|
312
|
+
for (const key of CLEAR_CLAUDE_ENV_KEYS) {
|
|
313
|
+
if (Object.prototype.hasOwnProperty.call(nextEnv, key)) {
|
|
314
|
+
delete nextEnv[key];
|
|
315
|
+
changed = true;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (!changed) return { result: 'missing', file };
|
|
319
|
+
|
|
320
|
+
const next = { ...current };
|
|
321
|
+
if (Object.keys(nextEnv).length > 0) {
|
|
322
|
+
next.env = nextEnv;
|
|
323
|
+
} else {
|
|
324
|
+
delete next.env;
|
|
325
|
+
}
|
|
326
|
+
writeJsonFile(file, next);
|
|
327
|
+
return { result: 'updated', file };
|
|
328
|
+
}
|
|
329
|
+
|
|
115
330
|
function serializeGatewayModels(config) {
|
|
116
331
|
return buildDesktopGatewayRoutes(config).map((route) => {
|
|
117
332
|
const item = { name: route.id, displayName: route.displayName };
|
|
@@ -463,10 +678,15 @@ async function openClaudeDesktop(options = {}) {
|
|
|
463
678
|
const runner = options.runner || spawnSync;
|
|
464
679
|
const isMocked = options.runner !== undefined;
|
|
465
680
|
const timeoutMs = options.timeoutMs || 5000;
|
|
681
|
+
const launchEnv = buildClaudeDesktopLaunchEnv(options.env || process.env, options.injectEnv || {});
|
|
682
|
+
const shouldCleanupPluginLocks = options.cleanupPluginLocks !== false;
|
|
466
683
|
|
|
467
684
|
const trace = [];
|
|
685
|
+
let pluginLocks = shouldCleanupPluginLocks
|
|
686
|
+
? [cleanupStaleClaudePluginLocks(options)]
|
|
687
|
+
: [];
|
|
468
688
|
for (const { command, args, optional, waitAfter, shell } of commands) {
|
|
469
|
-
const result = runner(command, args, { stdio: 'pipe', encoding: 'utf8', windowsHide: true, timeout: timeoutMs, shell });
|
|
689
|
+
const result = runner(command, args, { stdio: 'pipe', encoding: 'utf8', windowsHide: true, timeout: timeoutMs, shell, env: launchEnv });
|
|
470
690
|
const stderr = (result.stderr || '').toString().trim();
|
|
471
691
|
trace.push({ command, args, status: result.status, stderr });
|
|
472
692
|
|
|
@@ -478,24 +698,36 @@ async function openClaudeDesktop(options = {}) {
|
|
|
478
698
|
if (waitAfter && !isMocked) {
|
|
479
699
|
await sleep(waitAfter);
|
|
480
700
|
}
|
|
701
|
+
if (shouldCleanupPluginLocks && (command === 'pkill' || command === 'taskkill')) {
|
|
702
|
+
pluginLocks.push(cleanupStaleClaudePluginLocks(options));
|
|
703
|
+
}
|
|
481
704
|
}
|
|
482
705
|
|
|
483
|
-
return { result: 'reopened', trace };
|
|
706
|
+
return { result: 'reopened', trace, pluginLocks };
|
|
484
707
|
}
|
|
485
708
|
|
|
486
709
|
module.exports = {
|
|
710
|
+
buildClaudeDesktopCodeEnv,
|
|
487
711
|
buildClaudeDesktopEnterpriseConfig,
|
|
488
712
|
buildClaudeDesktopDirectEnterpriseConfig,
|
|
713
|
+
buildClaudeDesktopLaunchEnv,
|
|
489
714
|
buildClaudeDesktopMacDefaultsCommands,
|
|
490
715
|
buildClaudeDesktopMacDefaultsDeleteCommands,
|
|
491
716
|
buildClaudeDesktopOpenCommands,
|
|
717
|
+
cleanupStaleClaudePluginLocks,
|
|
718
|
+
checkClaudeDesktopCodeSettingsEnv,
|
|
719
|
+
clearClaudeDesktopCodeSettings,
|
|
492
720
|
clearClaudeDesktopConfig,
|
|
721
|
+
getClaudeCodeSettingsPath,
|
|
493
722
|
getClaudeDesktopConfigLibraryDir,
|
|
494
723
|
getClaudeDesktopConfigPath,
|
|
495
724
|
getClaudeDesktopDataDir,
|
|
496
725
|
getClaudeDesktopDataDirs,
|
|
497
726
|
isDesktopConfigured,
|
|
498
727
|
openClaudeDesktop,
|
|
728
|
+
shouldPromptClaudeDesktopCodeSettings,
|
|
729
|
+
shouldWriteClaudeDesktopCodeSettings,
|
|
730
|
+
writeClaudeDesktopCodeSettings,
|
|
499
731
|
writeClaudeDesktopConfig,
|
|
500
732
|
CLAUDE_DESKTOP_LABEL,
|
|
501
733
|
};
|
package/lib/doctor.js
CHANGED
|
@@ -2,6 +2,9 @@ const fs = require('fs');
|
|
|
2
2
|
const os = require('os');
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const { execSync, spawnSync } = require('child_process');
|
|
5
|
+
const {
|
|
6
|
+
getGatewayAutostartStatus,
|
|
7
|
+
} = require('./autostart');
|
|
5
8
|
const {
|
|
6
9
|
loadConfig,
|
|
7
10
|
validateConfig,
|
|
@@ -10,8 +13,9 @@ const {
|
|
|
10
13
|
normalizeAnthropicBaseUrl,
|
|
11
14
|
PROVIDERS,
|
|
12
15
|
CLAUDE_ENV_KEYS,
|
|
16
|
+
CLEAR_CLAUDE_ENV_KEYS,
|
|
13
17
|
} = require('./config');
|
|
14
|
-
const { isDesktopConfigured } = require('./desktop');
|
|
18
|
+
const { checkClaudeDesktopCodeSettingsEnv, getClaudeDesktopDataDirs, isDesktopConfigured } = require('./desktop');
|
|
15
19
|
const { checkClaudeCodeSettingsEnv } = require('./vscode');
|
|
16
20
|
|
|
17
21
|
const STATUS_OK = 'ok';
|
|
@@ -47,7 +51,7 @@ async function runDoctorChecks(options = {}) {
|
|
|
47
51
|
});
|
|
48
52
|
|
|
49
53
|
// 3. 配置文件
|
|
50
|
-
const config = loadConfig();
|
|
54
|
+
const config = options.config || loadConfig();
|
|
51
55
|
if (!config) {
|
|
52
56
|
checks.push({
|
|
53
57
|
name: '配置文件',
|
|
@@ -142,6 +146,23 @@ async function runDoctorChecks(options = {}) {
|
|
|
142
146
|
message: desktopConfigured ? '已通过 yingclaw 接入' : '未接入(如需运行 claw desktop)',
|
|
143
147
|
});
|
|
144
148
|
|
|
149
|
+
const desktopCode = checkClaudeDesktopCodeSettingsEnv(config, options);
|
|
150
|
+
const desktopCodeProblems = desktopCode.missing.length + desktopCode.mismatched.length;
|
|
151
|
+
checks.push({
|
|
152
|
+
name: 'Claude Desktop Code',
|
|
153
|
+
status: desktopCode.configured ? STATUS_OK : desktopCode.hasYingclawEnv ? STATUS_WARN : STATUS_INFO,
|
|
154
|
+
message: desktopCode.configured
|
|
155
|
+
? 'Code 模式环境已配置 · simple 启用'
|
|
156
|
+
: desktopCode.hasYingclawEnv
|
|
157
|
+
? `${desktopCodeProblems} 项配置未写入或不匹配`
|
|
158
|
+
: '未接入(可选;如需 Desktop Code 运行 claw desktop --with-code)',
|
|
159
|
+
fix: desktopCode.configured || !desktopCode.hasYingclawEnv ? null : '运行 claw desktop --with-code,并完全重启 Claude 桌面应用',
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
if (platform === 'win32' || options.windows) {
|
|
163
|
+
checks.push(...buildWindowsDoctorChecks(config, options));
|
|
164
|
+
}
|
|
165
|
+
|
|
145
166
|
// 9. DeepSeek 旧模型名提醒
|
|
146
167
|
if (config.provider === 'deepseek' && (
|
|
147
168
|
config.model === 'deepseek-v4-pro' ||
|
|
@@ -184,17 +205,194 @@ function checkWindowsEnvVars(options = {}) {
|
|
|
184
205
|
return { allWritten: missing.length === 0, missing };
|
|
185
206
|
}
|
|
186
207
|
|
|
208
|
+
function readConfigMirror(file) {
|
|
209
|
+
try {
|
|
210
|
+
return JSON.stringify(JSON.parse(fs.readFileSync(file, 'utf8')));
|
|
211
|
+
} catch {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function checkWindowsDesktopConfigMirrors(options = {}) {
|
|
217
|
+
const dataDirs = options.dataDirs || getClaudeDesktopDataDirs({ ...options, platform: 'win32' });
|
|
218
|
+
const files = dataDirs.map((dir) => path.join(dir, 'claude_desktop_config.json'));
|
|
219
|
+
const existing = files.filter((file) => fs.existsSync(file));
|
|
220
|
+
const missing = files.filter((file) => !fs.existsSync(file));
|
|
221
|
+
if (existing.length === 0) return { status: 'missing', files: existing, missing };
|
|
222
|
+
if (existing.length !== files.length) return { status: 'partial', files: existing, missing };
|
|
223
|
+
const contents = existing.map(readConfigMirror);
|
|
224
|
+
if (contents.some((content) => content == null)) return { status: 'invalid', files: existing, missing };
|
|
225
|
+
return {
|
|
226
|
+
status: new Set(contents).size === 1 ? 'synced' : 'mismatch',
|
|
227
|
+
files: existing,
|
|
228
|
+
missing,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function checkWindowsClaudeProcesses(options = {}) {
|
|
233
|
+
const runner = options.runner || spawnSync;
|
|
234
|
+
const result = runner('powershell.exe', [
|
|
235
|
+
'-NoProfile',
|
|
236
|
+
'-Command',
|
|
237
|
+
`$p = @(Get-Process Claude -ErrorAction SilentlyContinue); [pscustomobject]@{count=$p.Count;pids=@($p.Id)} | ConvertTo-Json -Compress`,
|
|
238
|
+
], { stdio: 'pipe', encoding: 'utf8', windowsHide: true });
|
|
239
|
+
if (result.status !== 0) return { running: false, count: 0, pids: [] };
|
|
240
|
+
try {
|
|
241
|
+
const parsed = JSON.parse(String(result.stdout || '').trim() || '{}');
|
|
242
|
+
const pids = Array.isArray(parsed.pids)
|
|
243
|
+
? parsed.pids.map(Number).filter(Number.isFinite)
|
|
244
|
+
: Number.isFinite(Number(parsed.pids)) ? [Number(parsed.pids)] : [];
|
|
245
|
+
return { running: Number(parsed.count) > 0, count: Number(parsed.count) || 0, pids };
|
|
246
|
+
} catch {
|
|
247
|
+
return { running: false, count: 0, pids: [] };
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function buildWindowsDoctorChecks(config = {}, options = {}) {
|
|
252
|
+
const checks = [];
|
|
253
|
+
const autostart = getGatewayAutostartStatus({
|
|
254
|
+
...options,
|
|
255
|
+
platform: 'win32',
|
|
256
|
+
file: options.startupFile || options.file,
|
|
257
|
+
port: config.desktopGatewayPort || options.port || 18080,
|
|
258
|
+
});
|
|
259
|
+
checks.push({
|
|
260
|
+
name: 'Windows Gateway 端口',
|
|
261
|
+
status: autostart.running ? STATUS_OK : STATUS_WARN,
|
|
262
|
+
message: autostart.running ? `127.0.0.1:${config.desktopGatewayPort || options.port || 18080} 正在监听` : '未监听',
|
|
263
|
+
fix: autostart.running ? null : '运行 claw gateway,或重新执行 claw desktop 设置自动启动',
|
|
264
|
+
});
|
|
265
|
+
checks.push({
|
|
266
|
+
name: 'Windows Gateway 自动启动',
|
|
267
|
+
status: autostart.installed ? STATUS_OK : STATUS_WARN,
|
|
268
|
+
message: autostart.installed ? `已写入 ${autostart.file}` : '未写入启动脚本',
|
|
269
|
+
fix: autostart.installed ? null : '运行 claw desktop 重新写入启动脚本',
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
const mirror = checkWindowsDesktopConfigMirrors(options);
|
|
273
|
+
const mirrorOk = mirror.status === 'synced' || mirror.status === 'missing';
|
|
274
|
+
checks.push({
|
|
275
|
+
name: 'Windows Claude 配置镜像',
|
|
276
|
+
status: mirrorOk ? STATUS_OK : STATUS_WARN,
|
|
277
|
+
message: mirror.status === 'synced'
|
|
278
|
+
? 'Roaming / Local 配置一致'
|
|
279
|
+
: mirror.status === 'missing'
|
|
280
|
+
? '未找到桌面 3P 配置'
|
|
281
|
+
: `${mirror.status}:${mirror.files.length} 个存在,${mirror.missing.length} 个缺失`,
|
|
282
|
+
fix: mirrorOk ? null : '运行 claw desktop 同步 Roaming 和 Local 配置',
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
const claude = checkWindowsClaudeProcesses(options);
|
|
286
|
+
checks.push({
|
|
287
|
+
name: 'Windows Claude 进程',
|
|
288
|
+
status: STATUS_INFO,
|
|
289
|
+
message: claude.running ? `正在运行 ${claude.count} 个 Claude 进程` : '未运行',
|
|
290
|
+
fix: claude.running ? '如配置未生效,请从系统托盘退出 Claude 后重开' : null,
|
|
291
|
+
});
|
|
292
|
+
return checks;
|
|
293
|
+
}
|
|
294
|
+
|
|
187
295
|
function summarize(checks) {
|
|
188
296
|
const counts = { ok: 0, fail: 0, warn: 0, info: 0 };
|
|
189
297
|
for (const c of checks) counts[c.status] = (counts[c.status] || 0) + 1;
|
|
190
298
|
return counts;
|
|
191
299
|
}
|
|
192
300
|
|
|
301
|
+
function redactEnvValue(key, env, expectedEnv) {
|
|
302
|
+
const present = Object.prototype.hasOwnProperty.call(env, key);
|
|
303
|
+
const expected = Object.prototype.hasOwnProperty.call(expectedEnv, key);
|
|
304
|
+
const sensitive = key.includes('TOKEN') || key.includes('KEY');
|
|
305
|
+
return {
|
|
306
|
+
present,
|
|
307
|
+
expected,
|
|
308
|
+
matchesExpected: present && expected ? String(env[key]) === String(expectedEnv[key]) : false,
|
|
309
|
+
value: present ? (sensitive ? '[redacted]' : String(env[key])) : null,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function buildDiagnosticReport(options = {}) {
|
|
314
|
+
const config = options.config || null;
|
|
315
|
+
const checks = options.checks || [];
|
|
316
|
+
const env = options.env || process.env;
|
|
317
|
+
const expectedEnv = options.expectedEnv || (config ? buildClaudeEnv(config) : {});
|
|
318
|
+
const provider = config ? PROVIDERS[config.provider] : null;
|
|
319
|
+
const envKeys = [...new Set([...CLEAR_CLAUDE_ENV_KEYS, ...Object.keys(expectedEnv)])];
|
|
320
|
+
|
|
321
|
+
return {
|
|
322
|
+
generatedAt: options.now || new Date().toISOString(),
|
|
323
|
+
platform: options.platform || process.platform,
|
|
324
|
+
nodeVersion: options.nodeVersion || process.version,
|
|
325
|
+
packageVersion: options.packageVersion || null,
|
|
326
|
+
config: config ? {
|
|
327
|
+
provider: config.provider,
|
|
328
|
+
providerName: config.providerName || provider?.name || config.provider,
|
|
329
|
+
baseUrl: normalizeAnthropicBaseUrl(config.baseUrl),
|
|
330
|
+
model: config.model,
|
|
331
|
+
fastModel: config.fastModel || null,
|
|
332
|
+
availableModelCount: Array.isArray(config.availableModels) ? config.availableModels.length : 0,
|
|
333
|
+
hasApiKey: Boolean(config.apiKey),
|
|
334
|
+
desktopGatewayPort: config.desktopGatewayPort || null,
|
|
335
|
+
hasDesktopGatewayKey: Boolean(config.desktopGatewayKey),
|
|
336
|
+
} : null,
|
|
337
|
+
checks: checks.map((check) => ({
|
|
338
|
+
name: check.name,
|
|
339
|
+
status: check.status,
|
|
340
|
+
message: check.message,
|
|
341
|
+
fix: check.fix || null,
|
|
342
|
+
})),
|
|
343
|
+
env: Object.fromEntries(envKeys.map((key) => [key, redactEnvValue(key, env, expectedEnv)])),
|
|
344
|
+
desktopCode: options.desktopCodeStatus ? {
|
|
345
|
+
configured: options.desktopCodeStatus.configured,
|
|
346
|
+
simpleEnabled: options.desktopCodeStatus.simpleEnabled,
|
|
347
|
+
file: options.desktopCodeStatus.file,
|
|
348
|
+
missing: options.desktopCodeStatus.missing || [],
|
|
349
|
+
mismatched: options.desktopCodeStatus.mismatched || [],
|
|
350
|
+
baseUrl: options.desktopCodeStatus.baseUrl || null,
|
|
351
|
+
model: options.desktopCodeStatus.model || null,
|
|
352
|
+
} : null,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function buildSupportBundle(options = {}) {
|
|
357
|
+
const diagnostic = buildDiagnosticReport(options);
|
|
358
|
+
return {
|
|
359
|
+
type: 'yingclaw-support-bundle',
|
|
360
|
+
version: 1,
|
|
361
|
+
generatedAt: diagnostic.generatedAt,
|
|
362
|
+
packageVersion: options.packageVersion || null,
|
|
363
|
+
platform: diagnostic.platform,
|
|
364
|
+
diagnostic,
|
|
365
|
+
gateway: options.gatewayStatus ? {
|
|
366
|
+
configured: options.gatewayStatus.configured,
|
|
367
|
+
running: options.gatewayStatus.running,
|
|
368
|
+
port: options.gatewayStatus.port,
|
|
369
|
+
url: options.gatewayStatus.url,
|
|
370
|
+
error: options.gatewayStatus.error || null,
|
|
371
|
+
} : null,
|
|
372
|
+
notes: [
|
|
373
|
+
'This file is generated by yingclaw and should not contain API keys.',
|
|
374
|
+
'If you share it, review it once for any provider-specific private URL before posting publicly.',
|
|
375
|
+
],
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function writeDiagnosticReport(file, report) {
|
|
380
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
381
|
+
fs.writeFileSync(file, JSON.stringify(report, null, 2) + '\n');
|
|
382
|
+
return file;
|
|
383
|
+
}
|
|
384
|
+
|
|
193
385
|
module.exports = {
|
|
386
|
+
buildDiagnosticReport,
|
|
387
|
+
buildSupportBundle,
|
|
388
|
+
buildWindowsDoctorChecks,
|
|
389
|
+
checkWindowsClaudeProcesses,
|
|
390
|
+
checkWindowsDesktopConfigMirrors,
|
|
194
391
|
runDoctorChecks,
|
|
195
392
|
checkShellRcBlock,
|
|
196
393
|
checkWindowsEnvVars,
|
|
197
394
|
summarize,
|
|
395
|
+
writeDiagnosticReport,
|
|
198
396
|
STATUS_OK,
|
|
199
397
|
STATUS_FAIL,
|
|
200
398
|
STATUS_WARN,
|