fix(sync): support commented/trailing-comma OpenClaw config (#235)

This commit is contained in:
nyk 2026-03-06 01:52:40 +07:00 committed by GitHub
parent 2b28b8ebe2
commit d92e01d64f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 167 additions and 4 deletions

View File

@ -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[] = []

View File

@ -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()
})
})

View File

@ -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 = []

112
src/lib/json-relaxed.ts Normal file
View File

@ -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
}