Преглед изворни кода

fix: Codex 远程插件按 remotePluginId 安装;Codex 报错原样返回

- 远程目录中的名称(如 clickup)在安装接口上返回 404 Plugin not found,改为先读详情取 remotePluginId
- Codex RPC 错误转为 502 codex_error 并带原始信息,不再是笼统的服务器内部错误

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Jerry Liang пре 6 дана
родитељ
комит
7c10d993ed

+ 13 - 3
apps/server/src/services/codex-plugins.ts

@@ -6,6 +6,7 @@ import { AppError, badRequest, notFound } from '../lib/errors'
 import type {
   PluginInstallResponse, PluginInstalledResponse, PluginListResponse, PluginMarketplaceEntry, PluginReadResponse, PluginSummary,
 } from '../providers/codex/protocol/v2'
+import { RpcRequestError } from '../providers/codex/rpc'
 
 const CACHE_MS = 10 * 60_000
 const PAGE_MAX = 100
@@ -32,8 +33,14 @@ export class CodexPluginService {
     return c
   }
 
-  private call<T>(method: string, params: unknown, timeoutMs = 60_000): Promise<T> {
-    return this.rpc().request<T>(this.settings(), method, params, timeoutMs)
+  /** Codex 返回的错误(如远程目录请求失败)原样转给后台,而不是笼统的"服务器内部错误" */
+  private async call<T>(method: string, params: unknown, timeoutMs = 60_000): Promise<T> {
+    try {
+      return await this.rpc().request<T>(this.settings(), method, params, timeoutMs)
+    } catch (err) {
+      if (err instanceof RpcRequestError) throw new AppError(502, 'codex_error', `Codex:${err.rpc.message}`)
+      throw err
+    }
   }
 
   async catalog(q: { query?: string; category?: string; installed?: boolean; offset?: number; limit?: number; refresh?: boolean }): Promise<CodexPluginCatalogDto> {
@@ -80,7 +87,10 @@ export class CodexPluginService {
   async install(name: string): Promise<CodexPluginInstallResultDto> {
     const p = await this.find(name)
     if (p.availability !== 'AVAILABLE' || p.installPolicy === 'NOT_AVAILABLE') throw badRequest(`插件 ${p.displayName} 当前不可安装`)
-    const r = await this.call<PluginInstallResponse>('plugin/install', { remoteMarketplaceName: p.marketplace, pluginName: p.name }, 300_000)
+    // 远程插件按 remotePluginId 安装:目录里的名称(如 clickup)在远程安装接口上会返回 404 Plugin not found
+    const read = await this.call<PluginReadResponse>('plugin/read', { remoteMarketplaceName: p.marketplace, pluginName: p.name })
+    const pluginName = read.plugin.summary.remotePluginId ?? p.name
+    const r = await this.call<PluginInstallResponse>('plugin/install', { remoteMarketplaceName: p.marketplace, pluginName }, 300_000)
     this.cache = null
     this.audit(`安装 Codex 插件 ${p.displayName}(${p.id})`)
     return { appsNeedingAuth: r.appsNeedingAuth.map(a => ({ id: a.id, name: a.name, installUrl: a.installUrl })) }

+ 3 - 0
apps/server/test/codex.test.ts

@@ -104,6 +104,9 @@ describe('CodexProvider(模拟 app-server)', () => {
     expect((await svc.catalog({ installed: true })).total).toBe(1)
     expect(await svc.install('notion')).toEqual({ appsNeedingAuth: [{ id: 'connector_n', name: 'Notion', installUrl: 'https://chatgpt.com/apps/notion' }] })
     await expect(svc.install('blocked')).rejects.toThrow(/不可安装/)
+    // Codex 的错误原样转出(502),不是"服务器内部错误"
+    await expect((svc as unknown as { call: (m: string, p: unknown) => Promise<unknown> }).call('plugin/install', { remoteMarketplaceName: 'openai-curated-remote', pluginName: 'notion' }))
+      .rejects.toMatchObject({ status: 502, code: 'codex_error', message: expect.stringContaining('Plugin not found') })
     await expect(svc.install('other-plugin')).rejects.toThrow(/不存在/)
     await expect(svc.install('../x')).rejects.toThrow(/格式/)
     await svc.uninstall('gmail')

+ 5 - 1
apps/server/test/fixtures-fake-codex.mjs

@@ -48,7 +48,11 @@ readline.createInterface({ input: process.stdin }).on('line', async line => {
       const plugins = method === 'plugin/installed' ? [p('gmail', globalThis.gmail ?? true)] : [p('gmail', globalThis.gmail ?? true), p('notion', false, { interface: { displayName: 'Notion', shortDescription: 'docs', category: 'Docs' } }), p('blocked', false, { availability: 'DISABLED_BY_ADMIN' })]
       return reply({ marketplaces: [{ name: 'openai-curated-remote', path: null, interface: null, plugins }, { name: 'other', path: '/x', interface: null, plugins: [p('other-plugin', false)] }], marketplaceLoadErrors: [], featuredPluginIds: [] })
     }
-    case 'plugin/install': globalThis.installed = params; return reply({ authPolicy: 'ON_USE', appsNeedingAuth: [{ id: 'connector_n', name: 'Notion', description: null, installUrl: 'https://chatgpt.com/apps/notion', category: null }] })
+    // 与真实远程目录一致:plugin/read 给出 remotePluginId,安装接口只认这个 ID
+    case 'plugin/read': return reply({ plugin: { marketplaceName: params.remoteMarketplaceName, summary: { id: `${params.pluginName}@openai-curated-remote`, name: params.pluginName, remotePluginId: `plugin_asdk_${params.pluginName}` }, skills: [], apps: [], mcpServers: [] } })
+    case 'plugin/install':
+      if (!params.pluginName.startsWith('plugin_asdk_')) return send({ id, error: { code: -32600, message: 'install remote plugin: 404 Not Found: {"detail":"Plugin not found"}' } })
+      globalThis.installed = params; return reply({ authPolicy: 'ON_USE', appsNeedingAuth: [{ id: 'connector_n', name: 'Notion', description: null, installUrl: 'https://chatgpt.com/apps/notion', category: null }] })
     case 'plugin/uninstall': globalThis.gmail = false; return reply({})
     case 'turn/start': {
       const threadId = params.threadId, turnId = 'turn-1'