fix(sync): support commented/trailing-comma OpenClaw config (#235)
This commit is contained in:
parent
2b28b8ebe2
commit
d92e01d64f
|
|
@ -4,6 +4,7 @@ import { logAuditEvent } from '@/lib/db'
|
|||
import { config } from '@/lib/config'
|
||||
import { validateBody, gatewayConfigUpdateSchema } from '@/lib/validation'
|
||||
import { mutationLimiter } from '@/lib/rate-limit'
|
||||
import { parseJsonRelaxed } from '@/lib/json-relaxed'
|
||||
|
||||
function getConfigPath(): string | null {
|
||||
return config.openclawConfigPath || null
|
||||
|
|
@ -24,7 +25,7 @@ export async function GET(request: NextRequest) {
|
|||
try {
|
||||
const { readFile } = require('fs/promises')
|
||||
const raw = await readFile(configPath, 'utf-8')
|
||||
const parsed = JSON.parse(raw)
|
||||
const parsed = parseJsonRelaxed<any>(raw)
|
||||
|
||||
// Redact sensitive fields for display
|
||||
const redacted = redactSensitive(JSON.parse(JSON.stringify(parsed)))
|
||||
|
|
@ -76,7 +77,7 @@ export async function PUT(request: NextRequest) {
|
|||
try {
|
||||
const { readFile, writeFile } = require('fs/promises')
|
||||
const raw = await readFile(configPath, 'utf-8')
|
||||
const parsed = JSON.parse(raw)
|
||||
const parsed = parseJsonRelaxed<any>(raw)
|
||||
|
||||
// Apply updates via dot-notation
|
||||
const appliedKeys: string[] = []
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import { parseJsonRelaxed } from '@/lib/json-relaxed'
|
||||
|
||||
describe('parseJsonRelaxed', () => {
|
||||
it('parses strict JSON unchanged', () => {
|
||||
const parsed = parseJsonRelaxed<{ a: number; b: string }>('{"a":1,"b":"ok"}')
|
||||
expect(parsed).toEqual({ a: 1, b: 'ok' })
|
||||
})
|
||||
|
||||
it('parses JSON with line comments and trailing commas', () => {
|
||||
const raw = `{
|
||||
// top-level comment
|
||||
"agents": {
|
||||
"list": [
|
||||
{ "id": "a", "name": "A", },
|
||||
],
|
||||
},
|
||||
}`
|
||||
|
||||
const parsed = parseJsonRelaxed<any>(raw)
|
||||
expect(parsed.agents.list[0].id).toBe('a')
|
||||
expect(parsed.agents.list[0].name).toBe('A')
|
||||
})
|
||||
|
||||
it('parses JSON with block comments', () => {
|
||||
const raw = `{
|
||||
/* comment */
|
||||
"gateway": { "port": 18789 }
|
||||
}`
|
||||
|
||||
const parsed = parseJsonRelaxed<any>(raw)
|
||||
expect(parsed.gateway.port).toBe(18789)
|
||||
})
|
||||
|
||||
it('does not strip URL fragments inside strings', () => {
|
||||
const raw = `{
|
||||
"url": "https://example.com/a//b",
|
||||
"ok": true,
|
||||
}`
|
||||
|
||||
const parsed = parseJsonRelaxed<any>(raw)
|
||||
expect(parsed.url).toBe('https://example.com/a//b')
|
||||
expect(parsed.ok).toBe(true)
|
||||
})
|
||||
|
||||
it('throws on invalid JSON after normalization', () => {
|
||||
expect(() => parseJsonRelaxed<any>('{ broken: true }')).toThrow()
|
||||
})
|
||||
})
|
||||
|
|
@ -12,6 +12,7 @@ import { join, isAbsolute, resolve } from 'path'
|
|||
import { existsSync, readFileSync } from 'fs'
|
||||
import { resolveWithin } from './paths'
|
||||
import { logger } from './logger'
|
||||
import { parseJsonRelaxed } from './json-relaxed'
|
||||
|
||||
interface OpenClawAgent {
|
||||
id: string
|
||||
|
|
@ -184,7 +185,7 @@ async function readOpenClawAgents(): Promise<OpenClawAgent[]> {
|
|||
|
||||
const { readFile } = require('fs/promises')
|
||||
const raw = await readFile(configPath, 'utf-8')
|
||||
const parsed = JSON.parse(raw)
|
||||
const parsed = parseJsonRelaxed<any>(raw)
|
||||
return parsed?.agents?.list || []
|
||||
}
|
||||
|
||||
|
|
@ -345,7 +346,7 @@ export async function writeAgentToConfig(agentConfig: any): Promise<void> {
|
|||
|
||||
const { readFile, writeFile } = require('fs/promises')
|
||||
const raw = await readFile(configPath, 'utf-8')
|
||||
const parsed = JSON.parse(raw)
|
||||
const parsed = parseJsonRelaxed<any>(raw)
|
||||
|
||||
if (!parsed.agents) parsed.agents = {}
|
||||
if (!parsed.agents.list) parsed.agents.list = []
|
||||
|
|
|
|||
|
|
@ -0,0 +1,112 @@
|
|||
/**
|
||||
* Parse JSON with tolerant fallback for JSONC-style inputs.
|
||||
* Supports comments and trailing commas, then validates with JSON.parse.
|
||||
*/
|
||||
export function parseJsonRelaxed<T>(raw: string): T {
|
||||
try {
|
||||
return JSON.parse(raw) as T
|
||||
} catch {
|
||||
const stripped = stripJsonComments(raw)
|
||||
const normalized = removeTrailingCommas(stripped)
|
||||
return JSON.parse(normalized) as T
|
||||
}
|
||||
}
|
||||
|
||||
function stripJsonComments(input: string): string {
|
||||
let output = ''
|
||||
let inString = false
|
||||
let stringDelimiter = '"'
|
||||
let inLineComment = false
|
||||
let inBlockComment = false
|
||||
|
||||
for (let i = 0; i < input.length; i++) {
|
||||
const current = input[i]
|
||||
const next = i + 1 < input.length ? input[i + 1] : ''
|
||||
const prev = i > 0 ? input[i - 1] : ''
|
||||
|
||||
if (inLineComment) {
|
||||
if (current === '\n') {
|
||||
inLineComment = false
|
||||
output += current
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (inBlockComment) {
|
||||
if (current === '*' && next === '/') {
|
||||
inBlockComment = false
|
||||
i += 1
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (inString) {
|
||||
output += current
|
||||
if (current === stringDelimiter && prev !== '\\') {
|
||||
inString = false
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if ((current === '"' || current === "'") && prev !== '\\') {
|
||||
inString = true
|
||||
stringDelimiter = current
|
||||
output += current
|
||||
continue
|
||||
}
|
||||
|
||||
if (current === '/' && next === '/') {
|
||||
inLineComment = true
|
||||
i += 1
|
||||
continue
|
||||
}
|
||||
|
||||
if (current === '/' && next === '*') {
|
||||
inBlockComment = true
|
||||
i += 1
|
||||
continue
|
||||
}
|
||||
|
||||
output += current
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
function removeTrailingCommas(input: string): string {
|
||||
let output = ''
|
||||
let inString = false
|
||||
let stringDelimiter = '"'
|
||||
|
||||
for (let i = 0; i < input.length; i++) {
|
||||
const current = input[i]
|
||||
const prev = i > 0 ? input[i - 1] : ''
|
||||
|
||||
if (inString) {
|
||||
output += current
|
||||
if (current === stringDelimiter && prev !== '\\') {
|
||||
inString = false
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if ((current === '"' || current === "'") && prev !== '\\') {
|
||||
inString = true
|
||||
stringDelimiter = current
|
||||
output += current
|
||||
continue
|
||||
}
|
||||
|
||||
if (current === ',') {
|
||||
let j = i + 1
|
||||
while (j < input.length && /\s/.test(input[j])) j += 1
|
||||
if (j < input.length && (input[j] === '}' || input[j] === ']')) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
output += current
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
Loading…
Reference in New Issue