Skip to content

[Bug] dsh web「知识节点」面板报告:POST /gf/nodes 返回 HTTP 405 #26

Description

@heptaspirit

版本(Version)

1.12.2

环境(Environment)

dsh: @deepseek-ai/dsh ^0.1.1-rc.2 (dsh web) OS: Windows

复现步骤(Steps to reproduce)

复现步骤

  1. 以 dsh web 启动 DeepSeek Harness。

  2. 打开任意会话,点击头部工具栏的「知识节点」按钮。

  3. 右侧面板打开,底部出现「加载中…」后立即报:

    加载失败: transport failure for /gf/nodes: HTTP 405
    

手动复现(对正在运行的 server 直接发请求):

POST http://127.0.0.1:3080/gf/nodes
Content-Type: application/json

{"type":"client-request","rpcId":"probe-1","method":"nodes","payload":{"workspaceRoot":"D:\\WorkSpace\\serendipity-engine"}}
# → HTTP 405

期望行为(Expected behavior)

打开「知识节点」面板,应从 graphflow 图谱存储返回 workbench / dialogue 的 JSON(正常显示主题与对话记录),而不是 HTTP 405。

实际行为(Actual behavior)

在 DeepSeek Harness(dsh web)界面点开右侧「知识节点」面板,取数请求报错:

加载失败: transport failure for /gf/nodes: HTTP 405

根因指向 graphflow 自身的 dsh glue 插件(@roarpeng/graphflow/dsh)。它负责注册 /gf RPC 通道供面板取数,但该注册逻辑全程静默吞错,导致 /gf 通道从未被宿主应答,请求落到 SPA 静态兜底路由,返回 405 Method Not Allowed。

这不是使用者的配置问题。 面板能渲染、MCP 工具能工作、glue 插件已加载且 active,唯独 /gf 通道没有生效——这是插件代码层面的缺陷。

补充信息(可选)

诊断证据(实测)

1. MCP 工具正常,面板取数异常

同一个插件的两条通道,一条能用一条不能用:

通道 实现路径 结果
MCP 工具(mcp__graphflow__graph_*,含 graphflow_diagnose) 独立 stdio 进程 graphflow-mcp,经 @deepseek-ai/dsh-mcp-client 挂载到工具目录 ✅ 正常
知识节点面板取数(/gf/nodes) dsh/plugin.mjs 的 registerNodesRpcChannel 注册 /gf RPC 通道 ❌ 405

2. glue 插件已加载且 active

通过 DSH 的 pluginInventory/list API 直接读取当前生效插件树:

entryId=include:graphflow-dsh   moduleName=@roarpeng/graphflow/dsh   enabled=true   fiberPhase=active
entryId=include:mcp-graphflow   moduleName=@deepseek-ai/dsh-mcp-client  enabled=true   fiberPhase=active

graphflow-dsh 已经加载并运行。 所以问题不是「没挂载」,而是「挂载了但 /gf 通道没注册成功 / 没有被应答」。

3. 路由对照探测

POST /gf/nodes              → HTTP 405   (复现用户报错)
POST /api/session.list      → HTTP 200   (内置 RPC 正常,宿主 /api 前缀工作正常)

/gf/nodes 这条 POST 没被任何已注册路由接住,落到 Web 前端的静态文件兜底(SPA dist),对「不存在的虚拟路径」返回 405。


根因分析

关键源码:dsh/plugin.mjs → registerNodesRpcChannel(ctx, config)

function registerNodesRpcChannel(ctx, config) {
  const connection = typeof ctx.get === "function" ? ctx.get("connection") : ctx.connection;
  if (!connection || typeof connection !== "object") return undefined;   // (1) 静默
  const rpc = connection.rpc;
  if (!rpc || typeof rpc.handle !== "function") return undefined;        // (2) 静默
  if (wiredRpcConnections.has(connection)) return undefined;             // (3) 静默
  wiredRpcConnections.add(connection);
  let disposer;
  try {
    disposer = rpc.handle("/gf", (endpoint, payload, signal) => rpcNodesHandler(...), {
      authority: "trusted-host",
    });
  } catch {
    wiredRpcConnections.delete(connection);
    return undefined;                                                    // (4) 静默
  }
  ...
  return dispose;
}

三处((1)(2)(4))失败全部静默 return undefined,不抛错、不写日志。 一旦 /gf 通道注册失败,面板取数就毫无提示地 405。

宿主侧路由:@deepseek-ai/dsh-host-webserver register()

register(route) {
  const table = route.kind === "exact" ? this.exact : this.prefixes;
  if (table.has(route.path)) throw new Error(`webserver: duplicate ${route.kind} route "${route.path}"`);
  table.set(route.path, route);
  return () => { table.delete(route.path); };
}

路由匹配是最长前缀优先(match())。若 /gf 前缀不在 prefix 表里(注册抛错或未发生),/gf/nodes 就命中 fallback(SPA 静态服务器)→ 405。

可疑根因:同步 ctx.get("connection")

glue 在 apply() 里同步调用 ctx.get("connection") 拿 connection 服务。而当前 dsh 的宿主 connection 服务是带依赖注入声明的:

id: connection
  name: '@deepseek-ai/dsh-client-connection'
  inject: [webRuntime]          # 依赖 webRuntime

若 graphflow-dsh apply 时 connection 尚未就绪,ctx.get("connection") 返回 undefined → 直接 return undefined(失败点 (1)),/gf 通道就此静默缺失。

对比 DSH 内置的 api-gateway(@deepseek-ai/dsh-api-gateway)——它等待 connection 的方式是异步注入:

ctx.inject(["connection"], (connectionCtx) => {
  connectionCtx.connection.rpc.intercept("/api", ...);
});

而不是 ctx.get("connection")。graphflow 的写法假设 connection 在 apply 时同步可用,这在当前 dsh 版本下很可能不成立。

补充:为什么 patch 加 inject 救不了

初步猜测「在 patch 里给 graphflow-dsh 行加 inject: [connection]」即可修复。但核实 Cordis 源码(@deepseek-ai/cordis/lib/index.js)后确认:

plugin(plugin, config, ...) {
  const fiber = new Fiber(this.ctx, config, Inject.resolve(plugin.inject), ...);
}

Cordis 只读取插件模块的静态 inject(plugin.inject)做依赖等待,不读取 patch 行上的 inject 字段。 因此该问题无法通过配置层(patch)修复,只能在插件代码层(dsh/plugin.mjs)修改。


建议修复方案(供作者参考)

  1. 异步等待 connection 服务:把

    const connection = ctx.get("connection");

    改为

    ctx.inject(["connection"], (connectionCtx) => { ... registerNodesRpcChannel ... });

    或等效地让插件声明静态 inject 依赖(若 Cordis 支持从插件对象声明)。

  2. 不要静默吞错:registerNodesRpcChannel 的四处失败至少应写入日志/回调,让 /gf 通道注册失败可见,而不是无声的 405。

  3. 冲突检查:注册前确认没有其它路由已抢占 /gf(webserver.register 抛的 duplicate ... route 会被 catch 吞掉),必要时优先处理。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions