Skip to content

feat(opencode): 新增 OpenCode 工具配置支持#16

Open
S0ngRu1 wants to merge 4 commits into
qiniu:mainfrom
S0ngRu1:feat/add-opencode-tool
Open

feat(opencode): 新增 OpenCode 工具配置支持#16
S0ngRu1 wants to merge 4 commits into
qiniu:mainfrom
S0ngRu1:feat/add-opencode-tool

Conversation

@S0ngRu1
Copy link
Copy Markdown

@S0ngRu1 S0ngRu1 commented May 21, 2026

Description

新增对 OpenCode 编码助手的配置支持,对应 #8

工具采用统一的自定义 provider qiniu(基于 @ai-sdk/openai-compatible 适配器)接入七牛端点,配置写入 ~/.config/opencode/opencode.json,目录权限 0o700、文件权限 0o600 兜底。API Key 明文落盘,与 Claude Code / Codex 工具的策略保持一致。模型采用多选流程(checkbox),从七牛 /v1/models 列表中勾选若干模型写入 provider.qiniu.models,方便在 OpenCode 的 /models 列表中切换;OpenCode 顶层 model 字段自动取首个选中的模型并加 qiniu/ 前缀路由到该 provider,覆盖 deepseek / qwen / anthropic 等任意前缀的模型。

Changes

  • 新增 OpenCodeTool 实现 ITool 接口(src/lib/tools/opencode-tool.ts),管理 model + provider.qiniu 两个键路径,保留用户自定义的其他 provider / agent / mcp 字段
  • provider.qiniu.npm === '@ai-sdk/openai-compatible' 作为托管标志,避免误删用户手写的同名 provider;卸载时只删以 qiniu/ 前缀开头的 model,用户手动改成 openai/gpt-4 等情况不会被清理
  • baseURL 自动剥离尾部斜杠并补 /v1,符合 OpenAI 兼容协议要求
  • provider.qiniu.models 在多次写入时累积,并自动补全 $schema 字段
  • loadConfig 通过 marketModelService.fetchModels 校验已配置的模型是否仍在七牛模型市场,命中部分缺失时跳过并 warning 提示,全部缺失时抛错引导重新配置
  • 新增 runOpenCodeModelSelectionFlow 多选模型流程(src/lib/wizard/flows/opencode-model-selection-flow.ts),检测到已有配置时先询问是否复用,避免每次都重新勾选;模型列表附带 tool_call / images / reasoning 能力标签
  • 调整模型选择流程渲染顺序,避免 fetch 失败的 warning 被后续 renderHeader 清屏覆盖
  • ModelConfig / Config 扩展 opencodeModels: string[](多选结果)与 opencodeModel: string(向后兼容的首选项)两个字段
  • ToolManager 注册 OpenCodeTool,别名 oc
  • 中英文 i18n 同步新增翻译键(opencode_select_models / opencode_reuse_previous_selection / opencode_models_unavailable 等)
  • 单测覆盖(tests/opencode-tool.test.mjs):单/多模型写入、幂等累积 models、用户 provider 与 agent/mcp 字段保护、model 字段被用户改写后的边界保护、baseURL 尾斜杠归一化、$schema 自动补全、空 / 非法 JSON 容错

Checklist

  • `pnpm build` succeeds
  • `node dist/cli.js --version` outputs correct version (0.3.3)
  • Tested basic CLI functionality (`doctor` 列出 OpenCode,`auth reload` 工具清单含 `opencode (oc)`)
  • Updated translations if adding user-facing strings (zh_CN + en_US)

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request adds the OpenCodeTool to the project, providing a comprehensive implementation for managing OpenCode CLI configurations, including model selection flows and cross-platform environment variable persistence. The review feedback correctly identifies a security vulnerability on Windows where execSync usage could lead to command injection via shell expansion; the reviewer recommends refactoring to execFileSync to mitigate this risk. Other feedback includes improving UI robustness by preventing the clearing of warning messages and removing dead code resulting from the suggested refactor.

Comment thread src/lib/tools/opencode-tool.ts Outdated

