🐛 fix(vscode): 设置写入 globalStorage 文件,更新不再丢提交 profile

This commit is contained in:
cruldra committed 2026-09-01 16:39:54 +08:00
1 parent 3a912fce3e
commit b2d0d17584
3 files changed
+164 -14

No files matched your search

+71 -13
View File
@@ -1,9 +1,10 @@
/**
* Non-secret extension settings persisted in `globalState`.
* Non-secret extension settings persisted as `settings.json` under
* `context.globalStorageUri`. That directory survives VSIX 更新;`globalState`
* 在部分 VS Code profile 下只活在内存里,一重载就丢。
*
* Token stays in `context.secrets` (see `src/auth/secrets.ts`); everything
* else (webhook port, prompt templates) lives here under a single JSON blob
* keyed by `SETTINGS_KEY`.
* Token stays in `context.secrets` (see `src/auth/secrets.ts`).
* 读:文件 → 旧 globalState(一次性迁移)。写:只写文件。
*
* Empty-string values for prompts are treated as "use the default" so the
* form can save user input verbatim and still fall back when the user clears
@@ -11,7 +12,7 @@
*/
import type { ExtensionContext } from 'vscode'
import { readFileSync } from 'node:fs'
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'
import path from 'node:path'
import { seedCommitProfilePath } from '../cc/commitProfile'
import { listClaudeProfiles } from '../cc/profiles'
@@ -208,6 +209,40 @@ export interface Settings {
}
export const SETTINGS_KEY = 'superpowers.settings'
const SETTINGS_FILE = 'settings.json'
export function settingsFilePath(ctx: ExtensionContext): string {
return path.join(ctx.globalStorageUri.fsPath, SETTINGS_FILE)
}
function parseStoredBlob(raw: unknown): Partial<Settings> | null {
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
return null
return raw as Partial<Settings>
}
/** 文件优先;没有文件时读旧 globalState,供迁移。 */
export function readStoredSettings(ctx: ExtensionContext): Partial<Settings> {
try {
const parsed: unknown = JSON.parse(readFileSync(settingsFilePath(ctx), 'utf8'))
const blob = parseStoredBlob(parsed)
if (blob)
return blob
}
catch {
// 文件不存在或坏掉:回退 memento。
}
return parseStoredBlob(ctx.globalState.get(SETTINGS_KEY)) ?? {}
}
function writeStoredSettings(ctx: ExtensionContext, blob: Partial<Settings>): void {
const dir = ctx.globalStorageUri.fsPath
mkdirSync(dir, { recursive: true })
const file = settingsFilePath(ctx)
const tmp = `${file}.tmp`
writeFileSync(tmp, `${JSON.stringify(blob, null, 2)}\n`)
renameSync(tmp, file)
}
function defaults(ctx: ExtensionContext): Settings {
return {
@@ -242,7 +277,7 @@ function defaults(ctx: ExtensionContext): Settings {
}
export function getSettings(ctx: ExtensionContext): Settings {
const stored = ctx.globalState.get<Partial<Settings>>(SETTINGS_KEY) ?? {}
const stored = readStoredSettings(ctx)
const base = defaults(ctx)
const webhookPort = typeof stored.webhookPort === 'number' && Number.isInteger(stored.webhookPort)
@@ -384,11 +419,11 @@ export function getSettings(ctx: ExtensionContext): Settings {
/**
* 读取工具栏「提交代码」有效的 profile path。
* ① raw stored 已有 key(含用户清空的 '')→ 原样返回
* ② key 从未出现 → list profiles 后 seed deepseek-v4-pro(不写回磁盘)
* ① stored 已有 key(含用户清空的 '')→ 原样返回
* ② key 从未出现 → list profiles 后 seed deepseek-v4-pro(启动时 persistSeededProfiles 会写盘)
*/
export async function getEffectiveCommitProfilePath(ctx: ExtensionContext): Promise<string> {
const stored = ctx.globalState.get<Partial<Settings>>(SETTINGS_KEY) ?? {}
const stored = readStoredSettings(ctx)
if (typeof stored.commitProfilePath === 'string')
return stored.commitProfilePath
const profiles = await listClaudeProfiles()
@@ -399,7 +434,7 @@ export async function getEffectiveCommitProfilePath(ctx: ExtensionContext): Prom
* 读取改动说明有效的 profile path。规则与提交代码相同:已有 key 原样返回,缺 key 才 seed。
*/
export async function getEffectivePrDiffSummaryProfilePath(ctx: ExtensionContext): Promise<string> {
const stored = ctx.globalState.get<Partial<Settings>>(SETTINGS_KEY) ?? {}
const stored = readStoredSettings(ctx)
if (typeof stored.prDiffSummaryProfilePath === 'string')
return stored.prDiffSummaryProfilePath
const profiles = await listClaudeProfiles()
@@ -407,7 +442,30 @@ export async function getEffectivePrDiffSummaryProfilePath(ctx: ExtensionContext
}
export async function saveSettings(ctx: ExtensionContext, next: Partial<Settings>): Promise<void> {
const current = ctx.globalState.get<Partial<Settings>>(SETTINGS_KEY) ?? {}
const merged: Partial<Settings> = { ...current, ...next }
await ctx.globalState.update(SETTINGS_KEY, merged)
const merged: Partial<Settings> = { ...readStoredSettings(ctx), ...next }
writeStoredSettings(ctx, merged)
}
/**
* 启动时:把旧 memento 迁到文件;commit / 改动说明 profile 缺 key 且能 seed 则写盘。
* 空串视为用户主动清空,不覆盖。
*/
export async function persistSeededProfiles(ctx: ExtensionContext): Promise<void> {
const stored = readStoredSettings(ctx)
const fileExists = existsSync(settingsFilePath(ctx))
const profiles = await listClaudeProfiles()
const patch: Partial<Settings> = {}
if (typeof stored.commitProfilePath !== 'string') {
const seeded = seedCommitProfilePath(undefined, profiles)
if (seeded)
patch.commitProfilePath = seeded
}
if (typeof stored.prDiffSummaryProfilePath !== 'string') {
const seeded = seedCommitProfilePath(undefined, profiles)
if (seeded)
patch.prDiffSummaryProfilePath = seeded
}
const shouldMigrate = !fileExists && Object.keys(stored).length > 0
if (shouldMigrate || Object.keys(patch).length > 0)
await saveSettings(ctx, patch)
}