// 炼境托管的 DSH 插件:会话创建时按该会话的工作目录匹配炼境登记的项目根目录,命中则只在这个会话的 // 作用域挂一个连项目专用地址的 mcp-client(同名工具覆盖全局 lian-jing),未命中不做任何事。 // 由炼境写入 %USERPROFILE%\.lian-jing\dsh\ 并在 ~/.dsh/cordis.patch.yml 托管块中引用,勿手改,会被覆盖。 // 内容变化时文件名与托管块条目 id 的版本号必须一起递增:DSH 只在条目 id 变化时才卸载旧条目、import 新模块 // (同 id 只改 name 不会重载,Node 也按 URL 缓存 ESM);mcp_inject.rs 的内容哈希测试会拦住漏升版本。 // // 约束:DSH 桌面 Host 对未处理异常会整体崩溃,agent/created 又是串行事件(监听器失败会让会话创建失败), // 所以这里所有回调都吞掉异常、挂载有时间上限,炼境离线或 DSH 接口变化时静默降级为只有全局工具。 import { execFile } from "node:child_process"; import { createHash } from "node:crypto"; import { existsSync } from "node:fs"; import path from "node:path"; export const name = "lianjing-session-mcp"; export const inject = ["agents", "loader", "tools"]; const MCP_KEY = "lian-jing"; /** 整个挂载(取认证 + 等工具就绪)的上限:agent/created 是串行事件,不能拖慢会话创建太久。 */ const ATTACH_TIMEOUT_MS = 5000; const AUTH_TIMEOUT_MS = 3000; const warn = (message) => { try { process.stderr.write(`[lian-jing] ${message}\n`); } catch {} }; /** 从工作目录逐级向上匹配白名单(键规则与炼境 normalize_dsh_root 一致),命中 null 停止哨兵即停。 */ export function findProjectId(roots, cwd) { if (!roots || typeof cwd !== "string" || cwd === "") return undefined; for (let dir = path.resolve(cwd), prev; dir !== prev; prev = dir, dir = path.dirname(dir)) { const key = dir.toLowerCase().replace(/\\+$/, ""); if (Object.prototype.hasOwnProperty.call(roots, key)) return roots[key] || undefined; } return undefined; } /** headless 由托管块里的全局行按启动目录路由,插件不重复挂载;DSH 0.1.x 没有 profileContext,退回看启动参数。 */ export function isHeadless(ctx) { let profile; try { profile = ctx.get("profileContext")?.name; } catch {} if (profile === undefined && process.argv.includes("headless")) profile = "headless"; return profile === "headless"; } /** * 子代理是否带工具限制(toolFilter)。插件挂在子代理自己作用域里的工具不受限制约束,所以受限子代理不挂载, * 只保留经限制过滤后的全局 lian-jing 工具;无法判断时同样不挂(宁可少给)。根会话与未受限子代理照常挂载。 */ export function isRestrictedSubagent(ctx, agent) { if (agent?.session?.header?.origin !== "subagent") return false; try { const view = ctx.tools?.view?.(agent); if (!view?.restrictableNames || typeof view.visible?.has !== "function") return true; for (const toolName of view.restrictableNames) { if (!view.visible.has(toolName)) return true; } return false; } catch { return true; } } /** 经炼境稳定入口取认证头;入口缺失、失败或超时都返回 undefined(只影响需认证的工具),不输出认证值。 */ export function readAuthorization(shim, projectId, timeoutMs) { return new Promise((resolve) => { const systemRoot = process.env.SystemRoot; if (typeof shim !== "string" || !existsSync(shim) || !systemRoot) { resolve(undefined); return; } const digest = createHash("sha256").update(projectId).digest("hex"); try { execFile( path.join(systemRoot, "System32", "cmd.exe"), // Node 拒绝直接执行 .cmd(EINVAL),须经 cmd /d /s /c 原样传参。 ["/d", "/s", "/c", `""${shim}" ${digest}"`], { encoding: "utf8", timeout: timeoutMs, windowsHide: true, windowsVerbatimArguments: true }, (error, stdout) => { if (error) { resolve(undefined); return; } try { const authorization = JSON.parse(stdout).Authorization; resolve(typeof authorization === "string" ? authorization : undefined); } catch { resolve(undefined); } }, ); } catch { resolve(undefined); } }); } const withTimeout = (promise, timeoutMs) => Promise.race([promise, new Promise((resolve) => setTimeout(resolve, timeoutMs).unref?.())]); export async function apply(ctx, config) { try { if (isHeadless(ctx)) return; if (typeof ctx.agents?.list !== "function" || typeof ctx.on !== "function" || typeof ctx.loader?.import !== "function") { warn("当前 DSH 不支持会话级插件接口,lian-jing 只提供全局工具"); return; } let McpClient; try { McpClient = await ctx.loader.import("@deepseek-ai/dsh-mcp-client"); } catch (error) { warn(`无法加载 dsh-mcp-client,lian-jing 只提供全局工具: ${error?.message ?? error}`); return; } if (typeof McpClient?.Config !== "function" || typeof McpClient?.apply !== "function") { warn("dsh-mcp-client 接口不符,lian-jing 只提供全局工具"); return; } const mounted = new Map(); const attach = async (agent) => { const id = findProjectId(config?.roots, agent?.session?.header?.cwd); if (!id || mounted.has(agent) || typeof agent?.ctx?.plugin !== "function") return; if (isRestrictedSubagent(ctx, agent)) return; mounted.set(agent, undefined); const authorization = await readAuthorization(config?.shim, id, AUTH_TIMEOUT_MS); if (!authorization) warn(`未取到项目 ${id} 的认证头,需认证的开发变量工具将不可用`); if (!mounted.has(agent)) return; const options = McpClient.Config({ transport: "streamable-http", serverName: MCP_KEY, url: `http://127.0.0.1:${config.port}/mcp/projects/${encodeURIComponent(id)}`, headers: authorization ? { Authorization: authorization } : {}, }); const fiber = agent.ctx.plugin(McpClient, options); mounted.set(agent, fiber); // 尽量在首轮请求前等工具就绪;fiber 不可等待时立即返回 await Promise.resolve(fiber).catch(() => {}); }; const safeAttach = (agent) => withTimeout(attach(agent), ATTACH_TIMEOUT_MS).catch((error) => warn(`会话级挂载失败: ${error?.message ?? error}`), ); for (const agent of ctx.agents.list()) void safeAttach(agent); ctx.on("agent/created", ({ agent }) => safeAttach(agent)); ctx.on("agent/disposed", ({ agent }) => { try { mounted.get(agent)?.dispose?.(); } catch {} mounted.delete(agent); }); ctx.effect?.(() => () => { for (const fiber of mounted.values()) { try { fiber?.dispose?.(); } catch {} } mounted.clear(); }, "lianjing-session-mcp.mounts"); } catch (error) { warn(`会话级插件初始化失败,lian-jing 只提供全局工具: ${error?.message ?? error}`); } }