function writeEnvVar(name: string, value: string): EnvWriteResult {
if (process.platform === 'win32') {
execSync(`setx ${name} "${escapeWindowsValue(value)}"`, { stdio: 'pipe' });
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

在 Windows 上使用 execSync 配合字符串模板执行 setx 时,如果 API Key 中包含 % 等字符,会被 cmd.exe 尝试进行变量扩展,导致写入的值不正确。建议改用 execFileSync 并传入参数数组,这样可以绕过 shell 的解析,更加安全可靠。同时,使用 execFileSync 后不再需要手动调用 escapeWindowsValue

Suggested change
execSync(`setx ${name} "${escapeWindowsValue(value)}"`, { stdio: 'pipe' });
execFileSync('setx', [name, value], { stdio: 'pipe' });

import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import { execSync } from 'node:child_process';
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

建议引入 execFileSync 以便在 Windows 环境下更安全地执行命令,避免 shell 字符转义和变量扩展问题。

Suggested change
import { execSync } from 'node:child_process';
import { execSync, execFileSync } from 'node:child_process';

Comment thread src/lib/tools/opencode-tool.ts Outdated
function removeEnvVar(name: string): EnvWriteResult {
if (process.platform === 'win32') {
try {
execSync(`reg delete "HKCU\\Environment" /v ${name} /f`, { stdio: 'pipe' });
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

同样建议改用 execFileSync 来执行注册表删除操作,避免潜在的 shell 解析问题。

Suggested change
execSync(`reg delete "HKCU\\Environment" /v ${name} /f`, { stdio: 'pipe' });
execFileSync('reg', ['delete', 'HKCU\\Environment', '/v', name, '/f'], { stdio: 'pipe' });

Comment thread src/lib/tools/opencode-tool.ts Outdated
Comment on lines +276 to +278
function escapeWindowsValue(value: string): string {
return value.replace(/"/g, '\\"');
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

由于改用了 execFileSyncescapeWindowsValue 函数已不再被使用,建议将其移除以保持代码整洁。

Comment on lines +29 to +33
if (models.length === 0) {
uiRenderer.renderWarning(t('model_no_models'));
}

uiRenderer.renderHeader();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

uiRenderer.renderHeader() 通常会清空控制台屏幕。如果在第 30 行渲染了「未获取到模型」的警告,紧接着调用 renderHeader() 会导致该警告被立即清除,用户可能看不见。建议将模型列表为空的检查移动到 renderHeader() 之后。

Suggested change
if (models.length === 0) {
uiRenderer.renderWarning(t('model_no_models'));
}
uiRenderer.renderHeader();
uiRenderer.renderHeader();
if (models.length === 0) {
uiRenderer.renderWarning(t('model_no_models'));
}

Copy link
Copy Markdown

@fennoai fennoai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found 2 issues in the OpenCode integration that can destroy existing user config.

config.model = model;
}

const provider = (isPlainObject(config.provider) ? config.provider : {}) as Record<string, unknown>;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

buildOpenCodeConfig() unconditionally replaces provider.anthropic. That means anyone who already uses OpenCode with their own Anthropic provider settings loses that config on load, and removeManagedOpenCodeConfig() later deletes the whole block instead of restoring it. This PR says it preserves user-defined provider fields, but the built-in anthropic provider is a destructive exception.

function parseJson(content: string): Record<string, unknown> {
if (!content.trim()) return {};
try {
const parsed = JSON.parse(content);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OpenCode supports JSONC configs, but this helper parses the existing file with plain JSON.parse(). Any valid config that contains comments or trailing commas will be treated as {}, so loadConfig()/unloadConfig() rewrites the file and drops the user's non-managed settings instead of preserving them.

- 新增 OpenCodeTool 实现 ITool 接口,配置写入 ~/.config/opencode/opencode.json
- 采用统一自定义 provider qiniu(@ai-sdk/openai-compatible 适配器),覆盖 deepseek/qwen/anthropic 等任意前缀的模型;baseURL 自动补 /v1
- API Key 明文写入配置文件,目录权限 0o700、文件权限 0o600 兜底(对齐 Claude Code / Codex 行为)
- 用 provider.qiniu.npm 字段作为托管标志,避免误删用户手写的同名 provider;卸载时只删 qiniu/-前缀的 model
- 新增单选模型流程 runOpenCodeModelSelectionFlow,复用七牛 /v1/models 列表
- 扩展 ModelConfig 增加 opencodeModel 字段
- 中英文 i18n 同步新增翻译键
- 单测覆盖配置生成、幂等累积 models、用户 provider 保护、model 篡改保护
@S0ngRu1 S0ngRu1 force-pushed the feat/add-opencode-tool branch from 3aa3eac to 5582712 Compare May 21, 2026 09:45
renderHeader() 内部会清屏,原本放在 renderWarning 之后会立即清掉
"未获取到模型" 提示,用户看不到。改为先 renderHeader 再 renderWarning。
@S0ngRu1
Copy link
Copy Markdown
Author

S0ngRu1 commented May 25, 2026

/review

@fennoai
Copy link
Copy Markdown

fennoai Bot commented May 25, 2026

@S0ngRu1 Thanks for the trigger request.

This repository currently enforces member-only triggers, and your account does not have repository membership access, so xgopilot cannot run for this request.

To proceed, you can:

  • Ask a repository maintainer to add you as a collaborator/member, or
  • Update .fennoai.yml and set require_member_to_trigger: false.

@S0ngRu1
Copy link
Copy Markdown
Author

S0ngRu1 commented May 25, 2026

/review

@fennoai
Copy link
Copy Markdown

fennoai Bot commented May 25, 2026

@S0ngRu1 Thanks for the trigger request.

This repository currently enforces member-only triggers, and your account does not have repository membership access, so xgopilot cannot run for this request.

To proceed, you can:

  • Ask a repository maintainer to add you as a collaborator/member, or
  • Update .fennoai.yml and set require_member_to_trigger: false.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant