yingclaw 2.2.3 → 2.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/README.md CHANGED
@@ -4,6 +4,8 @@ Claude Code × 国产大模型,一键接入。
4
4
 
5
5
  支持 DeepSeek、Kimi、阿里云百炼(Qwen)、MiniMax、智谱 GLM、小米 MiMo,也支持自定义 Anthropic 兼容接口,无需梯子即可使用 Claude Code。
6
6
 
7
+ ![yingclaw 交互菜单](https://raw.githubusercontent.com/DengShiyingA/yingclaw/main/screenshot.png)
8
+
7
9
  ## 安装
8
10
 
9
11
  ```bash
@@ -48,6 +50,23 @@ claw desktop
48
50
  ```
49
51
  将配置写入 Claude Desktop 第三方推理本地配置。macOS 会自动重启 Claude Desktop;Windows 需手动重新打开。
50
52
 
53
+ 接入 opencode:
54
+ ```bash
55
+ claw opencode
56
+ ```
57
+ 使用当前厂商接入 opencode。DeepSeek 等已知厂商会优先走 opencode 内置 provider;真正的 Anthropic 兼容自定义接口可使用:
58
+
59
+ ```bash
60
+ claw opencode-custom
61
+ ```
62
+
63
+ 两种方式都会写入 `~/.config/opencode/opencode.json` 和系统提示词文件,复用当前 API 连接。
64
+
65
+ 启动 opencode:
66
+ ```bash
67
+ claw opencode-start
68
+ ```
69
+
51
70
  ## 支持的厂商
52
71
 
53
72
  | 厂商 | 主模型 | 快速模型 |
@@ -69,6 +88,9 @@ claw # 交互菜单(无参数时自动进入)
69
88
  claw config # 配置 API 连接
70
89
  claw code # 接入 Claude Code 终端
71
90
  claw desktop # 接入 Claude 桌面应用
91
+ claw opencode # 使用当前厂商接入 opencode
92
+ claw opencode-custom # 使用自定义 Anthropic 接口接入 opencode
93
+ claw opencode-start # 启动 opencode
72
94
  claw switch # 快速切换厂商或模型
73
95
  claw status # 查看当前配置,验证 Key 是否有效
74
96
  claw update # 检查并升级到最新版本
@@ -112,7 +134,7 @@ CLAUDE_CODE_EFFORT_LEVEL
112
134
  **自定义接口**需支持 Anthropic `/v1/messages` 格式;工具会根据 Base URL 自动尝试获取模型列表,失败则手动输入。
113
135
 
114
136
  ## 卸载
115
-
137
+
116
138
  ```bash
117
139
  npm uninstall -g yingclaw
118
140
  claw reset # 可选:清除已写入的环境变量和桌面配置
package/bin/cli.js CHANGED
@@ -11,10 +11,10 @@ const {
11
11
  fetchModelsFromBaseUrl,
12
12
  resetConfig,
13
13
  validateConfig,
14
+ validateKey,
14
15
  normalizeAnthropicBaseUrl,
15
16
  resolveFastModel,
16
17
  buildClaudeEnv,
17
- classifyValidationStatus,
18
18
  PROVIDERS,
19
19
  CLAUDE_ENV_KEYS,
20
20
  } = require('../lib/config');
@@ -23,6 +23,14 @@ const pkg = require('../package.json');
23
23
  const { buildMenuStatusLines, buildStatusView } = require('../lib/panel');
24
24
  const { buildClaudeInstallCommand } = require('../lib/install');
25
25
  const { clearClaudeDesktopConfig, isDesktopConfigured, openClaudeDesktop, writeClaudeDesktopConfig } = require('../lib/desktop');
26
+ const { runDoctorChecks, summarize, STATUS_OK, STATUS_FAIL, STATUS_WARN, STATUS_INFO } = require('../lib/doctor');
27
+ const {
28
+ buildOpenCodeLaunchCommand,
29
+ getOpenCodePaths,
30
+ isOpenCodeConfigured,
31
+ isOpenCodeInstalled,
32
+ writeOpenCodeConfig,
33
+ } = require('../lib/opencode');
26
34
 
27
35
  const program = new Command();
28
36
 
@@ -38,42 +46,6 @@ async function getBanner() {
38
46
  );
39
47
  }
40
48
 
41
- async function validateKey(config) {
42
- let url;
43
- try {
44
- url = `${normalizeAnthropicBaseUrl(config.baseUrl)}/v1/messages`;
45
- new URL(url);
46
- } catch {
47
- return null;
48
- }
49
-
50
- const controller = new AbortController();
51
- const timeout = setTimeout(() => controller.abort(), 8000);
52
-
53
- try {
54
- const res = await fetch(url, {
55
- method: 'POST',
56
- signal: controller.signal,
57
- headers: {
58
- 'content-type': 'application/json',
59
- authorization: `Bearer ${config.apiKey}`,
60
- 'x-api-key': config.apiKey,
61
- 'anthropic-version': '2023-06-01',
62
- },
63
- body: JSON.stringify({
64
- model: config.model,
65
- max_tokens: 1,
66
- messages: [{ role: 'user', content: 'hi' }],
67
- }),
68
- });
69
- return classifyValidationStatus(res.status);
70
- } catch {
71
- return null;
72
- } finally {
73
- clearTimeout(timeout);
74
- }
75
- }
76
-
77
49
  function getConfigValidationMessage(config) {
78
50
  const validation = validateConfig(config);
79
51
  return validation.valid ? null : validation.message;
@@ -126,6 +98,76 @@ function getSavedConfigHint() {
126
98
  return '⚠ API Key 以明文存储在 ~/.clawai.json';
127
99
  }
128
100
 
101
+ async function promptOpenCodeSystemPrompt(chalk) {
102
+ const paths = getOpenCodePaths();
103
+ let existingPrompt = '';
104
+ try {
105
+ existingPrompt = require('fs').readFileSync(paths.promptFile, 'utf8').trim();
106
+ } catch {}
107
+
108
+ if (existingPrompt) {
109
+ const action = await select({ loop: false,
110
+ message: chalk.cyan('opencode 系统提示词'),
111
+ choices: [
112
+ { name: '沿用当前系统提示词', value: 'keep' },
113
+ { name: '重新输入系统提示词', value: 'replace' },
114
+ ],
115
+ });
116
+ if (action === 'keep') return existingPrompt;
117
+ }
118
+
119
+ return input({
120
+ message: chalk.cyan('输入 opencode 系统提示词'),
121
+ default: existingPrompt || '始终使用中文回答。修改代码前先说明计划。优先保持改动最小。',
122
+ validate: (v) => v.trim().length > 0 ? true : '系统提示词不能为空',
123
+ }).then(v => v.trim());
124
+ }
125
+
126
+ async function runOpenCodeConfigFlow({ mode = 'auto' } = {}) {
127
+ const chalk = (await import('chalk')).default;
128
+ const ora = (await import('ora')).default;
129
+ const boxen = (await import('boxen')).default;
130
+
131
+ console.log(await getBanner());
132
+
133
+ const config = loadConfig();
134
+ if (!config) {
135
+ console.log(chalk.red('\n未配置 API 连接,请先运行: claw config\n'));
136
+ return;
137
+ }
138
+ const configProblem = getConfigValidationMessage(config);
139
+ if (configProblem) {
140
+ console.log(chalk.red(`\n配置无效:${configProblem}`));
141
+ console.log(chalk.dim('请运行 claw config 重新配置。\n'));
142
+ return;
143
+ }
144
+
145
+ const systemPrompt = await promptOpenCodeSystemPrompt(chalk);
146
+ const spinner = ora(mode === 'custom' ? '写入 opencode 自定义接口配置...' : '写入 opencode 当前厂商配置...').start();
147
+ let result;
148
+ try {
149
+ result = writeOpenCodeConfig(config, systemPrompt, { mode });
150
+ spinner.succeed(chalk.green(mode === 'custom' ? 'opencode 自定义接口已接入' : 'opencode 已接入当前厂商'));
151
+ } catch (e) {
152
+ spinner.fail(chalk.red(`写入失败: ${e.message}`));
153
+ return;
154
+ }
155
+
156
+ const providerMode = mode === 'custom'
157
+ ? '自定义 Anthropic 接口'
158
+ : (config.provider === 'custom' ? '自定义接口' : 'opencode 内置厂商');
159
+
160
+ console.log(boxen(
161
+ chalk.bold('opencode 配置完成\n\n') +
162
+ chalk.dim('接入方式 ') + chalk.cyan(providerMode) + '\n' +
163
+ chalk.dim('配置文件 ') + chalk.cyan(result.configFile) + '\n' +
164
+ chalk.dim('提示词 ') + chalk.cyan(result.promptFile) + '\n' +
165
+ chalk.dim('模型 ') + chalk.yellow(config.model) + '\n\n' +
166
+ chalk.white('启动时选择“启动 opencode”,或直接运行 ') + chalk.cyan.bold('opencode'),
167
+ { padding: { top: 0, bottom: 0, left: 2, right: 2 }, borderStyle: 'round', borderColor: 'green', margin: { top: 1, bottom: 1 } }
168
+ ));
169
+ }
170
+
129
171
  async function offerDesktopSync(chalk, ora, config) {
130
172
  if (!isDesktopConfigured()) return;
131
173
  const syncDesktop = await confirm({ message: 'Claude 桌面应用已配置,是否同步新模型?', default: true });
@@ -268,6 +310,7 @@ async function showStatus() {
268
310
  apiStatus: valid,
269
311
  claudeInstalled: isClaudeInstalled(),
270
312
  env: process.env,
313
+ opencode: isOpenCodeConfigured(),
271
314
  });
272
315
 
273
316
  const lines = view.lines.map(({ label, value }) => {
@@ -535,6 +578,43 @@ program
535
578
  ));
536
579
  });
537
580
 
581
+ program
582
+ .command('opencode')
583
+ .description('使用当前厂商接入 opencode API 和系统提示词')
584
+ .action(() => runOpenCodeConfigFlow({ mode: 'auto' }));
585
+
586
+ program
587
+ .command('opencode-custom')
588
+ .description('使用自定义 Anthropic 接口接入 opencode')
589
+ .action(() => runOpenCodeConfigFlow({ mode: 'custom' }));
590
+
591
+ program
592
+ .command('opencode-start [project]')
593
+ .description('启动 opencode')
594
+ .action(async (project) => {
595
+ const chalk = (await import('chalk')).default;
596
+
597
+ if (!isOpenCodeInstalled()) {
598
+ console.log(chalk.yellow('\nopencode 未安装,请先运行:'));
599
+ console.log(chalk.cyan('brew install anomalyco/tap/opencode'));
600
+ console.log(chalk.dim('或运行:npm i -g opencode-ai@latest\n'));
601
+ return;
602
+ }
603
+
604
+ const launch = buildOpenCodeLaunchCommand(project || process.cwd());
605
+ await new Promise((resolve) => {
606
+ const child = spawn(launch.command, launch.args, {
607
+ stdio: 'inherit',
608
+ shell: process.platform === 'win32',
609
+ });
610
+ child.on('exit', resolve);
611
+ child.on('error', () => {
612
+ console.log(chalk.yellow('\nopencode 启动失败,请确认 opencode 命令可用'));
613
+ resolve();
614
+ });
615
+ });
616
+ });
617
+
538
618
  program
539
619
  .command('code-reset')
540
620
  .description('恢复 Claude Code 终端默认配置')
@@ -857,6 +937,52 @@ program
857
937
  }
858
938
  });
859
939
 
940
+ program
941
+ .command('doctor')
942
+ .description('诊断当前环境,列出所有问题和修复建议')
943
+ .action(async () => {
944
+ const chalk = (await import('chalk')).default;
945
+ const ora = (await import('ora')).default;
946
+ const boxen = (await import('boxen')).default;
947
+
948
+ console.log(await getBanner());
949
+
950
+ const spinner = ora('运行诊断检查...').start();
951
+ const checks = await runDoctorChecks();
952
+ spinner.stop();
953
+
954
+ const icons = {
955
+ [STATUS_OK]: chalk.green('✓'),
956
+ [STATUS_FAIL]: chalk.red('✗'),
957
+ [STATUS_WARN]: chalk.yellow('⚠'),
958
+ [STATUS_INFO]: chalk.cyan('ℹ'),
959
+ };
960
+
961
+ console.log();
962
+ for (const c of checks) {
963
+ console.log(` ${icons[c.status]} ${chalk.bold(c.name)} ${chalk.dim(c.message)}`);
964
+ if (c.fix) console.log(` ${chalk.dim('→')} ${chalk.cyan(c.fix)}`);
965
+ }
966
+
967
+ const counts = summarize(checks);
968
+ const summaryBits = [];
969
+ if (counts.fail) summaryBits.push(chalk.red(`${counts.fail} 个错误`));
970
+ if (counts.warn) summaryBits.push(chalk.yellow(`${counts.warn} 个警告`));
971
+ if (counts.ok) summaryBits.push(chalk.green(`${counts.ok} 个通过`));
972
+
973
+ const allOk = counts.fail === 0 && counts.warn === 0;
974
+ console.log(boxen(
975
+ (allOk ? chalk.bold.green('✓ 一切正常') : chalk.bold('诊断完成')) +
976
+ (summaryBits.length ? '\n\n' + summaryBits.join(' · ') : ''),
977
+ {
978
+ padding: { top: 0, bottom: 0, left: 2, right: 2 },
979
+ borderStyle: 'round',
980
+ borderColor: counts.fail ? 'red' : counts.warn ? 'yellow' : 'green',
981
+ margin: { top: 1, bottom: 1 },
982
+ }
983
+ ));
984
+ });
985
+
860
986
  program
861
987
  .command('update')
862
988
  .description('检查并更新 yingclaw 到最新版本')
@@ -931,6 +1057,7 @@ async function renderStatusBar(apiStatus) {
931
1057
  apiStatus,
932
1058
  claudeInstalled,
933
1059
  env: process.env,
1060
+ opencode: isOpenCodeConfigured(),
934
1061
  });
935
1062
  const statusLines = buildMenuStatusLines(view, { apiStatus, claudeInstalled, platform: process.platform });
936
1063
  cfgPart = statusLines.map((line, index) => {
@@ -993,10 +1120,24 @@ async function maybeCheckApi(config, forceRecheck) {
993
1120
 
994
1121
  const ADVANCED_DISABLED_HINT = '需先配置 API 连接';
995
1122
 
1123
+ async function runOpenCodeMenu(chalk, hasConfig) {
1124
+ const action = await select({ loop: false,
1125
+ message: chalk.cyan('opencode 接入方式'),
1126
+ choices: [
1127
+ { name: '使用当前厂商接入 opencode', value: 'opencode', disabled: !hasConfig && ADVANCED_DISABLED_HINT },
1128
+ { name: '自定义 Anthropic 接口接入 opencode', value: 'opencode-custom', disabled: !hasConfig && ADVANCED_DISABLED_HINT },
1129
+ { name: '启动 opencode', value: 'opencode-start' },
1130
+ { name: chalk.dim('↩ 返回主菜单'), value: '__BACK__' },
1131
+ ],
1132
+ });
1133
+ return action;
1134
+ }
1135
+
996
1136
  async function runAdvancedMenu(chalk, hasConfig) {
997
1137
  const action = await select({ loop: false,
998
1138
  message: chalk.cyan('高级选项'),
999
1139
  choices: [
1140
+ { name: '🩺 诊断(一键自检并给出修复建议)', value: 'doctor' },
1000
1141
  { name: '🔁 重新检测 API', value: 'recheck', disabled: !hasConfig && ADVANCED_DISABLED_HINT },
1001
1142
  { name: '↩️ 恢复 Claude Code 终端默认', value: 'code-reset' },
1002
1143
  { name: '↩️ 恢复 Claude 桌面默认', value: 'desktop-reset' },
@@ -1055,6 +1196,8 @@ async function runMenu() {
1055
1196
  { name: '🔄 切换厂商或模型', value: 'switch', disabled: disabledHint },
1056
1197
  { name: '💻 接入 Claude Code 终端', value: 'code', disabled: disabledHint },
1057
1198
  { name: '🖥 接入 Claude 桌面应用', value: 'desktop', disabled: disabledHint },
1199
+ { name: '⌘ opencode ›', value: 'opencode-menu' },
1200
+ { name: '▶ 启动 opencode', value: 'opencode-start' },
1058
1201
  { name: '📊 查看当前配置', value: 'status', disabled: !config && '需先配置 API 连接' },
1059
1202
  { name: '🛠 高级 ›', value: 'advanced' },
1060
1203
  { name: '退出', value: 'exit' },
@@ -1069,6 +1212,11 @@ async function runMenu() {
1069
1212
  if (adv === '__BACK__') continue;
1070
1213
  resolvedAction = adv;
1071
1214
  }
1215
+ if (action === 'opencode-menu') {
1216
+ const opencodeAction = await runOpenCodeMenu(chalk, !!config && !configProblem);
1217
+ if (opencodeAction === '__BACK__') continue;
1218
+ resolvedAction = opencodeAction;
1219
+ }
1072
1220
 
1073
1221
  if (resolvedAction === 'recheck') {
1074
1222
  lastCheckResult = undefined;
@@ -1094,10 +1242,27 @@ async function runMenu() {
1094
1242
  continue;
1095
1243
  }
1096
1244
 
1245
+ if (resolvedAction === 'opencode-start') {
1246
+ if (!isOpenCodeInstalled()) {
1247
+ console.log(chalk.yellow('\nopencode 未安装,请先安装:'));
1248
+ console.log(chalk.cyan('brew install anomalyco/tap/opencode'));
1249
+ } else {
1250
+ const launch = buildOpenCodeLaunchCommand(process.cwd());
1251
+ await new Promise((resolve) => {
1252
+ const child = spawn(launch.command, launch.args, { stdio: 'inherit', shell: process.platform === 'win32' });
1253
+ child.on('exit', resolve);
1254
+ child.on('error', resolve);
1255
+ });
1256
+ }
1257
+ continue;
1258
+ }
1259
+
1097
1260
  const cmdMap = {
1098
1261
  install: 'install-claude',
1099
1262
  config: 'config',
1100
1263
  code: 'code',
1264
+ opencode: 'opencode',
1265
+ 'opencode-custom': 'opencode-custom',
1101
1266
  'code-reset': 'code-reset',
1102
1267
  switch: 'switch',
1103
1268
  desktop: 'desktop',
@@ -1105,6 +1270,7 @@ async function runMenu() {
1105
1270
  status: 'status',
1106
1271
  reset: 'reset',
1107
1272
  update: 'update',
1273
+ doctor: 'doctor',
1108
1274
  };
1109
1275
 
1110
1276
  // 执行子命令(用 spawn 隔离,避免 commander 对 program 的副作用)
@@ -1115,7 +1281,7 @@ async function runMenu() {
1115
1281
  });
1116
1282
 
1117
1283
  // 改 config 的命令需要刷新缓存
1118
- if (['config', 'switch', 'reset', 'code-reset'].includes(resolvedAction)) {
1284
+ if (['config', 'switch', 'reset', 'code-reset', 'opencode', 'opencode-custom'].includes(resolvedAction)) {
1119
1285
  lastCheckResult = undefined;
1120
1286
  lastCheckedHash = null;
1121
1287
  }
package/lib/config.js CHANGED
@@ -283,6 +283,42 @@ function classifyValidationStatus(statusCode) {
283
283
  return null;
284
284
  }
285
285
 
286
+ // 发一次最小的 /v1/messages 请求验证 Key(true=有效, false=无效, null=网络/服务异常)
287
+ async function validateKey(config, options = {}) {
288
+ let url;
289
+ try {
290
+ url = `${normalizeAnthropicBaseUrl(config.baseUrl)}/v1/messages`;
291
+ new URL(url);
292
+ } catch {
293
+ return null;
294
+ }
295
+
296
+ const controller = new AbortController();
297
+ const timeout = setTimeout(() => controller.abort(), options.timeoutMs || 8000);
298
+ try {
299
+ const res = await fetch(url, {
300
+ method: 'POST',
301
+ signal: controller.signal,
302
+ headers: {
303
+ 'content-type': 'application/json',
304
+ authorization: `Bearer ${config.apiKey}`,
305
+ 'x-api-key': config.apiKey,
306
+ 'anthropic-version': '2023-06-01',
307
+ },
308
+ body: JSON.stringify({
309
+ model: config.model,
310
+ max_tokens: 1,
311
+ messages: [{ role: 'user', content: 'hi' }],
312
+ }),
313
+ });
314
+ return classifyValidationStatus(res.status);
315
+ } catch {
316
+ return null;
317
+ } finally {
318
+ clearTimeout(timeout);
319
+ }
320
+ }
321
+
286
322
  function shellQuote(value) {
287
323
  return `'${String(value).replace(/'/g, `'\\''`)}'`;
