[codex] add locked file updates for config writes (#785)
* add locked file updates for config writes * add glm vision turbo preset
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
import { readFile, writeFile, copyFile } from 'fs/promises'
|
||||
import YAML from 'js-yaml'
|
||||
import { readFile } from 'fs/promises'
|
||||
import { getGatewayManagerInstance } from '../../services/gateway-bootstrap'
|
||||
import { getActiveConfigPath, getActiveEnvPath } from '../../services/hermes/hermes-profile'
|
||||
import { saveEnvValue } from '../../services/config-helpers'
|
||||
import { logger } from '../../services/logger'
|
||||
import { safeFileStore } from '../../services/safe-file-store'
|
||||
|
||||
const PLATFORM_SECTIONS = new Set([
|
||||
'telegram', 'discord', 'slack', 'whatsapp', 'matrix',
|
||||
@@ -90,20 +90,7 @@ async function readEnvPlatforms(): Promise<Record<string, any>> {
|
||||
}
|
||||
|
||||
async function readConfig(): Promise<Record<string, any>> {
|
||||
const raw = await readFile(configPath(), 'utf-8')
|
||||
return (YAML.load(raw, { json: true }) as Record<string, any>) || {}
|
||||
}
|
||||
|
||||
async function writeConfig(data: Record<string, any>): Promise<void> {
|
||||
const cp = configPath()
|
||||
await copyFile(cp, cp + '.bak')
|
||||
const yamlStr = YAML.dump(data, {
|
||||
lineWidth: -1,
|
||||
noRefs: true,
|
||||
quotingType: '"',
|
||||
forceQuotes: true, // Force quotes on all string values
|
||||
})
|
||||
await writeFile(cp, yamlStr, 'utf-8')
|
||||
return safeFileStore.readYaml(configPath())
|
||||
}
|
||||
|
||||
export async function getConfig(ctx: any) {
|
||||
@@ -139,9 +126,15 @@ export async function updateConfig(ctx: any) {
|
||||
ctx.status = 400; ctx.body = { error: 'Missing section or values' }; return
|
||||
}
|
||||
try {
|
||||
const config = await readConfig()
|
||||
config[section] = deepMerge(config[section] || {}, values)
|
||||
await writeConfig(config)
|
||||
await safeFileStore.updateYaml(configPath(), (config) => {
|
||||
config[section] = deepMerge(config[section] || {}, values)
|
||||
return config
|
||||
}, {
|
||||
backup: true,
|
||||
dumpOptions: {
|
||||
forceQuotes: true,
|
||||
},
|
||||
})
|
||||
|
||||
// 使用 GatewayManager 重启平台网关
|
||||
if (PLATFORM_SECTIONS.has(section)) {
|
||||
@@ -173,37 +166,41 @@ export async function updateCredentials(ctx: any) {
|
||||
if (!envMap) {
|
||||
ctx.status = 400; ctx.body = { error: `Unknown platform: ${platform}` }; return
|
||||
}
|
||||
const config = await readConfig()
|
||||
let configChanged = false
|
||||
const flatValues: Record<string, any> = {}
|
||||
for (const [key, val] of Object.entries(values)) {
|
||||
if (key === 'extra' && val && typeof val === 'object') {
|
||||
for (const [subKey, subVal] of Object.entries(val as Record<string, any>)) { flatValues[`extra.${subKey}`] = subVal }
|
||||
} else { flatValues[key] = val }
|
||||
}
|
||||
for (const [cfgPath, val] of Object.entries(flatValues)) {
|
||||
const envVar = envMap[cfgPath]
|
||||
if (!envVar) continue
|
||||
if (val === undefined || val === null || val === '') {
|
||||
await saveEnvValue(envVar, '')
|
||||
const parts = cfgPath.split('.')
|
||||
let obj: any = config.platforms?.[platform]
|
||||
if (obj) {
|
||||
if (parts.length === 1) { delete obj[parts[0]] }
|
||||
else {
|
||||
let cur = obj
|
||||
for (let i = 0; i < parts.length - 1; i++) { if (!cur[parts[i]]) break; cur = cur[parts[i]] }
|
||||
delete cur[parts[parts.length - 1]]
|
||||
if (obj.extra && Object.keys(obj.extra).length === 0) delete obj.extra
|
||||
await safeFileStore.updateYaml(configPath(), async (config) => {
|
||||
for (const [cfgPath, val] of Object.entries(flatValues)) {
|
||||
const envVar = envMap[cfgPath]
|
||||
if (!envVar) continue
|
||||
if (val === undefined || val === null || val === '') {
|
||||
await saveEnvValue(envVar, '')
|
||||
const parts = cfgPath.split('.')
|
||||
let obj: any = config.platforms?.[platform]
|
||||
if (obj) {
|
||||
if (parts.length === 1) { delete obj[parts[0]] }
|
||||
else {
|
||||
let cur = obj
|
||||
for (let i = 0; i < parts.length - 1; i++) { if (!cur[parts[i]]) break; cur = cur[parts[i]] }
|
||||
delete cur[parts[parts.length - 1]]
|
||||
if (obj.extra && Object.keys(obj.extra).length === 0) delete obj.extra
|
||||
}
|
||||
if (Object.keys(obj).length === 0) { if (!config.platforms) config.platforms = {}; delete config.platforms[platform] }
|
||||
}
|
||||
if (Object.keys(obj).length === 0) { if (!config.platforms) config.platforms = {}; delete config.platforms[platform] }
|
||||
configChanged = true
|
||||
} else {
|
||||
await saveEnvValue(envVar, String(val))
|
||||
}
|
||||
} else {
|
||||
await saveEnvValue(envVar, String(val))
|
||||
}
|
||||
}
|
||||
if (configChanged) { await writeConfig(config) }
|
||||
return config
|
||||
}, {
|
||||
backup: true,
|
||||
dumpOptions: {
|
||||
forceQuotes: true,
|
||||
},
|
||||
})
|
||||
|
||||
// 使用 GatewayManager 重启平台网关
|
||||
const mgr = getGatewayManagerInstance()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { randomUUID } from 'crypto'
|
||||
import { startDeviceFlow, pollDeviceFlow } from '../../services/hermes/copilot-device-flow'
|
||||
import { saveEnvValue, readConfigYaml, writeConfigYaml } from '../../services/config-helpers'
|
||||
import { saveEnvValue, updateConfigYaml } from '../../services/config-helpers'
|
||||
import {
|
||||
invalidateAllCaches,
|
||||
resolveCopilotOAuthTokenWithSource,
|
||||
@@ -192,16 +192,17 @@ export async function disable(ctx: any): Promise<void> {
|
||||
// 不能 swallow —— 否则会出现 "list 已隐藏 copilot 但 default 仍是 copilot" 的中间态。
|
||||
let clearedDefault = false
|
||||
try {
|
||||
const cfg = await readConfigYaml()
|
||||
const modelSection = cfg.model
|
||||
if (typeof modelSection === 'object' && modelSection !== null) {
|
||||
const provider = String(modelSection.provider || '').trim().toLowerCase()
|
||||
if (provider === 'copilot') {
|
||||
cfg.model = {}
|
||||
await writeConfigYaml(cfg)
|
||||
clearedDefault = true
|
||||
clearedDefault = await updateConfigYaml((cfg) => {
|
||||
const modelSection = cfg.model
|
||||
if (typeof modelSection === 'object' && modelSection !== null) {
|
||||
const provider = String(modelSection.provider || '').trim().toLowerCase()
|
||||
if (provider === 'copilot') {
|
||||
cfg.model = {}
|
||||
return { data: cfg, result: true }
|
||||
}
|
||||
}
|
||||
}
|
||||
return { data: cfg, result: false, write: false }
|
||||
}) || false
|
||||
} catch (err: any) {
|
||||
logger.error(err, 'Copilot disable failed: cannot clear default model')
|
||||
ctx.status = 500
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { readFile } from 'fs/promises'
|
||||
import { existsSync, readFileSync } from 'fs'
|
||||
import { getActiveEnvPath, getActiveAuthPath } from '../../services/hermes/hermes-profile'
|
||||
import { readConfigYaml, writeConfigYaml, fetchProviderModels, buildModelGroups, PROVIDER_ENV_MAP } from '../../services/config-helpers'
|
||||
import { readConfigYaml, updateConfigYaml, fetchProviderModels, buildModelGroups, PROVIDER_ENV_MAP } from '../../services/config-helpers'
|
||||
import { buildProviderModelMap, PROVIDER_PRESETS } from '../../shared/providers'
|
||||
import { getCopilotModelsDetailed, resolveCopilotOAuthToken, type CopilotModelMeta } from '../../services/hermes/copilot-models'
|
||||
import { readAppConfig, writeAppConfig, type ModelVisibilityRule } from '../../services/app-config'
|
||||
@@ -483,11 +483,12 @@ export async function setConfigModel(ctx: any) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const config = await readConfigYaml()
|
||||
config.model = {}
|
||||
config.model.default = defaultModel
|
||||
if (reqProvider) { config.model.provider = reqProvider }
|
||||
await writeConfigYaml(config)
|
||||
await updateConfigYaml((config) => {
|
||||
config.model = {}
|
||||
config.model.default = defaultModel
|
||||
if (reqProvider) { config.model.provider = reqProvider }
|
||||
return config
|
||||
})
|
||||
ctx.body = { success: true }
|
||||
} catch (err: any) {
|
||||
ctx.status = 500
|
||||
|
||||
@@ -2,7 +2,7 @@ import { existsSync, readFileSync } from 'fs'
|
||||
import { writeFile } from 'fs/promises'
|
||||
import { getActiveAuthPath } from '../../services/hermes/hermes-profile'
|
||||
import * as hermesCli from '../../services/hermes/hermes-cli'
|
||||
import { readConfigYaml, writeConfigYaml, saveEnvValue, PROVIDER_ENV_MAP } from '../../services/config-helpers'
|
||||
import { updateConfigYaml, saveEnvValue, PROVIDER_ENV_MAP } from '../../services/config-helpers'
|
||||
import { PROVIDER_PRESETS } from '../../shared/providers'
|
||||
import { logger } from '../../services/logger'
|
||||
|
||||
@@ -50,48 +50,18 @@ export async function create(ctx: any) {
|
||||
try {
|
||||
const poolKey = providerKey || `custom:${name.trim().toLowerCase().replace(/ /g, '-')}`
|
||||
const isBuiltin = poolKey in PROVIDER_ENV_MAP
|
||||
const config = await readConfigYaml()
|
||||
if (typeof config.model !== 'object' || config.model === null) { config.model = {} }
|
||||
if (!isBuiltin) {
|
||||
if (!Array.isArray(config.custom_providers)) { config.custom_providers = [] }
|
||||
const existing = (config.custom_providers as any[]).find(
|
||||
(e: any) => `custom:${e.name}` === poolKey
|
||||
)
|
||||
if (existing) {
|
||||
existing.base_url = base_url
|
||||
existing.api_key = api_key
|
||||
existing.model = model
|
||||
const preset = PROVIDER_PRESETS.find(p => p.value === poolKey.replace('custom:', ''))
|
||||
if (preset?.api_mode) existing.api_mode = preset.api_mode
|
||||
if (context_length && context_length > 0) {
|
||||
if (!existing.models) existing.models = {}
|
||||
existing.models[model] = existing.models[model] || {}
|
||||
existing.models[model].context_length = context_length
|
||||
}
|
||||
} else {
|
||||
const entry = buildProviderEntry(name.trim().toLowerCase().replace(/ /g, '-'), base_url, api_key, model, context_length)
|
||||
const preset = PROVIDER_PRESETS.find(p => p.value === poolKey.replace('custom:', ''))
|
||||
if (preset?.api_mode) entry.api_mode = preset.api_mode
|
||||
config.custom_providers.push(entry)
|
||||
}
|
||||
config.model.default = model
|
||||
config.model.provider = poolKey
|
||||
} else {
|
||||
if (PROVIDER_ENV_MAP[poolKey].api_key_env) {
|
||||
await saveEnvValue(PROVIDER_ENV_MAP[poolKey].api_key_env, api_key)
|
||||
if (PROVIDER_ENV_MAP[poolKey].base_url_env) { await saveEnvValue(PROVIDER_ENV_MAP[poolKey].base_url_env, base_url) }
|
||||
config.model.default = model
|
||||
config.model.provider = poolKey
|
||||
} else {
|
||||
await updateConfigYaml(async (config) => {
|
||||
if (typeof config.model !== 'object' || config.model === null) { config.model = {} }
|
||||
if (!isBuiltin) {
|
||||
if (!Array.isArray(config.custom_providers)) { config.custom_providers = [] }
|
||||
const existing = (config.custom_providers as any[]).find(
|
||||
(e: any) => `custom:${e.name}` === `custom:${poolKey}`
|
||||
(e: any) => `custom:${e.name}` === poolKey
|
||||
)
|
||||
if (existing) {
|
||||
existing.base_url = base_url
|
||||
existing.api_key = api_key
|
||||
existing.model = model
|
||||
const preset = PROVIDER_PRESETS.find(p => p.value === poolKey)
|
||||
const preset = PROVIDER_PRESETS.find(p => p.value === poolKey.replace('custom:', ''))
|
||||
if (preset?.api_mode) existing.api_mode = preset.api_mode
|
||||
if (context_length && context_length > 0) {
|
||||
if (!existing.models) existing.models = {}
|
||||
@@ -99,18 +69,49 @@ export async function create(ctx: any) {
|
||||
existing.models[model].context_length = context_length
|
||||
}
|
||||
} else {
|
||||
const entry = buildProviderEntry(poolKey, base_url, api_key, model, context_length)
|
||||
const preset = PROVIDER_PRESETS.find(p => p.value === poolKey)
|
||||
const entry = buildProviderEntry(name.trim().toLowerCase().replace(/ /g, '-'), base_url, api_key, model, context_length)
|
||||
const preset = PROVIDER_PRESETS.find(p => p.value === poolKey.replace('custom:', ''))
|
||||
if (preset?.api_mode) entry.api_mode = preset.api_mode
|
||||
config.custom_providers.push(entry)
|
||||
}
|
||||
config.model.default = model
|
||||
config.model.provider = `custom:${poolKey}`
|
||||
config.model.provider = poolKey
|
||||
} else {
|
||||
if (PROVIDER_ENV_MAP[poolKey].api_key_env) {
|
||||
await saveEnvValue(PROVIDER_ENV_MAP[poolKey].api_key_env, api_key)
|
||||
if (PROVIDER_ENV_MAP[poolKey].base_url_env) { await saveEnvValue(PROVIDER_ENV_MAP[poolKey].base_url_env, base_url) }
|
||||
config.model.default = model
|
||||
config.model.provider = poolKey
|
||||
} else {
|
||||
if (!Array.isArray(config.custom_providers)) { config.custom_providers = [] }
|
||||
const existing = (config.custom_providers as any[]).find(
|
||||
(e: any) => `custom:${e.name}` === `custom:${poolKey}`
|
||||
)
|
||||
if (existing) {
|
||||
existing.base_url = base_url
|
||||
existing.api_key = api_key
|
||||
existing.model = model
|
||||
const preset = PROVIDER_PRESETS.find(p => p.value === poolKey)
|
||||
if (preset?.api_mode) existing.api_mode = preset.api_mode
|
||||
if (context_length && context_length > 0) {
|
||||
if (!existing.models) existing.models = {}
|
||||
existing.models[model] = existing.models[model] || {}
|
||||
existing.models[model].context_length = context_length
|
||||
}
|
||||
} else {
|
||||
const entry = buildProviderEntry(poolKey, base_url, api_key, model, context_length)
|
||||
const preset = PROVIDER_PRESETS.find(p => p.value === poolKey)
|
||||
if (preset?.api_mode) entry.api_mode = preset.api_mode
|
||||
config.custom_providers.push(entry)
|
||||
}
|
||||
config.model.default = model
|
||||
config.model.provider = `custom:${poolKey}`
|
||||
}
|
||||
}
|
||||
}
|
||||
delete config.model.base_url
|
||||
delete config.model.api_key
|
||||
await writeConfigYaml(config)
|
||||
delete config.model.base_url
|
||||
delete config.model.api_key
|
||||
return config
|
||||
})
|
||||
// TODO: Test if provider works without gateway restart
|
||||
// try { await hermesCli.restartGateway() } catch (e: any) { logger.error(e, 'Gateway restart failed') }
|
||||
ctx.body = { success: true }
|
||||
@@ -127,21 +128,21 @@ export async function update(ctx: any) {
|
||||
try {
|
||||
const isCustom = poolKey.startsWith('custom:')
|
||||
if (isCustom) {
|
||||
const config = await readConfigYaml()
|
||||
if (!Array.isArray(config.custom_providers)) {
|
||||
ctx.status = 404; ctx.body = { error: `Custom provider "${poolKey}" not found` }; return
|
||||
}
|
||||
const entry = (config.custom_providers as any[]).find((e: any) => {
|
||||
return `custom:${e.name.trim().toLowerCase().replace(/ /g, '-')}` === poolKey
|
||||
const found = await updateConfigYaml((config) => {
|
||||
if (!Array.isArray(config.custom_providers)) return { data: config, result: false, write: false }
|
||||
const entry = (config.custom_providers as any[]).find((e: any) => {
|
||||
return `custom:${e.name.trim().toLowerCase().replace(/ /g, '-')}` === poolKey
|
||||
})
|
||||
if (!entry) return { data: config, result: false, write: false }
|
||||
if (name !== undefined) entry.name = name
|
||||
if (base_url !== undefined) entry.base_url = base_url
|
||||
if (api_key !== undefined) entry.api_key = api_key
|
||||
if (model !== undefined) entry.model = model
|
||||
return { data: config, result: true }
|
||||
})
|
||||
if (!entry) {
|
||||
if (!found) {
|
||||
ctx.status = 404; ctx.body = { error: `Custom provider "${poolKey}" not found` }; return
|
||||
}
|
||||
if (name !== undefined) entry.name = name
|
||||
if (base_url !== undefined) entry.base_url = base_url
|
||||
if (api_key !== undefined) entry.api_key = api_key
|
||||
if (model !== undefined) entry.model = model
|
||||
await writeConfigYaml(config)
|
||||
} else {
|
||||
const envMapping = PROVIDER_ENV_MAP[poolKey]
|
||||
if (!envMapping?.api_key_env) {
|
||||
@@ -160,46 +161,49 @@ export async function update(ctx: any) {
|
||||
export async function remove(ctx: any) {
|
||||
const poolKey = decodeURIComponent(ctx.params.poolKey)
|
||||
try {
|
||||
const config = await readConfigYaml()
|
||||
const isCustom = poolKey.startsWith('custom:')
|
||||
if (isCustom) {
|
||||
const idx = Array.isArray(config.custom_providers)
|
||||
? (config.custom_providers as any[]).findIndex((e: any) => {
|
||||
return `custom:${e.name.trim().toLowerCase().replace(/ /g, '-')}` === poolKey
|
||||
})
|
||||
: -1
|
||||
if (idx === -1) {
|
||||
ctx.status = 404; ctx.body = { error: `Custom provider "${poolKey}" not found` }; return
|
||||
}
|
||||
;(config.custom_providers as any[]).splice(idx, 1)
|
||||
await writeConfigYaml(config)
|
||||
await clearStoredAuthProvider(poolKey)
|
||||
} else {
|
||||
const envMapping = PROVIDER_ENV_MAP[poolKey]
|
||||
if (envMapping?.api_key_env) {
|
||||
await saveEnvValue(envMapping.api_key_env, '')
|
||||
if (envMapping.base_url_env) { await saveEnvValue(envMapping.base_url_env, '') }
|
||||
}
|
||||
await clearStoredAuthProvider(poolKey)
|
||||
}
|
||||
const currentProvider = config.model?.provider
|
||||
if (currentProvider === poolKey) {
|
||||
const freshConfig = await readConfigYaml()
|
||||
const remaining = Array.isArray(freshConfig.custom_providers) ? freshConfig.custom_providers as any[] : []
|
||||
if (remaining.length > 0) {
|
||||
const fallbackCp = remaining[0]
|
||||
const fallbackKey = `custom:${fallbackCp.name.trim().toLowerCase().replace(/ /g, '-')}`
|
||||
if (typeof freshConfig.model !== 'object' || freshConfig.model === null) { freshConfig.model = {} }
|
||||
freshConfig.model.default = fallbackCp.model
|
||||
freshConfig.model.provider = fallbackKey
|
||||
delete freshConfig.model.base_url
|
||||
delete freshConfig.model.api_key
|
||||
await writeConfigYaml(freshConfig)
|
||||
const removed = await updateConfigYaml(async (config) => {
|
||||
if (isCustom) {
|
||||
const idx = Array.isArray(config.custom_providers)
|
||||
? (config.custom_providers as any[]).findIndex((e: any) => {
|
||||
return `custom:${e.name.trim().toLowerCase().replace(/ /g, '-')}` === poolKey
|
||||
})
|
||||
: -1
|
||||
if (idx === -1) return { data: config, result: false, write: false }
|
||||
;(config.custom_providers as any[]).splice(idx, 1)
|
||||
} else {
|
||||
freshConfig.model = {}
|
||||
await writeConfigYaml(freshConfig)
|
||||
const envMapping = PROVIDER_ENV_MAP[poolKey]
|
||||
if (envMapping?.api_key_env) {
|
||||
await saveEnvValue(envMapping.api_key_env, '')
|
||||
if (envMapping.base_url_env) { await saveEnvValue(envMapping.base_url_env, '') }
|
||||
}
|
||||
}
|
||||
if (config.model?.provider === poolKey) {
|
||||
const remaining = Array.isArray(config.custom_providers) ? config.custom_providers as any[] : []
|
||||
if (remaining.length > 0) {
|
||||
const fallbackCp = remaining[0]
|
||||
const fallbackKey = `custom:${fallbackCp.name.trim().toLowerCase().replace(/ /g, '-')}`
|
||||
if (typeof config.model !== 'object' || config.model === null) { config.model = {} }
|
||||
config.model.default = fallbackCp.model
|
||||
config.model.provider = fallbackKey
|
||||
delete config.model.base_url
|
||||
delete config.model.api_key
|
||||
} else {
|
||||
config.model = {}
|
||||
}
|
||||
}
|
||||
return { data: config, result: true }
|
||||
})
|
||||
if (!removed) {
|
||||
ctx.status = 404; ctx.body = { error: `Custom provider "${poolKey}" not found` }; return
|
||||
}
|
||||
if (!isCustom) {
|
||||
const envMapping = PROVIDER_ENV_MAP[poolKey]
|
||||
if (!envMapping) {
|
||||
ctx.status = 404; ctx.body = { error: `Provider "${poolKey}" not found` }; return
|
||||
}
|
||||
}
|
||||
await clearStoredAuthProvider(poolKey)
|
||||
// TODO: Test if provider works without gateway restart
|
||||
// try { await hermesCli.restartGateway() } catch (e: any) { logger.error(e, 'Gateway restart failed') }
|
||||
ctx.body = { success: true }
|
||||
|
||||
@@ -2,7 +2,7 @@ import { readdir, readFile } from 'fs/promises'
|
||||
import { join, resolve } from 'path'
|
||||
import { createHash } from 'crypto'
|
||||
import {
|
||||
readConfigYaml, writeConfigYaml,
|
||||
readConfigYaml, updateConfigYaml,
|
||||
safeReadFile, extractDescription, listFilesRecursive, getHermesDir,
|
||||
} from '../../services/config-helpers'
|
||||
import { pinSkill } from '../../services/hermes/hermes-cli'
|
||||
@@ -260,14 +260,15 @@ export async function toggle(ctx: any) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const config = await readConfigYaml()
|
||||
if (!config.skills) config.skills = {}
|
||||
if (!Array.isArray(config.skills.disabled)) config.skills.disabled = []
|
||||
const disabled = config.skills.disabled as string[]
|
||||
const idx = disabled.indexOf(name)
|
||||
if (enabled) { if (idx !== -1) disabled.splice(idx, 1) }
|
||||
else { if (idx === -1) disabled.push(name) }
|
||||
await writeConfigYaml(config)
|
||||
await updateConfigYaml((config) => {
|
||||
if (!config.skills) config.skills = {}
|
||||
if (!Array.isArray(config.skills.disabled)) config.skills.disabled = []
|
||||
const disabled = config.skills.disabled as string[]
|
||||
const idx = disabled.indexOf(name)
|
||||
if (enabled) { if (idx !== -1) disabled.splice(idx, 1) }
|
||||
else { if (idx === -1) disabled.push(name) }
|
||||
return config
|
||||
})
|
||||
ctx.body = { success: true }
|
||||
} catch (err: any) {
|
||||
ctx.status = 500
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import axios from 'axios'
|
||||
import { readFile, writeFile, chmod } from 'fs/promises'
|
||||
import { chmod } from 'fs/promises'
|
||||
import { getActiveEnvPath } from '../../services/hermes/hermes-profile'
|
||||
import { restartGateway } from '../../services/hermes/hermes-cli'
|
||||
import { safeFileStore } from '../../services/safe-file-store'
|
||||
|
||||
const ILINK_BASE = 'https://ilinkai.weixin.qq.com'
|
||||
const envPath = () => getActiveEnvPath()
|
||||
@@ -38,27 +39,26 @@ export async function save(ctx: any) {
|
||||
const { account_id, token, base_url } = ctx.request.body as { account_id: string; token: string; base_url?: string }
|
||||
if (!account_id || !token) { ctx.status = 400; ctx.body = { error: 'Missing account_id or token' }; return }
|
||||
try {
|
||||
let raw: string
|
||||
try { raw = await readFile(envPath(), 'utf-8') } catch { raw = '' }
|
||||
const entries: Record<string, string> = { WEIXIN_ACCOUNT_ID: account_id, WEIXIN_TOKEN: token }
|
||||
if (base_url) entries.WEIXIN_BASE_URL = base_url
|
||||
const lines = raw.split('\n')
|
||||
const existingKeys = new Set<string>()
|
||||
const result: string[] = []
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim()
|
||||
if (trimmed.startsWith('#')) { result.push(line); continue }
|
||||
const eqIdx = trimmed.indexOf('=')
|
||||
if (eqIdx !== -1) {
|
||||
const key = trimmed.slice(0, eqIdx).trim()
|
||||
if (key in entries) { result.push(`${key}=${entries[key]}`); existingKeys.add(key); continue }
|
||||
}
|
||||
result.push(line)
|
||||
}
|
||||
for (const [key, val] of Object.entries(entries)) { if (!existingKeys.has(key)) { result.push(`${key}=${val}`) } }
|
||||
let output = result.join('\n').replace(/\n{3,}/g, '\n\n').replace(/\n+$/, '') + '\n'
|
||||
const ep = envPath()
|
||||
await writeFile(ep, output, 'utf-8')
|
||||
await safeFileStore.updateText(ep, (raw) => {
|
||||
const lines = raw.split('\n')
|
||||
const existingKeys = new Set<string>()
|
||||
const result: string[] = []
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim()
|
||||
if (trimmed.startsWith('#')) { result.push(line); continue }
|
||||
const eqIdx = trimmed.indexOf('=')
|
||||
if (eqIdx !== -1) {
|
||||
const key = trimmed.slice(0, eqIdx).trim()
|
||||
if (key in entries) { result.push(`${key}=${entries[key]}`); existingKeys.add(key); continue }
|
||||
}
|
||||
result.push(line)
|
||||
}
|
||||
for (const [key, val] of Object.entries(entries)) { if (!existingKeys.has(key)) { result.push(`${key}=${val}`) } }
|
||||
return result.join('\n').replace(/\n{3,}/g, '\n\n').replace(/\n+$/, '') + '\n'
|
||||
})
|
||||
try { await chmod(ep, 0o600) } catch { }
|
||||
await restartGateway()
|
||||
ctx.body = { success: true }
|
||||
|
||||
Reference in New Issue
Block a user