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 { config } from '@/lib/config'
|
||||||
import { validateBody, gatewayConfigUpdateSchema } from '@/lib/validation'
|
import { validateBody, gatewayConfigUpdateSchema } from '@/lib/validation'
|
||||||
import { mutationLimiter } from '@/lib/rate-limit'
|
import { mutationLimiter } from '@/lib/rate-limit'
|
||||||
|
import { parseJsonRelaxed } from '@/lib/json-relaxed'
|
||||||
|
|
||||||
function getConfigPath(): string | null {
|
function getConfigPath(): string | null {
|
||||||
return config.openclawConfigPath || null
|
return config.openclawConfigPath || null
|
||||||
|
|
@ -24,7 +25,7 @@ export async function GET(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const { readFile } = require('fs/promises')
|
const { readFile } = require('fs/promises')
|
||||||
const raw = await readFile(configPath, 'utf-8')
|
const raw = await readFile(configPath, 'utf-8')
|
||||||
const parsed = JSON.parse(raw)
|
const parsed = parseJsonRelaxed<any>(raw)
|
||||||
|
|
||||||
// Redact sensitive fields for display
|
// Redact sensitive fields for display
|
||||||
const redacted = redactSensitive(JSON.parse(JSON.stringify(parsed)))
|
const redacted = redactSensitive(JSON.parse(JSON.stringify(parsed)))
|
||||||
|
|
@ -76,7 +77,7 @@ export async function PUT(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const { readFile, writeFile } = require('fs/promises')
|
const { readFile, writeFile } = require('fs/promises')
|
||||||
const raw = await readFile(configPath, 'utf-8')
|
const raw = await readFile(configPath, 'utf-8')
|
||||||
const parsed = JSON.parse(raw)
|
const parsed = parseJsonRelaxed<any>(raw)
|
||||||
|
|
||||||
// Apply updates via dot-notation
|
// Apply updates via dot-notation
|
||||||
const appliedKeys: string[] = []
|
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 { existsSync, readFileSync } from 'fs'
|
||||||
import { resolveWithin } from './paths'
|
import { resolveWithin } from './paths'
|
||||||
import { logger } from './logger'
|
import { logger } from './logger'
|
||||||
|
import { parseJsonRelaxed } from './json-relaxed'
|
||||||
|
|
||||||
interface OpenClawAgent {
|
interface OpenClawAgent {
|
||||||
id: string
|
id: string
|
||||||
|
|
@ -184,7 +185,7 @@ async function readOpenClawAgents(): Promise<OpenClawAgent[]> {
|
||||||
|
|
||||||
const { readFile } = require('fs/promises')
|
const { readFile } = require('fs/promises')
|
||||||
const raw = await readFile(configPath, 'utf-8')
|
const raw = await readFile(configPath, 'utf-8')
|
||||||
const parsed = JSON.parse(raw)
|
const parsed = parseJsonRelaxed<any>(raw)
|
||||||
return parsed?.agents?.list || []
|
return parsed?.agents?.list || []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -345,7 +346,7 @@ export async function writeAgentToConfig(agentConfig: any): Promise<void> {
|
||||||
|
|
||||||
const { readFile, writeFile } = require('fs/promises')
|
const { readFile, writeFile } = require('fs/promises')
|
||||||
const raw = await readFile(configPath, 'utf-8')
|
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) parsed.agents = {}
|
||||||
if (!parsed.agents.list) parsed.agents.list = []
|
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