288
324
  }
@@ -405,6 +441,7 @@ module.exports = {
405
441
  buildClaudeEnv,
406
442
  buildEnvBlock,
407
443
  classifyValidationStatus,
444
+ validateKey,
408
445
  PROVIDERS,
409
446
  CONFIG_FILE,
410
447
  CLAUDE_ENV_KEYS,
package/lib/doctor.js ADDED
@@ -0,0 +1,192 @@
1
+ const fs = require('fs');
2
+ const os = require('os');
3
+ const path = require('path');
4
+ const { execSync, spawnSync } = require('child_process');
5
+ const {
6
+ loadConfig,
7
+ validateConfig,
8
+ validateKey,
9
+ buildClaudeEnv,
10
+ normalizeAnthropicBaseUrl,
11
+ PROVIDERS,
12
+ CLAUDE_ENV_KEYS,
13
+ } = require('./config');
14
+ const { isDesktopConfigured } = require('./desktop');
15
+
16
+ const STATUS_OK = 'ok';
17
+ const STATUS_FAIL = 'fail';
18
+ const STATUS_WARN = 'warn';
19
+ const STATUS_INFO = 'info';
20
+
21
+ async function runDoctorChecks(options = {}) {
22
+ const checks = [];
23
+ const platform = options.platform || process.platform;
24
+ const env = options.env || process.env;
25
+
26
+ // 1. Node 版本
27
+ const nodeVersion = process.versions.node;
28
+ const nodeMajor = parseInt(nodeVersion.split('.')[0], 10);
29
+ checks.push({
30
+ name: 'Node 版本',
31
+ status: nodeMajor >= 18 ? STATUS_OK : STATUS_FAIL,
32
+ message: `v${nodeVersion}${nodeMajor >= 18 ? '' : '(< 18)'}`,
33
+ fix: nodeMajor < 18 ? '从 https://nodejs.org 下载 LTS 版本(≥18)' : null,
34
+ });
35
+
36
+ // 2. Claude Code
37
+ let claudeVersion = null;
38
+ try {
39
+ claudeVersion = execSync('claude --version', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
40
+ } catch {}
41
+ checks.push({
42
+ name: 'Claude Code',
43
+ status: claudeVersion ? STATUS_OK : STATUS_WARN,
44
+ message: claudeVersion || '未安装',
45
+ fix: claudeVersion ? null : '运行 claw install-claude',
46
+ });
47
+
48
+ // 3. 配置文件
49
+ const config = loadConfig();
50
+ if (!config) {
51
+ checks.push({
52
+ name: '配置文件',
53
+ status: STATUS_FAIL,
54
+ message: '~/.clawai.json 不存在',
55
+ fix: '运行 claw config',
56
+ });
57
+ return checks;
58
+ }
59
+ const validation = validateConfig(config);
60
+ if (!validation.valid) {
61
+ checks.push({
62
+ name: '配置文件',
63
+ status: STATUS_FAIL,
64
+ message: `~/.clawai.json 无效:${validation.message}`,
65
+ fix: '运行 claw config 重新配置',
66
+ });
67
+ return checks;
68
+ }
69
+ const provider = PROVIDERS[config.provider];
70
+ checks.push({
71
+ name: '配置文件',
72
+ status: STATUS_OK,
73
+ message: `${provider?.name || config.provider} · ${config.model}`,
74
+ });
75
+
76
+ // 4. shell rc / Windows 环境变量已写入
77
+ if (platform === 'win32') {
78
+ const result = checkWindowsEnvVars(options);
79
+ checks.push({
80
+ name: 'Windows 用户环境变量',
81
+ status: result.allWritten ? STATUS_OK : STATUS_WARN,
82
+ message: result.allWritten ? '已写入' : `${result.missing.length} 个变量未写入`,
83
+ fix: result.allWritten ? null : '运行 claw code',
84
+ });
85
+ } else {
86
+ const rcCheck = checkShellRcBlock(options);
87
+ checks.push({
88
+ name: 'shell 配置文件',
89
+ status: rcCheck.found ? STATUS_OK : STATUS_WARN,
90
+ message: rcCheck.found ? `${rcCheck.file} 已写入 yingclaw 块` : '未找到 yingclaw 配置块',
91
+ fix: rcCheck.found ? null : '运行 claw code',
92
+ });
93
+ }
94
+
95
+ // 5. 当前终端环境变量是否生效
96
+ const expected = buildClaudeEnv(config);
97
+ const inactive = Object.entries(expected).filter(([k, v]) => env[k] !== v);
98
+ checks.push({
99
+ name: '当前终端环境变量',
100
+ status: inactive.length === 0 ? STATUS_OK : STATUS_WARN,
101
+ message: inactive.length === 0 ? '已生效' : `${inactive.length}/${Object.keys(expected).length} 未生效`,
102
+ fix: inactive.length === 0
103
+ ? null
104
+ : (platform === 'win32' ? '重新打开 PowerShell / CMD' : '运行 source ~/.zshrc 或重开终端'),
105
+ });
106
+
107
+ // 6. API Key(顺便确认网络可达)
108
+ const valid = await validateKey(config, { timeoutMs: 6000 });
109
+ if (valid === true) {
110
+ checks.push({ name: 'API Key', status: STATUS_OK, message: '校验通过' });
111
+ } else if (valid === false) {
112
+ checks.push({
113
+ name: 'API Key',
114
+ status: STATUS_FAIL,
115
+ message: '无效或已过期(401/403)',
116
+ fix: '运行 claw config 重新输入 Key',
117
+ });
118
+ } else {
119
+ checks.push({
120
+ name: 'API Key',
121
+ status: STATUS_WARN,
122
+ message: '无法连接(网络 / 服务异常)',
123
+ fix: '检查网络 / VPN,或确认 Base URL 没拼错',
124
+ });
125
+ }
126
+
127
+ // 7. Claude 桌面应用接入状态
128
+ const desktopConfigured = isDesktopConfigured();
129
+ checks.push({
130
+ name: 'Claude 桌面应用',
131
+ status: STATUS_INFO,
132
+ message: desktopConfigured ? '已通过 yingclaw 接入' : '未接入(如需运行 claw desktop)',
133
+ });
134
+
135
+ // 8. DeepSeek 旧模型名提醒
136
+ if (config.provider === 'deepseek' && (
137
+ config.model === 'deepseek-v4-pro' ||
138
+ config.model === 'deepseek-v4-flash' ||
139
+ config.fastModel === 'deepseek-v4-flash'
140
+ )) {
141
+ checks.push({
142
+ name: '模型名',
143
+ status: STATUS_WARN,
144
+ message: '使用旧 DeepSeek 模型名',
145
+ fix: '运行 claw switch 升级到 [1m] 长上下文版本',
146
+ });
147
+ }
148
+
149
+ return checks;
150
+ }
151
+
152
+ function checkShellRcBlock(options = {}) {
153
+ const homeDir = options.homeDir || os.homedir();
154
+ const rcFiles = options.rcFiles || [
155
+ path.join(homeDir, '.zshrc'),
156
+ path.join(homeDir, '.bashrc'),
157
+ path.join(homeDir, '.bash_profile'),
158
+ ];
159
+ for (const file of rcFiles) {
160
+ if (!fs.existsSync(file)) continue;
161
+ const content = fs.readFileSync(file, 'utf8');
162
+ if (content.includes('# clawai-start')) return { found: true, file };
163
+ }
164
+ return { found: false };
165
+ }
166
+
167
+ function checkWindowsEnvVars(options = {}) {
168
+ const runner = options.runner || spawnSync;
169
+ const missing = [];
170
+ for (const key of CLAUDE_ENV_KEYS) {
171
+ const result = runner('reg', ['query', 'HKCU\\Environment', '/V', key], { stdio: 'pipe', encoding: 'utf8' });
172
+ if (result.status !== 0) missing.push(key);
173
+ }
174
+ return { allWritten: missing.length === 0, missing };
175
+ }
176
+
177
+ function summarize(checks) {
178
+ const counts = { ok: 0, fail: 0, warn: 0, info: 0 };
179
+ for (const c of checks) counts[c.status] = (counts[c.status] || 0) + 1;
180
+ return counts;
181
+ }
182
+
183
+ module.exports = {
184
+ runDoctorChecks,
185
+ checkShellRcBlock,
186
+ checkWindowsEnvVars,
187
+ summarize,
188
+ STATUS_OK,
189
+ STATUS_FAIL,
190
+ STATUS_WARN,
191
+ STATUS_INFO,
192
+ };
@@ -0,0 +1,162 @@
1
+ const fs = require('fs');
2
+ const os = require('os');
3
+ const path = require('path');
4
+ const { spawnSync } = require('child_process');
5
+ const { normalizeAnthropicBaseUrl } = require('./config');
6
+
7
+ const OPENCODE_PROVIDER_ID = 'yingclaw';
8
+ const OPENCODE_PROMPT_REF = '{file:~/.config/opencode/prompts/build.md}';
9
+
10
+ function getOpenCodePaths(options = {}) {
11
+ const homeDir = options.homeDir || os.homedir();
12
+ const configDir = path.join(homeDir, '.config', 'opencode');
13
+ return {
14
+ configDir,
15
+ configFile: path.join(configDir, 'opencode.json'),
16
+ promptDir: path.join(configDir, 'prompts'),
17
+ promptFile: path.join(configDir, 'prompts', 'build.md'),
18
+ authFile: path.join(homeDir, '.local', 'share', 'opencode', 'auth.json'),
19
+ };
20
+ }
21
+
22
+ function readJsonFile(file) {
23
+ try {
24
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
25
+ } catch {
26
+ return {};
27
+ }
28
+ }
29
+
30
+ function modelMap(config) {
31
+ const models = [config.model, config.fastModel].filter(Boolean);
32
+ return Object.fromEntries([...new Set(models)].map((model) => [model, { name: model }]));
33
+ }
34
+
35
+ function normalizeDeepSeekModel(model) {
36
+ if (model === 'deepseek-v4-pro[1m]') return 'deepseek-v4-pro';
37
+ if (model === 'deepseek-v4-flash[1m]') return 'deepseek-v4-flash';
38
+ return model;
39
+ }
40
+
41
+ function builtInOpenCodeModel(config) {
42
+ if (config.provider === 'deepseek' || normalizeAnthropicBaseUrl(config.baseUrl) === 'https://api.deepseek.com/anthropic') {
43
+ const model = normalizeDeepSeekModel(config.model);
44
+ // DeepSeek's Anthropic-compatible flash endpoint currently hangs for opencode title requests.
45
+ return { providerID: 'deepseek', model, smallModel: model };
46
+ }
47
+ return null;
48
+ }
49
+
50
+ function buildOpenCodeConfig(config, options = {}) {
51
+ const existing = options.existing || {};
52
+ const providerName = config.providerName || config.provider || OPENCODE_PROVIDER_ID;
53
+ const fastModel = config.fastModel || config.model;
54
+ const builtIn = options.mode === 'custom' ? null : builtInOpenCodeModel(config);
55
+
56
+ if (builtIn) {
57
+ return {
58
+ ...existing,
59
+ $schema: 'https://opencode.ai/config.json',
60
+ model: `${builtIn.providerID}/${builtIn.model}`,
61
+ small_model: `${builtIn.providerID}/${builtIn.smallModel}`,
62
+ agent: {
63
+ ...(existing.agent || {}),
64
+ build: {
65
+ ...(existing.agent?.build || {}),
66
+ prompt: OPENCODE_PROMPT_REF,
67
+ },
68
+ },
69
+ };
70
+ }
71
+
72
+ return {
73
+ ...existing,
74
+ $schema: 'https://opencode.ai/config.json',
75
+ model: `${OPENCODE_PROVIDER_ID}/${config.model}`,
76
+ small_model: `${OPENCODE_PROVIDER_ID}/${fastModel}`,
77
+ provider: {
78
+ ...(existing.provider || {}),
79
+ [OPENCODE_PROVIDER_ID]: {
80
+ ...(existing.provider?.[OPENCODE_PROVIDER_ID] || {}),
81
+ npm: '@ai-sdk/anthropic',
82
+ name: providerName,
83
+ options: {
84
+ ...(existing.provider?.[OPENCODE_PROVIDER_ID]?.options || {}),
85
+ baseURL: normalizeAnthropicBaseUrl(config.baseUrl),
86
+ apiKey: config.apiKey,
87
+ },
88
+ models: modelMap({ ...config, fastModel }),
89
+ },
90
+ },
91
+ agent: {
92
+ ...(existing.agent || {}),
93
+ build: {
94
+ ...(existing.agent?.build || {}),
95
+ prompt: OPENCODE_PROMPT_REF,
96
+ },
97
+ },
98
+ };
99
+ }
100
+
101
+ function writePrivateFile(file, body) {
102
+ fs.mkdirSync(path.dirname(file), { recursive: true });
103
+ fs.writeFileSync(file, body);
104
+ try {
105
+ fs.chmodSync(file, 0o600);
106
+ } catch {}
107
+ }
108
+
109
+ function writeOpenCodeConfig(config, prompt, options = {}) {
110
+ const paths = getOpenCodePaths(options);
111
+ const existing = readJsonFile(paths.configFile);
112
+ const next = buildOpenCodeConfig(config, { existing, mode: options.mode });
113
+ const builtIn = options.mode === 'custom' ? null : builtInOpenCodeModel(config);
114
+ const promptBody = prompt.endsWith('\n') ? prompt : `${prompt}\n`;
115
+
116
+ writePrivateFile(paths.promptFile, promptBody);
117
+ if (builtIn) {
118
+ const auth = readJsonFile(paths.authFile);
119
+ auth[builtIn.providerID] = { type: 'api', key: config.apiKey };
120
+ writePrivateFile(paths.authFile, JSON.stringify(auth, null, 2) + '\n');
121
+ }
122
+ writePrivateFile(paths.configFile, JSON.stringify(next, null, 2) + '\n');
123
+
124
+ return { configFile: paths.configFile, promptFile: paths.promptFile };
125
+ }
126
+
127
+ function isOpenCodeConfigured(options = {}) {
128
+ const paths = getOpenCodePaths(options);
129
+ return {
130
+ configured: fs.existsSync(paths.configFile),
131
+ promptConfigured: fs.existsSync(paths.promptFile),
132
+ };
133
+ }
134
+
135
+ function isOpenCodeInstalled(options = {}) {
136
+ const runner = options.runner || spawnSync;
137
+ try {
138
+ const result = runner('opencode', ['--version'], { stdio: 'ignore', shell: process.platform === 'win32' });
139
+ return result.status === 0;
140
+ } catch {
141
+ return false;
142
+ }
143
+ }
144
+
145
+ function buildOpenCodeLaunchCommand(project) {
146
+ return {
147
+ command: 'opencode',
148
+ args: project ? [project] : [],
149
+ };
150
+ }
151
+
152
+ module.exports = {
153
+ OPENCODE_PROMPT_REF,
154
+ OPENCODE_PROVIDER_ID,
155
+ buildOpenCodeConfig,
156
+ buildOpenCodeLaunchCommand,
157
+ builtInOpenCodeModel,
158
+ getOpenCodePaths,
159
+ isOpenCodeConfigured,
160
+ isOpenCodeInstalled,
161
+ writeOpenCodeConfig,
162
+ };
package/lib/panel.js CHANGED
@@ -21,6 +21,7 @@ function buildStatusView(config, options = {}) {
21
21
  const mainModel = expectedEnv.ANTHROPIC_MODEL;
22
22
  const fastModel = expectedEnv.CLAUDE_CODE_SUBAGENT_MODEL;
23
23
  const envActive = isEnvActive(config, env);
24
+ const opencode = options.opencode || {};
24
25
  const warnings = [];
25
26
 
26
27
  if (config.provider === 'deepseek' && (
@@ -46,6 +47,8 @@ function buildStatusView(config, options = {}) {
46
47
  { label: 'Claude Code', value: claudeInstalled ? '已安装' : '未检测到' },
47
48
  { label: '当前终端', value: envActive ? '已生效' : '未生效' },
48
49
  { label: 'Base URL', value: config.baseUrl },
50
+ { label: 'opencode', value: opencode.configured ? '已接入' : '未接入' },
51
+ { label: '系统提示词', value: opencode.promptConfigured ? '已配置' : '未配置' },
49
52
  ],
50
53
  };
51
54
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yingclaw",
3
- "version": "2.2.3",
3
+ "version": "2.3.1",
4
4
  "description": "Claude Code × 国产大模型一键接入:DeepSeek、Kimi、Qwen、MiniMax、GLM、MiMo",
5
5
  "main": "index.js",
6
6
  "bin": {