From ef3196d732fe90c9465fc5e13d3dd8e65a0e97a9 Mon Sep 17 00:00:00 2001 From: lewis617 Date: Mon, 18 May 2026 11:17:52 +0800 Subject: [PATCH 1/5] feat: mask WAVE_API_KEY env var in configuration placeholder Prevent exposing raw API key values in the configuration dialog by masking with asterisks, keeping only first 4 and last 4 characters visible. --- src/services/configurationService.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/services/configurationService.ts b/src/services/configurationService.ts index 3913267..0ad2504 100644 --- a/src/services/configurationService.ts +++ b/src/services/configurationService.ts @@ -17,6 +17,12 @@ export interface ConfigurationData { envFastModel?: string; } +/** Mask a secret value, keeping first 4 and last 4 characters visible */ +function maskSecret(value: string): string { + if (value.length <= 10) return '****'; + return value.slice(0, 4) + '****' + value.slice(-4); +} + export class ConfigurationService { constructor(private context: vscode.ExtensionContext) {} @@ -30,7 +36,7 @@ export class ConfigurationService { fastModel: this.context.globalState.get('fastModel') || '', language: this.context.globalState.get('language') || 'Chinese', envAiUrl: process.env.WAVE_AI_URL || undefined, - envApiKey: process.env.WAVE_API_KEY || undefined, + envApiKey: process.env.WAVE_API_KEY ? maskSecret(process.env.WAVE_API_KEY) : undefined, envHeaders: process.env.WAVE_CUSTOM_HEADERS || undefined, envBaseUrl: process.env.WAVE_BASE_URL || undefined, envModel: process.env.WAVE_MODEL || undefined, From 70e0e607a8f2d1ddbfa03aa0e99354176a47cfb0 Mon Sep 17 00:00:00 2001 From: lewis617 Date: Mon, 18 May 2026 11:32:51 +0800 Subject: [PATCH 2/5] feat: add auth method radio selection with form coupling Add three-way radio group (SSO / API Key / Headers) to configuration dialog. Switching methods clears unrelated fields. Persists authMethod to globalState and updates documentation. --- docs/index.md | 38 ++- src/services/configurationService.ts | 4 + .../src/components/ConfigurationDialog.tsx | 288 ++++++++++++------ webview/src/types/index.ts | 2 + 4 files changed, 230 insertions(+), 102 deletions(-) diff --git a/docs/index.md b/docs/index.md index 7c6d27e..ea177b5 100644 --- a/docs/index.md +++ b/docs/index.md @@ -9,22 +9,50 @@ title: Wave 代码智聊 [下载最新版本](https://github.com/netease-lcap/wave-vsce/releases) · [查看产品规格](/spec) -## 鉴权配置说明(以下两种方案二选一) +## 鉴权配置说明(以下三种方案三选一) -### 方案一:OpenAI 兼容格式的 KEY +插件提供三种认证方式,选择其一即可: + +### 方案一:SSO 单点登录(推荐内部使用) + +填写 **服务端链接** 后,点击 `SSO 登录` 按钮完成认证。支持网易内部 SSO,无需手动管理密钥。 + +### 方案二:API Key 鉴权 只要是 OpenAI 兼容格式都可以。网易内部推荐使用互娱服务:[AIGW 文档](https://aigw.doc.nie.netease.com/)。外网可使用 OpenAI 或 DeepSeek 官方服务。 -### 方案二:用户维度(网易内部) +配置项: + +| 字段 | 说明 | 示例 | +| --- | --- | --- | +| **Base URL** | API 服务地址 | `https://api.openai.com/v1` | +| **API Key** | 访问密钥 | `sk-xxxxxxxxxxxxxxxx` | +| **Model** | 主模型名称 | `gpt-4` | +| **Fast Model** | 快速模型名称 | `gpt-3.5-turbo` | + +### 方案三:Headers 自定义鉴权(网易内部 AIGW) 1. 打开 [ModelSpace App 管理](https://modelspace.netease.com/model_access/app_manage),新建 App,生成的 **App Code** 将作为 `X-AIGW-APP`。 2. 给 App 添加成员。 3. 每个成员访问 [权限控制台](https://console-auth.nie.netease.com/mymessage/mymessage),复制 **v2 Token** 内容,作为 `X-Access-Token`。 -4. 在插件配置中添加 Headers(每行一个 `Key: Value`): +4. 选择 `Headers` 认证方式,在 Headers 配置中添加(每行一个 `Key: Value`): ``` X-AIGW-APP: your_app_code X-Access-Token: your_access_token ``` -5. 将 **BASE URL** 配置为:`https://aigw.netease.com/v1` +5. 将 **Base URL** 配置为:`https://aigw.netease.com/v1` + +## 环境变量配置(可选) + +以上配置项均可通过环境变量预设,表单中会显示为占位符(敏感信息已脱敏): + +| 环境变量 | 对应配置项 | +| --- | --- | +| `WAVE_AI_URL` | 服务端链接 | +| `WAVE_API_KEY` | API Key(占位符中显示为 `****`) | +| `WAVE_CUSTOM_HEADERS` | Headers | +| `WAVE_BASE_URL` | Base URL | +| `WAVE_MODEL` | Model | +| `WAVE_FAST_MODEL` | Fast Model | diff --git a/src/services/configurationService.ts b/src/services/configurationService.ts index 0ad2504..2dc3bc2 100644 --- a/src/services/configurationService.ts +++ b/src/services/configurationService.ts @@ -1,6 +1,7 @@ import * as vscode from 'vscode'; export interface ConfigurationData { + authMethod?: 'sso' | 'apiKey' | 'headers'; aiUrl?: string; apiKey?: string; headers?: string; @@ -28,6 +29,7 @@ export class ConfigurationService { public async loadConfiguration(): Promise { return { + authMethod: this.context.globalState.get<'sso' | 'apiKey' | 'headers'>('authMethod') || 'sso', aiUrl: this.context.globalState.get('aiUrl') || '', apiKey: this.context.globalState.get('apiKey') || '', headers: this.context.globalState.get('headers') || '', @@ -46,6 +48,8 @@ export class ConfigurationService { public async saveConfiguration(configData: Partial): Promise { try { + if (configData.authMethod !== undefined) await this.context.globalState.update('authMethod', configData.authMethod); + if (configData.aiUrl !== undefined) await this.context.globalState.update('aiUrl', configData.aiUrl); if (configData.apiKey !== undefined) await this.context.globalState.update('apiKey', configData.apiKey); if (configData.headers !== undefined) await this.context.globalState.update('headers', configData.headers); if (configData.baseURL !== undefined) await this.context.globalState.update('baseURL', configData.baseURL); diff --git a/webview/src/components/ConfigurationDialog.tsx b/webview/src/components/ConfigurationDialog.tsx index 1f011e8..0cab9be 100644 --- a/webview/src/components/ConfigurationDialog.tsx +++ b/webview/src/components/ConfigurationDialog.tsx @@ -21,8 +21,12 @@ const ConfigurationDialog: React.FC }) => { const [activeTab, setActiveTab] = useState<'general' | 'plugins'>('general'); const [activePluginTab, setActivePluginTab] = useState<'explore' | 'installed' | 'marketplaces'>('explore'); - + + // Auth method: 'sso' | 'apiKey' | 'headers' + const [authMethod, setAuthMethod] = useState<'sso' | 'apiKey' | 'headers'>('sso'); + const [formData, setFormData] = useState({ + authMethod: 'sso', aiUrl: '', apiKey: '', headers: '', @@ -149,9 +153,32 @@ const ConfigurationDialog: React.FC useEffect(() => { if (configurationData) { setFormData(configurationData); + // Derive auth method from existing values or use saved value + const savedMethod = configurationData.authMethod; + if (savedMethod) { + setAuthMethod(savedMethod); + } else if (configurationData.apiKey) { + setAuthMethod('apiKey'); + } else if (configurationData.headers) { + setAuthMethod('headers'); + } else { + setAuthMethod('sso'); + } } }, [configurationData]); + const handleAuthMethodChange = (method: 'sso' | 'apiKey' | 'headers') => { + setAuthMethod(method); + // Clear fields not relevant to the selected method and update formData.authMethod + if (method === 'sso') { + setFormData(prev => ({ ...prev, authMethod: 'sso', apiKey: '', headers: '', baseURL: '', model: '', fastModel: '' })); + } else if (method === 'apiKey') { + setFormData(prev => ({ ...prev, authMethod: 'apiKey', aiUrl: '', headers: '' })); + } else { + setFormData(prev => ({ ...prev, authMethod: 'headers', aiUrl: '', apiKey: '' })); + } + }; + // Handle clicking outside to close dialog useEffect(() => { const handleClickOutside = (event: MouseEvent) => { @@ -225,113 +252,180 @@ const ConfigurationDialog: React.FC
- - handleInputChange('aiUrl', e.target.value)} - placeholder={configurationData?.envAiUrl || '请联系管理员获取'} - disabled={isLoading} - /> + +
+ + + +
-
- - {isAuthenticated ? ( -
-
- {authUser?.email && {authUser.email}} - ID: {authUser?.id} -
- + {authMethod === 'sso' && ( + <> +
+ + handleInputChange('aiUrl', e.target.value)} + placeholder={configurationData?.envAiUrl || '请联系管理员获取'} + disabled={isLoading} + />
- ) : ( -
- - {authMessage && ( -
- {authMessage} + +
+ + {isAuthenticated ? ( +
+
+ {authUser?.email && {authUser.email}} + ID: {authUser?.id} +
+ +
+ ) : ( +
+ + {authMessage && ( +
+ {authMessage} +
+ )}
)}
- )} -
+ + )} -
- - handleInputChange('apiKey', e.target.value)} - placeholder={configurationData?.envApiKey || '输入 API Key (或设置 WAVE_API_KEY 环境变量)'} - disabled={isLoading} - /> -
+ {authMethod === 'apiKey' && ( + <> +
+ + handleInputChange('baseURL', e.target.value)} + placeholder={configurationData?.envBaseUrl || 'https://api.example.com/v1 (或设置 WAVE_BASE_URL)'} + disabled={isLoading} + /> +
-
- -