mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-29 04:00:36 +00:00
Merge remote-tracking branch 'origin/main' into feat/debug-logging-refactor
This commit is contained in:
commit
4abec5c055
331 changed files with 19546 additions and 7771 deletions
|
|
@ -57,7 +57,7 @@ export const DEFAULT_EXCLUDED_ENV_VARS = ['DEBUG', 'DEBUG_MODE'];
|
|||
const MIGRATE_V2_OVERWRITE = true;
|
||||
|
||||
// Settings version to track migration state
|
||||
export const SETTINGS_VERSION = 2;
|
||||
export const SETTINGS_VERSION = 3;
|
||||
export const SETTINGS_VERSION_KEY = '$version';
|
||||
|
||||
const MIGRATION_MAP: Record<string, string> = {
|
||||
|
|
@ -74,8 +74,6 @@ const MIGRATION_MAP: Record<string, string> = {
|
|||
customThemes: 'ui.customThemes',
|
||||
customWittyPhrases: 'ui.customWittyPhrases',
|
||||
debugKeystrokeLogging: 'general.debugKeystrokeLogging',
|
||||
disableAutoUpdate: 'general.disableAutoUpdate',
|
||||
disableUpdateNag: 'general.disableUpdateNag',
|
||||
dnsResolutionOrder: 'advanced.dnsResolutionOrder',
|
||||
enforcedAuthType: 'security.auth.enforcedType',
|
||||
excludeTools: 'tools.exclude',
|
||||
|
|
@ -128,6 +126,43 @@ const MIGRATION_MAP: Record<string, string> = {
|
|||
visionModelPreview: 'experimental.visionModelPreview',
|
||||
};
|
||||
|
||||
// Settings that need boolean inversion during migration (V1 -> V3)
|
||||
// Old negative naming -> new positive naming with inverted value
|
||||
const INVERTED_BOOLEAN_MIGRATIONS: Record<string, string> = {
|
||||
disableAutoUpdate: 'general.enableAutoUpdate',
|
||||
disableUpdateNag: 'general.enableAutoUpdate',
|
||||
disableLoadingPhrases: 'ui.accessibility.enableLoadingPhrases',
|
||||
disableFuzzySearch: 'context.fileFiltering.enableFuzzySearch',
|
||||
disableCacheControl: 'model.generationConfig.enableCacheControl',
|
||||
};
|
||||
|
||||
// Consolidated settings: multiple old V1 keys that map to a single new key.
|
||||
// Policy: if ANY of the old disable* settings is true, the new enable* should be false.
|
||||
const CONSOLIDATED_SETTINGS: Record<string, string[]> = {
|
||||
'general.enableAutoUpdate': ['disableAutoUpdate', 'disableUpdateNag'],
|
||||
};
|
||||
|
||||
// V2 nested paths that need inversion when migrating to V3
|
||||
const INVERTED_V2_PATHS: Record<string, string> = {
|
||||
'general.disableAutoUpdate': 'general.enableAutoUpdate',
|
||||
'general.disableUpdateNag': 'general.enableAutoUpdate',
|
||||
'ui.accessibility.disableLoadingPhrases':
|
||||
'ui.accessibility.enableLoadingPhrases',
|
||||
'context.fileFiltering.disableFuzzySearch':
|
||||
'context.fileFiltering.enableFuzzySearch',
|
||||
'model.generationConfig.disableCacheControl':
|
||||
'model.generationConfig.enableCacheControl',
|
||||
};
|
||||
|
||||
// Consolidated V2 paths: multiple old paths that map to a single new path.
|
||||
// Policy: if ANY of the old disable* settings is true, the new enable* should be false.
|
||||
const CONSOLIDATED_V2_PATHS: Record<string, string[]> = {
|
||||
'general.enableAutoUpdate': [
|
||||
'general.disableAutoUpdate',
|
||||
'general.disableUpdateNag',
|
||||
],
|
||||
};
|
||||
|
||||
export function getSystemSettingsPath(): string {
|
||||
if (process.env['QWEN_CODE_SYSTEM_SETTINGS_PATH']) {
|
||||
return process.env['QWEN_CODE_SYSTEM_SETTINGS_PATH'];
|
||||
|
|
@ -169,7 +204,7 @@ export interface SummarizeToolOutputSettings {
|
|||
}
|
||||
|
||||
export interface AccessibilitySettings {
|
||||
disableLoadingPhrases?: boolean;
|
||||
enableLoadingPhrases?: boolean;
|
||||
screenReader?: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -210,6 +245,14 @@ function setNestedProperty(
|
|||
current[lastKey] = value;
|
||||
}
|
||||
|
||||
// Dynamically determine the top-level keys from the V2 settings structure.
|
||||
const KNOWN_V2_CONTAINERS = new Set([
|
||||
...Object.values(MIGRATION_MAP).map((path) => path.split('.')[0]),
|
||||
...Object.values(INVERTED_BOOLEAN_MIGRATIONS).map(
|
||||
(path) => path.split('.')[0],
|
||||
),
|
||||
]);
|
||||
|
||||
export function needsMigration(settings: Record<string, unknown>): boolean {
|
||||
// Check version field first - if present and matches current version, no migration needed
|
||||
if (SETTINGS_VERSION_KEY in settings) {
|
||||
|
|
@ -238,10 +281,20 @@ export function needsMigration(settings: Record<string, unknown>): boolean {
|
|||
return true;
|
||||
});
|
||||
|
||||
return hasV1Keys;
|
||||
// Also check for old inverted boolean keys (disable* -> enable*)
|
||||
const hasInvertedBooleanKeys = Object.keys(INVERTED_BOOLEAN_MIGRATIONS).some(
|
||||
(v1Key) => v1Key in settings,
|
||||
);
|
||||
|
||||
return hasV1Keys || hasInvertedBooleanKeys;
|
||||
}
|
||||
|
||||
function migrateSettingsToV2(
|
||||
/**
|
||||
* Migrates V1 (flat) settings directly to V3.
|
||||
* This includes both structural migration (flat -> nested) and boolean
|
||||
* inversion (disable* -> enable*), so migrateV2ToV3 will be skipped.
|
||||
*/
|
||||
function migrateV1ToV3(
|
||||
flatSettings: Record<string, unknown>,
|
||||
): Record<string, unknown> | null {
|
||||
if (!needsMigration(flatSettings)) {
|
||||
|
|
@ -273,6 +326,43 @@ function migrateSettingsToV2(
|
|||
}
|
||||
}
|
||||
|
||||
// Handle consolidated settings first (multiple old keys -> single new key)
|
||||
// Policy: if ANY of the old disable* settings is true, the new enable* should be false
|
||||
for (const [newPath, oldKeys] of Object.entries(CONSOLIDATED_SETTINGS)) {
|
||||
let hasAnyDisable = false;
|
||||
let hasAnyValue = false;
|
||||
for (const oldKey of oldKeys) {
|
||||
if (flatKeys.has(oldKey)) {
|
||||
hasAnyValue = true;
|
||||
const oldValue = flatSettings[oldKey];
|
||||
if (typeof oldValue === 'boolean' && oldValue === true) {
|
||||
hasAnyDisable = true;
|
||||
}
|
||||
flatKeys.delete(oldKey);
|
||||
}
|
||||
}
|
||||
if (hasAnyValue) {
|
||||
// enableAutoUpdate = !hasAnyDisable (if any disable* was true, enable should be false)
|
||||
setNestedProperty(v2Settings, newPath, !hasAnyDisable);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle remaining V1 settings that need boolean inversion (disable* -> enable*)
|
||||
// Skip keys that were already handled by consolidated settings
|
||||
const consolidatedKeys = new Set(Object.values(CONSOLIDATED_SETTINGS).flat());
|
||||
for (const [oldKey, newPath] of Object.entries(INVERTED_BOOLEAN_MIGRATIONS)) {
|
||||
if (consolidatedKeys.has(oldKey)) {
|
||||
continue;
|
||||
}
|
||||
if (flatKeys.has(oldKey)) {
|
||||
const oldValue = flatSettings[oldKey];
|
||||
if (typeof oldValue === 'boolean') {
|
||||
setNestedProperty(v2Settings, newPath, !oldValue);
|
||||
}
|
||||
flatKeys.delete(oldKey);
|
||||
}
|
||||
}
|
||||
|
||||
// Preserve mcpServers at the top level
|
||||
if (flatSettings['mcpServers']) {
|
||||
v2Settings['mcpServers'] = flatSettings['mcpServers'];
|
||||
|
|
@ -311,6 +401,90 @@ function migrateSettingsToV2(
|
|||
return v2Settings;
|
||||
}
|
||||
|
||||
// Migrate V2 settings to V3 (invert disable* -> enable* booleans)
|
||||
function migrateV2ToV3(
|
||||
settings: Record<string, unknown>,
|
||||
): Record<string, unknown> | null {
|
||||
const version = settings[SETTINGS_VERSION_KEY];
|
||||
if (typeof version === 'number' && version >= 3) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let changed = false;
|
||||
const result = structuredClone(settings);
|
||||
const processedPaths = new Set<string>();
|
||||
|
||||
// Handle consolidated V2 paths first (multiple old paths -> single new path)
|
||||
// Policy: if ANY of the old disable* settings is true, the new enable* should be false
|
||||
for (const [newPath, oldPaths] of Object.entries(CONSOLIDATED_V2_PATHS)) {
|
||||
let hasAnyDisable = false;
|
||||
let hasAnyValue = false;
|
||||
for (const oldPath of oldPaths) {
|
||||
const oldValue = getNestedProperty(result, oldPath);
|
||||
if (typeof oldValue === 'boolean') {
|
||||
hasAnyValue = true;
|
||||
if (oldValue === true) {
|
||||
hasAnyDisable = true;
|
||||
}
|
||||
deleteNestedProperty(result, oldPath);
|
||||
processedPaths.add(oldPath);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (hasAnyValue) {
|
||||
// enableAutoUpdate = !hasAnyDisable (if any disable* was true, enable should be false)
|
||||
setNestedProperty(result, newPath, !hasAnyDisable);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle remaining V2 paths that need inversion
|
||||
for (const [oldPath, newPath] of Object.entries(INVERTED_V2_PATHS)) {
|
||||
if (processedPaths.has(oldPath)) {
|
||||
continue;
|
||||
}
|
||||
const oldValue = getNestedProperty(result, oldPath);
|
||||
if (typeof oldValue === 'boolean') {
|
||||
// Remove old property
|
||||
deleteNestedProperty(result, oldPath);
|
||||
// Set new property with inverted value
|
||||
setNestedProperty(result, newPath, !oldValue);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
result[SETTINGS_VERSION_KEY] = SETTINGS_VERSION;
|
||||
return result;
|
||||
}
|
||||
|
||||
// Even if no changes, bump version to 3 to skip future migration checks
|
||||
if (typeof version === 'number' && version < SETTINGS_VERSION) {
|
||||
result[SETTINGS_VERSION_KEY] = SETTINGS_VERSION;
|
||||
return result;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function deleteNestedProperty(
|
||||
obj: Record<string, unknown>,
|
||||
path: string,
|
||||
): void {
|
||||
const keys = path.split('.');
|
||||
const lastKey = keys.pop();
|
||||
if (!lastKey) return;
|
||||
|
||||
let current: Record<string, unknown> = obj;
|
||||
for (const key of keys) {
|
||||
const next = current[key];
|
||||
if (typeof next !== 'object' || next === null) {
|
||||
return;
|
||||
}
|
||||
current = next as Record<string, unknown>;
|
||||
}
|
||||
delete current[lastKey];
|
||||
}
|
||||
|
||||
function getNestedProperty(
|
||||
obj: Record<string, unknown>,
|
||||
path: string,
|
||||
|
|
@ -330,9 +504,26 @@ const REVERSE_MIGRATION_MAP: Record<string, string> = Object.fromEntries(
|
|||
Object.entries(MIGRATION_MAP).map(([key, value]) => [value, key]),
|
||||
);
|
||||
|
||||
// Dynamically determine the top-level keys from the V2 settings structure.
|
||||
const KNOWN_V2_CONTAINERS = new Set(
|
||||
Object.values(MIGRATION_MAP).map((path) => path.split('.')[0]),
|
||||
// Reverse map for old V2 paths (before rename) to V1 keys.
|
||||
// Used when migrating settings that still have old V2 naming (e.g., general.disableAutoUpdate).
|
||||
const OLD_V2_TO_V1_MAP: Record<string, string> = {};
|
||||
for (const [oldV2Path, newV3Path] of Object.entries(INVERTED_V2_PATHS)) {
|
||||
// Find the V1 key that maps to this V3 path
|
||||
for (const [v1Key, v3Path] of Object.entries(INVERTED_BOOLEAN_MIGRATIONS)) {
|
||||
if (v3Path === newV3Path) {
|
||||
OLD_V2_TO_V1_MAP[oldV2Path] = v1Key;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reverse map for new V3 paths to V1 keys (with boolean inversion).
|
||||
// Used when migrating settings that have new V3 naming (e.g., general.enableAutoUpdate).
|
||||
const V3_TO_V1_INVERTED_MAP: Record<string, string> = Object.fromEntries(
|
||||
Object.entries(INVERTED_BOOLEAN_MIGRATIONS).map(([v1Key, v3Path]) => [
|
||||
v3Path,
|
||||
v1Key,
|
||||
]),
|
||||
);
|
||||
|
||||
function getSettingsFileKeyWarnings(
|
||||
|
|
@ -371,7 +562,7 @@ function getSettingsFileKeyWarnings(
|
|||
|
||||
ignoredLegacyKeys.add(oldKey);
|
||||
warnings.push(
|
||||
`⚠️ Legacy setting '${oldKey}' will be ignored in ${settingsFilePath}. Please use '${newPath}' instead.`,
|
||||
`Warning: Legacy setting '${oldKey}' will be ignored in ${settingsFilePath}. Please use '${newPath}' instead.`,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -389,7 +580,7 @@ function getSettingsFileKeyWarnings(
|
|||
}
|
||||
|
||||
warnings.push(
|
||||
`⚠️ Unknown setting '${key}' will be ignored in ${settingsFilePath}.`,
|
||||
`Warning: Unknown setting '${key}' will be ignored in ${settingsFilePath}.`,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -408,7 +599,8 @@ export function getSettingsWarnings(loadedSettings: LoadedSettings): string[] {
|
|||
for (const scope of [SettingScope.User, SettingScope.Workspace]) {
|
||||
const settingsFile = loadedSettings.forScope(scope);
|
||||
if (settingsFile.rawJson === undefined) {
|
||||
continue; // File not present / not loaded.
|
||||
continue;
|
||||
// File not present / not loaded.
|
||||
}
|
||||
const settingsObject = settingsFile.originalSettings as unknown as Record<
|
||||
string,
|
||||
|
|
@ -440,6 +632,26 @@ export function migrateSettingsToV1(
|
|||
}
|
||||
}
|
||||
|
||||
// Handle old V2 inverted paths (no value inversion needed)
|
||||
// e.g., general.disableAutoUpdate -> disableAutoUpdate
|
||||
for (const [oldV2Path, v1Key] of Object.entries(OLD_V2_TO_V1_MAP)) {
|
||||
const value = getNestedProperty(v2Settings, oldV2Path);
|
||||
if (value !== undefined) {
|
||||
v1Settings[v1Key] = value;
|
||||
v2Keys.delete(oldV2Path.split('.')[0]);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle new V3 inverted paths (WITH value inversion)
|
||||
// e.g., general.enableAutoUpdate -> disableAutoUpdate (inverted)
|
||||
for (const [v3Path, v1Key] of Object.entries(V3_TO_V1_INVERTED_MAP)) {
|
||||
const value = getNestedProperty(v2Settings, v3Path);
|
||||
if (value !== undefined && typeof value === 'boolean') {
|
||||
v1Settings[v1Key] = !value;
|
||||
v2Keys.delete(v3Path.split('.')[0]);
|
||||
}
|
||||
}
|
||||
|
||||
// Preserve mcpServers at the top level
|
||||
if (v2Settings['mcpServers']) {
|
||||
v1Settings['mcpServers'] = v2Settings['mcpServers'];
|
||||
|
|
@ -737,7 +949,7 @@ export function loadSettings(
|
|||
|
||||
let settingsObject = rawSettings as Record<string, unknown>;
|
||||
if (needsMigration(settingsObject)) {
|
||||
const migratedSettings = migrateSettingsToV2(settingsObject);
|
||||
const migratedSettings = migrateV1ToV3(settingsObject);
|
||||
if (migratedSettings) {
|
||||
if (MIGRATE_V2_OVERWRITE) {
|
||||
try {
|
||||
|
|
@ -776,6 +988,33 @@ export function loadSettings(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// V2 to V3 migration (invert disable* -> enable* booleans)
|
||||
const v3Migrated = migrateV2ToV3(settingsObject);
|
||||
if (v3Migrated) {
|
||||
if (MIGRATE_V2_OVERWRITE) {
|
||||
try {
|
||||
// Only backup if not already backed up by V1->V2 migration
|
||||
const backupPath = `${filePath}.orig`;
|
||||
if (!fs.existsSync(backupPath)) {
|
||||
fs.renameSync(filePath, backupPath);
|
||||
}
|
||||
fs.writeFileSync(
|
||||
filePath,
|
||||
JSON.stringify(v3Migrated, null, 2),
|
||||
'utf-8',
|
||||
);
|
||||
} catch (e) {
|
||||
writeStderrLine(
|
||||
`Error migrating settings file to V3: ${getErrorMessage(e)}`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
migratedInMemorScopes.add(scope);
|
||||
}
|
||||
settingsObject = v3Migrated;
|
||||
}
|
||||
|
||||
return { settings: settingsObject as Settings, rawJson: content };
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue