From 6f051813e3ca46d76bb8e5ae34817ff223e3cd19 Mon Sep 17 00:00:00 2001 From: axzhang1216 Date: Fri, 10 Jul 2026 19:03:28 +0800 Subject: [PATCH] fix(web): parse markdown responses as text in WebPlatformAdapter.fetchRemoteConfig MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Importing a skill from the cloud market failed with 'SyntaxError: No number after minus sign in JSON at position 1 (line 1 column 2)' because the WebPlatformAdapter hard-coded res.json() for every response, while the same IPC handler on Electron already routes by Content-Type / .json suffix (see apps/desktop/main/ipc/window.ts: '支持 JSON 和纯文本/Markdown'). The Electron path handled skill bodies (.md) correctly; the Web path only worked for the catalog (skill.json). Aligning the Web adapter with the same Content-Type / .json-suffix rule fixes both fetchCloudCatalog and importFromCloud at once, with no new abstraction. --- src/services/platform/web.test.ts | 31 +++++++++++++++++++++++++++++++ src/services/platform/web.ts | 4 +++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/services/platform/web.test.ts b/src/services/platform/web.test.ts index b7bb6d6a..04839d74 100644 --- a/src/services/platform/web.test.ts +++ b/src/services/platform/web.test.ts @@ -71,4 +71,35 @@ describe('WebPlatformAdapter', () => { globalThis.fetch = originalFetch } }) + + it('parses markdown skill bodies as text instead of JSON', async () => { + // Regression: importing a skill from the cloud market failed with + // "SyntaxError: No number after minus sign in JSON..." because + // fetchRemoteConfig hard-coded res.json() for every response. + const originalFetch = globalThis.fetch + const md = `---\nid: skill_md\nname: Markdown Skill\ndescription: desc\n---\nbody text` + globalThis.fetch = (() => + Promise.resolve(new Response(md, { headers: { 'content-type': 'text/markdown' } }))) as typeof fetch + + try { + const result = await new WebPlatformAdapter().fetchRemoteConfig('https://example.com/skills/skill_md.md') + assert.equal(result.success, true) + assert.equal(result.data, md) + } finally { + globalThis.fetch = originalFetch + } + }) + + it('parses .json URLs as JSON even without an application/json content-type', async () => { + const originalFetch = globalThis.fetch + globalThis.fetch = (() => + Promise.resolve(new Response('[1,2,3]', { headers: { 'content-type': 'text/plain' } }))) as typeof fetch + + try { + const result = await new WebPlatformAdapter().fetchRemoteConfig('https://example.com/cn/skill.json') + assert.deepEqual(result.data, [1, 2, 3]) + } finally { + globalThis.fetch = originalFetch + } + }) }) diff --git a/src/services/platform/web.ts b/src/services/platform/web.ts index 749adfd0..46757b44 100644 --- a/src/services/platform/web.ts +++ b/src/services/platform/web.ts @@ -25,7 +25,9 @@ export class WebPlatformAdapter implements PlatformAdapter { try { const res = await fetch(url) if (!res.ok) return { success: false, error: `HTTP ${res.status}` } - const data = await res.json() + const contentType = res.headers.get('content-type') || '' + const isJson = contentType.includes('application/json') || url.split('?')[0].endsWith('.json') + const data = isJson ? await res.json() : await res.text() return { success: true, data } } catch (e) { return { success: false, error: String(e) }