Skip to content

Commit ac9f2ab

Browse files
committed
feat(config): 废弃 settings.json 中的 API Key 配置
- 在 `coding-plans.vendors` 中添加 `apiKey` 字段,标记为已废弃,仅保留兼容用途。 - 建议优先使用 VS Code Secret Storage 存储 API Key 以提升安全性。 - 更新配置逻辑:若 `settings.json` 中配置了 `apiKey`,其优先级高于 Secret Storage 中的密钥。 - 更新版本号至 0.8.4。
1 parent ff2f71b commit ac9f2ab

8 files changed

Lines changed: 119 additions & 26 deletions

File tree

DEV.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ npm run test:pages
158158
## 多协议供应商接入说明
159159

160160
- 配置入口优先使用 `Coding Plans: Manage Vendor Configuration`。该命令从 `coding-plans.vendors` 动态生成供应商 QuickPick,选择供应商后可设置 API Key、刷新模型或打开供应商设置。
161+
- `coding-plans.vendors[].apiKey` 已标记为 deprecated,仅保留兼容用途;为保证 API Key 安全,建议优先使用 VS Code Secret Storage,而不是写入 `settings.json`。若此处仍配置了值,它会覆盖 Secret Storage 中同名供应商的 key。
161162
- Copilot Chat 的原生 Add Models 表单只用于添加 `Coding Plans` provider group 和填写 Group Name;`vendorName/apiKey` 字段保留为旧 group 配置兼容项,不再作为必填交互。
162163
- 调试请求链路时,可通过 `coding-plans.logLevel` 控制输出面板日志级别;需要完整追踪时切到 `debug`,日常建议保持 `info`
163164
- `coding-plans.vendors[].defaultApiStyle` 用于声明供应商默认协议风格,模型也可以通过 `coding-plans.vendors[].models[].apiStyle` 单独覆盖:

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ code --install-extension techfetch-dev.coding-plans-for-copilot
105105
{
106106
"name": "my-openai-vendor",
107107
"baseUrl": "https://api.example.com/v1",
108+
"apiKey": "sk-example",
108109
"defaultApiStyle": "openai-chat",
109110
"useModelsEndpoint": true,
110111
"models": []
@@ -143,6 +144,7 @@ code --install-extension techfetch-dev.coding-plans-for-copilot
143144
| `coding-plans.vendors` | `array` | 内置供应商模板 | 供应商配置列表。 |
144145
| `coding-plans.vendors[].name` | `string` | 必填 | 供应商唯一名称。 |
145146
| `coding-plans.vendors[].baseUrl` | `string` | 必填 | API 基础地址。 |
147+
| `coding-plans.vendors[].apiKey` | `string` || 已废弃。仅兼容使用;为保证安全,建议优先通过 VS Code Secret Storage 保存 API Key。若此处配置了值,会覆盖 Secret Storage 中同名供应商的 key。 |
146148
| `coding-plans.vendors[].usageUrl` | `string` || 套餐 usage 接口地址,配置后状态栏显示额度百分比。 |
147149
| `coding-plans.vendors[].defaultApiStyle` | `string` | `openai-chat` | 协议风格:`openai-chat` / `openai-responses` / `anthropic`|
148150
| `coding-plans.vendors[].defaultTemperature` | `number` | `0.2` | 供应商默认 temperature。 |
@@ -171,7 +173,8 @@ code --install-extension techfetch-dev.coding-plans-for-copilot
171173
| `coding-plans.commitMessage.options.requireConventionalType` | `boolean` | `true` | 是否强制 Conventional Commits 类型。 |
172174
| `coding-plans.commitMessage.options.warnOnValidationFailure` | `boolean` | `true` | 校验失败时是否提示告警。 |
173175

174-
`API Key` 不在 `settings.json` 明文存储,请通过「设置 API Key」写入 VS Code Secret Storage。
176+
`coding-plans.vendors[].apiKey` 已标记为 deprecated,仅保留兼容用途。
177+
建议优先通过「设置 API Key」写入 VS Code Secret Storage,避免将密钥保存在 `settings.json` 中;若仍配置 `coding-plans.vendors[].apiKey`,它会覆盖 Secret Storage 中同名供应商的 key。
175178

176179
### 上下文窗口展示
177180

README_en.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ The built-in Xiaomi MiMo default uses the Token Plan endpoint. If you want pay-a
108108
{
109109
"name": "my-openai-vendor",
110110
"baseUrl": "https://api.example.com/v1",
111+
"apiKey": "sk-example",
111112
"defaultApiStyle": "openai-chat",
112113
"useModelsEndpoint": true,
113114
"models": []
@@ -146,6 +147,7 @@ The built-in Xiaomi MiMo default uses the Token Plan endpoint. If you want pay-a
146147
| `coding-plans.vendors` | `array` | Built-in vendor templates | Vendor configuration list. |
147148
| `coding-plans.vendors[].name` | `string` | Required | Vendor unique name. |
148149
| `coding-plans.vendors[].baseUrl` | `string` | Required | API base address. |
150+
| `coding-plans.vendors[].apiKey` | `string` | Empty | Deprecated. Kept for compatibility only; prefer VS Code Secret Storage for safer API key handling. When set, it overrides the same vendor key in Secret Storage. |
149151
| `coding-plans.vendors[].usageUrl` | `string` | Empty | Plan usage API address; when configured, status bar displays quota percentage. |
150152
| `coding-plans.vendors[].defaultApiStyle` | `string` | `openai-chat` | Protocol style: `openai-chat` / `openai-responses` / `anthropic`. |
151153
| `coding-plans.vendors[].defaultTemperature` | `number` | `0.2` | Vendor default temperature. |
@@ -174,7 +176,8 @@ The built-in Xiaomi MiMo default uses the Token Plan endpoint. If you want pay-a
174176
| `coding-plans.commitMessage.options.requireConventionalType` | `boolean` | `true` | Whether to enforce Conventional Commits type. |
175177
| `coding-plans.commitMessage.options.warnOnValidationFailure` | `boolean` | `true` | Whether to show warning on validation failure. |
176178

177-
`API Key` is not stored in plaintext in `settings.json`. Please write it to VS Code Secret Storage via "Set API Key".
179+
`coding-plans.vendors[].apiKey` is deprecated and kept only for compatibility.
180+
Prefer "Set API Key" so the key stays in VS Code Secret Storage instead of `settings.json`; if `coding-plans.vendors[].apiKey` is still set, it overrides the same vendor key in Secret Storage.
178181

179182
### Context Window Display
180183

package.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "coding-plans-for-copilot",
33
"displayName": "%displayName%",
44
"description": "%description%",
5-
"version": "0.8.1",
5+
"version": "0.8.4",
66
"publisher": "techfetch-dev",
77
"repository": {
88
"type": "git",
@@ -237,6 +237,12 @@
237237
"baseUrl": {
238238
"type": "string"
239239
},
240+
"apiKey": {
241+
"type": "string",
242+
"secret": true,
243+
"description": "%configuration.coding-plans.vendors.apiKey.description%",
244+
"deprecationMessage": "%configuration.coding-plans.vendors.apiKey.deprecationMessage%"
245+
},
240246
"usageUrl": {
241247
"type": "string",
242248
"description": "Optional vendor usage endpoint. When configured, the extension fetches coding-plan quota usage and shows percentage summaries in the status bar. Currently verified for Zhipu usage APIs."

package.nls.json

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@
88
"configuration.coding-plans.vendors.description": "Configure AI model vendors with their endpoints and models.",
99
"configuration.coding-plans.vendors.defaultTemperature.description": "Default temperature for this vendor. Models can override it individually. Recommended: 0.1-0.3 for coding, 0.3-0.5 for more creative output.",
1010
"configuration.coding-plans.vendors.defaultTopP.description": "Default top-p for this vendor. Models can override it individually. `0` means omit `top_p`, and is also the runtime default. Set a positive value only when you want to explicitly control nucleus sampling. `anthropic` requests ignore this value and do not send `top_p`.",
11+
"configuration.coding-plans.vendors.apiKey.description": "Deprecated vendor API key in settings. Prefer Secret Storage for better security. When set, this value overrides the same vendor key in Secret Storage.",
12+
"configuration.coding-plans.vendors.apiKey.deprecationMessage": "Deprecated: prefer Secret Storage to keep API keys out of settings.json.",
1113
"configuration.coding-plans.vendors.models.temperature.description": "Per-model temperature override. Inherits vendor defaultTemperature when omitted.",
1214
"configuration.coding-plans.vendors.models.topP.description": "Per-model top-p override. Inherits vendor defaultTopP when omitted. `0` means omit `top_p`. `anthropic` requests ignore this value and do not send `top_p`.",
13-
"providers.codingPlans.config.vendorName.title": "Vendor Name (legacy optional)",
14-
"providers.codingPlans.config.vendorName.description": "Optional legacy filter. Prefer Coding Plans: Manage Vendor Configuration to pick a vendor from coding-plans.vendors.",
15+
"providers.codingPlans.config.vendorName.title": "Vendor Name (optional)",
16+
"providers.codingPlans.config.vendorName.description": "Pick from settings.json coding-plans.vendors.name.",
1517
"providers.codingPlans.config.apiKey.title": "API Key",
16-
"providers.codingPlans.config.apiKey.description": "Optional legacy API key input. Prefer Coding Plans: Manage Vendor Configuration so the key is stored by vendor name.",
18+
"providers.codingPlans.config.apiKey.description": "Optional API key. Prefer Secret Storage for better security.",
1719
"providers.codingPlans.displayName": "Coding Plan",
1820
"commands.coding-plans.generateCommitMessage.title": "Generate Commit Message",
1921
"commands.coding-plans.selectCommitMessageModel.title": "Select Commit Message Model",

package.nls.zh-cn.json

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@
88
"configuration.coding-plans.vendors.description": "配置 AI 模型供应商及其端点和模型。",
99
"configuration.coding-plans.vendors.defaultTemperature.description": "该供应商的默认 temperature,模型可单独覆盖。建议:代码生成/重构用 0.1-0.3,需更灵活表达时可用 0.3-0.5。",
1010
"configuration.coding-plans.vendors.defaultTopP.description": "该供应商的默认 topP,模型可单独覆盖。`0` 表示不发送 `top_p`,也是运行时默认值。仅当你需要显式控制 nucleus sampling 时再设置为正数。`anthropic` 风格请求会忽略该值,不发送 `top_p`。",
11+
"configuration.coding-plans.vendors.apiKey.description": "已废弃的供应商 API Key 配置。为保证安全,建议优先使用 Secret Storage。若此处配置了值,会覆盖 Secret Storage 中同名供应商的 key。",
12+
"configuration.coding-plans.vendors.apiKey.deprecationMessage": "已废弃:建议优先使用 Secret Storage,避免将 API Key 保存在 settings.json 中。",
1113
"configuration.coding-plans.vendors.models.temperature.description": "模型级 temperature 覆盖项;未配置时继承供应商 defaultTemperature。",
1214
"configuration.coding-plans.vendors.models.topP.description": "模型级 topP 覆盖项;未配置时继承供应商 defaultTopP。`0` 表示不发送 `top_p`。`anthropic` 风格请求会忽略该值,不发送 `top_p`。",
13-
"providers.codingPlans.config.vendorName.title": "供应商名称(旧版可选",
14-
"providers.codingPlans.config.vendorName.description": "旧版可选过滤项。建议使用“管理编码套餐配置”从 coding-plans.vendors 中选择供应商",
15+
"providers.codingPlans.config.vendorName.title": "供应商名称(可选",
16+
"providers.codingPlans.config.vendorName.description": "从 settings.json 的 coding-plans.vendors.name 中选择",
1517
"providers.codingPlans.config.apiKey.title": "API Key",
16-
"providers.codingPlans.config.apiKey.description": "旧版可选 API Key 输入项。建议使用“管理编码套餐配置”按供应商名称安全存储",
18+
"providers.codingPlans.config.apiKey.description": "可选 API Key。为保证安全,建议优先使用 Secret Storage",
1719
"providers.codingPlans.displayName": "Coding Plan",
1820
"commands.coding-plans.generateCommitMessage.title": "生成提交消息",
1921
"commands.coding-plans.selectCommitMessageModel.title": "选择提交消息模型",

src/config/configStore.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export interface VendorModelConfig {
3838
export interface VendorConfig {
3939
name: string;
4040
baseUrl: string;
41+
apiKey?: string;
4142
usageUrl?: string;
4243
defaultApiStyle: VendorApiStyle;
4344
defaultTemperature?: number;
@@ -88,6 +89,10 @@ export class ConfigStore implements vscode.Disposable {
8889
}
8990

9091
async getApiKey(vendorName: string): Promise<string> {
92+
const configuredApiKey = this.getConfiguredVendorApiKey(vendorName);
93+
if (configuredApiKey) {
94+
return configuredApiKey;
95+
}
9196
const key = await this.context.secrets.get(VENDOR_API_KEY_PREFIX + vendorName);
9297
return (key || '').trim();
9398
}
@@ -486,6 +491,9 @@ export class ConfigStore implements vscode.Disposable {
486491
return undefined;
487492
}
488493
const baseUrl = typeof obj.baseUrl === 'string' ? obj.baseUrl.trim() : '';
494+
const apiKey = typeof obj.apiKey === 'string' && obj.apiKey.trim().length > 0
495+
? obj.apiKey.trim()
496+
: undefined;
489497
const usageUrl = typeof obj.usageUrl === 'string' && obj.usageUrl.trim().length > 0
490498
? obj.usageUrl.trim()
491499
: undefined;
@@ -499,7 +507,17 @@ export class ConfigStore implements vscode.Disposable {
499507
.map(m => this.normalizeModel(m, defaultVision, defaultApiStyle))
500508
.filter((m): m is VendorModelConfig => m !== undefined)
501509
: [];
502-
return { name, baseUrl, usageUrl, defaultApiStyle, defaultTemperature, defaultTopP, useModelsEndpoint, defaultVision, models };
510+
return { name, baseUrl, apiKey, usageUrl, defaultApiStyle, defaultTemperature, defaultTopP, useModelsEndpoint, defaultVision, models };
511+
}
512+
513+
private getConfiguredVendorApiKey(vendorName: string): string {
514+
const normalizedVendorName = vendorName.trim().toLowerCase();
515+
if (normalizedVendorName.length === 0) {
516+
return '';
517+
}
518+
519+
const vendor = this.getVendors().find(candidate => candidate.name.trim().toLowerCase() === normalizedVendorName);
520+
return vendor?.apiKey?.trim() ?? '';
503521
}
504522

505523
private normalizeModel(

src/test/runTest.ts

Lines changed: 74 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ type VendorModelRecord = {
2626
type VendorRecord = {
2727
name: string;
2828
baseUrl: string;
29+
apiKey?: string;
2930
usageUrl?: string;
3031
defaultApiStyle?: 'openai-chat' | 'openai-responses' | 'anthropic';
3132
defaultTemperature?: number;
@@ -804,21 +805,39 @@ async function runConfigNormalizationTests(configStoreCtor: ConfigStoreCtor): Pr
804805
}]
805806
}]);
806807

807-
configStore = new configStoreCtor(createExtensionContext() as never);
808-
try {
809-
const vendor = configStore.getVendors()[0];
810-
assert.equal(vendor?.models[0]?.apiStyle, 'anthropic');
811-
assert.deepEqual(vendor?.models[0]?.capabilities, { tools: false, vision: false });
812-
assert.equal(vendor?.models[0]?.temperature, undefined);
813-
assert.equal(vendor?.models[0]?.topP, undefined);
814-
console.log('PASS 模型级 apiStyle 覆盖供应商默认值');
815-
} finally {
816-
configStore.dispose();
817-
}
818-
819-
activeState = createState([{
820-
name: 'Vendor',
821-
baseUrl: 'https://example.test/v1',
808+
configStore = new configStoreCtor(createExtensionContext() as never);
809+
try {
810+
const vendor = configStore.getVendors()[0];
811+
assert.equal(vendor?.models[0]?.apiStyle, 'anthropic');
812+
assert.deepEqual(vendor?.models[0]?.capabilities, { tools: false, vision: false });
813+
assert.equal(vendor?.models[0]?.temperature, undefined);
814+
assert.equal(vendor?.models[0]?.topP, undefined);
815+
console.log('PASS 模型级 apiStyle 覆盖供应商默认值');
816+
} finally {
817+
configStore.dispose();
818+
}
819+
820+
activeState = createState([{
821+
name: 'Vendor',
822+
baseUrl: 'https://example.test/v1',
823+
apiKey: ' configured-in-settings ',
824+
defaultApiStyle: 'openai-chat',
825+
defaultVision: false,
826+
models: []
827+
}]);
828+
829+
configStore = new configStoreCtor(createExtensionContext() as never);
830+
try {
831+
const vendor = configStore.getVendors()[0];
832+
assert.equal(vendor?.apiKey, 'configured-in-settings');
833+
console.log('PASS vendors[].apiKey 可被正确归一化');
834+
} finally {
835+
configStore.dispose();
836+
}
837+
838+
activeState = createState([{
839+
name: 'Vendor',
840+
baseUrl: 'https://example.test/v1',
822841
defaultApiStyle: 'openai-responses',
823842
defaultVision: true,
824843
models: []
@@ -956,7 +975,45 @@ async function runConfigNormalizationTests(configStoreCtor: ConfigStoreCtor): Pr
956975
configStore.dispose();
957976
}
958977
}
959-
978+
979+
async function runConfigStoreVendorApiKeyPriorityTests(configStoreCtor: ConfigStoreCtor): Promise<void> {
980+
activeState = createState([{
981+
name: 'Vendor',
982+
baseUrl: 'https://example.test/v1',
983+
apiKey: 'settings-key',
984+
defaultApiStyle: 'openai-chat',
985+
models: []
986+
}]);
987+
988+
let secretContext = createExtensionContextWithSecrets();
989+
secretContext.secrets.set('coding-plans.vendor.apiKey.Vendor', 'secret-key');
990+
let configStore = new configStoreCtor(secretContext.context as never);
991+
try {
992+
assert.equal(await configStore.getApiKey('Vendor'), 'settings-key');
993+
console.log('PASS vendors[].apiKey 优先于 Secret Storage');
994+
} finally {
995+
configStore.dispose();
996+
}
997+
998+
activeState = createState([{
999+
name: 'Vendor',
1000+
baseUrl: 'https://example.test/v1',
1001+
apiKey: ' ',
1002+
defaultApiStyle: 'openai-chat',
1003+
models: []
1004+
}]);
1005+
1006+
secretContext = createExtensionContextWithSecrets();
1007+
secretContext.secrets.set('coding-plans.vendor.apiKey.Vendor', 'secret-key');
1008+
configStore = new configStoreCtor(secretContext.context as never);
1009+
try {
1010+
assert.equal(await configStore.getApiKey('Vendor'), 'secret-key');
1011+
console.log('PASS vendors[].apiKey 为空时回退到 Secret Storage');
1012+
} finally {
1013+
configStore.dispose();
1014+
}
1015+
}
1016+
9601017
function runTokenWindowResolutionTests(baseProviderModule: BaseProviderModule): void {
9611018
const { BaseAIProvider } = baseProviderModule;
9621019
const vscode = require('vscode') as typeof import('vscode');
@@ -3558,6 +3615,7 @@ async function main(): Promise<void> {
35583615
await runTestCase(ConfigStore, testCase);
35593616
}
35603617
await runConfigNormalizationTests(ConfigStore);
3618+
await runConfigStoreVendorApiKeyPriorityTests(ConfigStore);
35613619
runTokenWindowResolutionTests(baseProviderModule);
35623620
runGenericProviderContextSizeTests(ConfigStore, genericProviderModule);
35633621
await runGenericProviderDiscoveryDefaultVisionTests(ConfigStore, genericProviderModule);

0 commit comments

Comments
 (0)