yingclaw 2.3.0 → 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
@@ -50,6 +50,23 @@ claw desktop
50
50
  ```
51
51
  将配置写入 Claude Desktop 第三方推理本地配置。macOS 会自动重启 Claude Desktop;Windows 需手动重新打开。
52
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
+
53
70
  ## 支持的厂商
54
71
 
55
72
  | 厂商 | 主模型 | 快速模型 |
@@ -71,6 +88,9 @@ claw # 交互菜单(无参数时自动进入)
71
88
  claw config # 配置 API 连接
72
89
  claw code # 接入 Claude Code 终端
73
90
  claw desktop # 接入 Claude 桌面应用
91
+ claw opencode # 使用当前厂商接入 opencode
92
+ claw opencode-custom # 使用自定义 Anthropic 接口接入 opencode
93
+ claw opencode-start # 启动 opencode
74
94
  claw switch # 快速切换厂商或模型
75
95
  claw status # 查看当前配置,验证 Key 是否有效
76
96
  claw update # 检查并升级到最新版本
package/bin/cli.js CHANGED
@@ -24,6 +24,13 @@ const { buildMenuStatusLines, buildStatusView } = require('../lib/panel');
24
24
  const { buildClaudeInstallCommand } = require('../lib/install');
25
25
  const { clearClaudeDesktopConfig, isDesktopConfigured, openClaudeDesktop, writeClaudeDesktopConfig } = require('../lib/desktop');
26
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');
27
34
 
28
35
  const program = new Command();
29
36
 
@@ -91,6 +98,76 @@ function getSavedConfigHint() {
91
98
  return '⚠ API Key 以明文存储在 ~/.clawai.json';
92
99
  }
93
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
+
94
171
  async function offerDesktopSync(chalk, ora, config) {
95
172
  if (!isDesktopConfigured()) return;
96
173
  const syncDesktop = await confirm({ message: 'Claude 桌面应用已配置,是否同步新模型?', default: true });
@@ -233,6 +310,7 @@ async function showStatus() {
233
310
  apiStatus: valid,
234
311
  claudeInstalled: isClaudeInstalled(),
235
312
  env: process.env,
313
+ opencode: isOpenCodeConfigured(),
236
314
  });
237
315
 
238
316
  const lines = view.lines.map(({ label, value }) => {
@@ -500,6 +578,43 @@ program
500
578
  ));
501
579
  });
502
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
+
503
618
  program
504
619
  .command('code-reset')
505
620
  .description('恢复 Claude Code 终端默认配置')
@@ -942,6 +1057,7 @@ async function renderStatusBar(apiStatus) {
942
1057
  apiStatus,
943
1058
  claudeInstalled,
944
1059
  env: process.env,
1060
+ opencode: isOpenCodeConfigured(),
945
1061
  });
946
1062
  const statusLines = buildMenuStatusLines(view, { apiStatus, claudeInstalled, platform: process.platform });
947
1063
  cfgPart = statusLines.map((line, index) => {
@@ -1004,6 +1120,19 @@ async function maybeCheckApi(config, forceRecheck) {
1004
1120
 
1005
1121
  const ADVANCED_DISABLED_HINT = '需先配置 API 连接';
1006
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
+
1007
1136
  async function runAdvancedMenu(chalk, hasConfig) {
1008
1137
  const action = await select({ loop: false,
1009
1138
  message: chalk.cyan('高级选项'),
@@ -1067,6 +1196,8 @@ async function runMenu() {
1067
1196
  { name: '🔄 切换厂商或模型', value: 'switch', disabled: disabledHint },
1068
1197
  { name: '💻 接入 Claude Code 终端', value: 'code', disabled: disabledHint },
1069
1198
  { name: '🖥 接入 Claude 桌面应用', value: 'desktop', disabled: disabledHint },
1199
+ { name: '⌘ opencode ›', value: 'opencode-menu' },
1200
+ { name: '▶ 启动 opencode', value: 'opencode-start' },
1070
1201
  { name: '📊 查看当前配置', value: 'status', disabled: !config && '需先配置 API 连接' },
1071
1202
  { name: '🛠 高级 ›', value: 'advanced' },
1072
1203
  { name: '退出', value: 'exit' },
@@ -1081,6 +1212,11 @@ async function runMenu() {
1081
1212
  if (adv === '__BACK__') continue;
1082
1213
  resolvedAction = adv;
1083
1214
  }
1215
+ if (action === 'opencode-menu') {
1216
+ const opencodeAction = await runOpenCodeMenu(chalk, !!config && !configProblem);
1217
+ if (opencodeAction === '__BACK__') continue;
1218
+ resolvedAction = opencodeAction;
1219
+ }
1084
1220
 
1085
1221
  if (resolvedAction === 'recheck') {
1086
1222
  lastCheckResult = undefined;
@@ -1106,10 +1242,27 @@ async function runMenu() {
1106
1242
  continue;
1107
1243
  }
1108
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
+
1109
1260
  const cmdMap = {
1110
1261
  install: 'install-claude',
1111
1262
  config: 'config',
1112
1263
  code: 'code',
1264
+ opencode: 'opencode',
1265
+ 'opencode-custom': 'opencode-custom',
1113
1266
  'code-reset': 'code-reset',
1114
1267
  switch: 'switch',
1115
1268
  desktop: 'desktop',
@@ -1128,7 +1281,7 @@ async function runMenu() {
1128
1281
  });
1129
1282
 
1130
1283
  // 改 config 的命令需要刷新缓存
1131
- if (['config', 'switch', 'reset', 'code-reset'].includes(resolvedAction)) {
1284
+ if (['config', 'switch', 'reset', 'code-reset', 'opencode', 'opencode-custom'].includes(resolvedAction)) {
1132
1285
  lastCheckResult = undefined;
1133
1286
  lastCheckedHash = null;
1134
1287
  }
@@ -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.3.0",
3
+ "version": "2.3.1",
4
4
  "description": "Claude Code × 国产大模型一键接入:DeepSeek、Kimi、Qwen、MiniMax、GLM、MiMo",
5
5
  "main": "index.js",
6
6
  "bin": {