build / build-and-scan (push) Canceled after 0s
文档库:目录改为编号制(01-规范/02-架构设计/03-数据库/04-调整方案/
05-交接单/06-ops/07-scripts/08-skills/09-archive),顶层散文件归入 01-规范/;
INDEX.md 与 docs-manifest.json 重刷(档案 146 篇);旧目录名引用全量对齐。
IM 线:src/im/**(SDK / hub / store / presence / ws / gateway-token)、
src/web/routes/im.ts、src/db/plugin-data/**、src/supervisor/plugin-assembly.ts
及对应 test/**。
插件线:poc/{im-agent-bridge,im-connection-gateway,im-conversation-tabs,
business-plugins-im,carbon-mcp-probe}、src/web/routes/{sessions,overlay-device}.ts、
src/net/relay/{device-grant,instance-credential}.ts。
仓库卫生:清出 40 个历史误入库 / 已改名文件(34 个交接单归档 + 6 个旧结构,
本地均有副本);dsh-server-docs/.gitignore 补 tmp/;交接单不入库(政策)。
133 lines
5.1 KiB
JavaScript
133 lines
5.1 KiB
JavaScript
// @dsh-local/carbon-mcp-probe — host 半区(POC 探针)
|
||
//
|
||
// 本包在 host 侧只做一件事:**取证**。
|
||
// 真正的 MCP 挂载不在这里写代码,而是由同目录的 cordis.patch.yml
|
||
// insert 一条内核 `@deepseek-ai/dsh-mcp-client` entry 完成 —— 这正是本轮要验证的契约。
|
||
//
|
||
// 探针把「boot 后 ctx.tools 注册表里到底有没有 MCP 工具」落成 JSON 证据文件,
|
||
// 使验收不依赖 agent / 模型 / 界面。
|
||
|
||
import { appendFileSync, writeFileSync } from "node:fs";
|
||
|
||
const DUMP = process.env.CARBON_PROBE_DUMP || "/tmp/carbon-probe-dump.json";
|
||
const LOG = process.env.CARBON_PROBE_LOG || "/tmp/carbon-probe-host.log";
|
||
const DELAY_MS = Number(process.env.CARBON_PROBE_DELAY_MS || 8000);
|
||
|
||
function log(line) {
|
||
const row = `${new Date().toISOString()} ${line}\n`;
|
||
try { appendFileSync(LOG, row); } catch { /* 日志不可写不致命 */ }
|
||
process.stdout.write(`[carbon-mcp-probe] ${row}`);
|
||
}
|
||
|
||
/** 在有界预算内 BFS 一个对象图,收集所有含 needle 的键/字符串路径。 */
|
||
function bfsFind(root, needle, maxDepth = 5, maxNodes = 600) {
|
||
const seen = new Set();
|
||
const hits = [];
|
||
const queue = [[root, "$", 0]];
|
||
let visited = 0;
|
||
while (queue.length && visited < maxNodes) {
|
||
const [value, path, depth] = queue.shift();
|
||
if (value === null || value === undefined) continue;
|
||
const t = typeof value;
|
||
if (t !== "object" && t !== "function") {
|
||
if (t === "string" && value.includes(needle)) hits.push({ path, kind: "string", value: value.slice(0, 160) });
|
||
continue;
|
||
}
|
||
if (seen.has(value)) continue;
|
||
seen.add(value);
|
||
visited += 1;
|
||
|
||
let keys = [];
|
||
try { keys = Object.getOwnPropertyNames(value); } catch { continue; }
|
||
for (const k of keys) {
|
||
if (k.includes(needle)) hits.push({ path: `${path}.${k}`, kind: "KEY" });
|
||
if (depth + 1 > maxDepth) continue;
|
||
let child;
|
||
try { child = value[k]; } catch { continue; } // getter 抛错就跳过,绝不冒泡
|
||
const childPath = `${path}.${k}`;
|
||
if (child instanceof Map) {
|
||
let i = 0;
|
||
for (const [mk, mv] of child) {
|
||
const mp = `${childPath}{Map}["${String(mk)}"]`;
|
||
if (String(mk).includes(needle)) hits.push({ path: mp, kind: "MAPKEY" });
|
||
if (i++ > 250) break;
|
||
queue.push([mv, mp, depth + 1]);
|
||
}
|
||
} else if (Array.isArray(child)) {
|
||
child.slice(0, 60).forEach((v, i) => queue.push([v, `${childPath}[${i}]`, depth + 1]));
|
||
} else if (typeof child === "string") {
|
||
if (child.includes(needle)) hits.push({ path: childPath, kind: "string", value: child.slice(0, 160) });
|
||
} else if (child !== null && typeof child === "object") {
|
||
queue.push([child, childPath, depth + 1]);
|
||
}
|
||
}
|
||
}
|
||
return { hits, visited, truncated: queue.length > 0 };
|
||
}
|
||
|
||
function probeTools(ctx) {
|
||
const rec = { ts: new Date().toISOString(), pid: process.pid };
|
||
|
||
// 通道 A:直接属性(未声明 inject 时应报错 —— 这本身是契约事实)
|
||
try {
|
||
const direct = ctx.tools;
|
||
rec.channelA = { ok: true, type: typeof direct };
|
||
} catch (e) {
|
||
rec.channelA = { ok: false, error: String(e) };
|
||
}
|
||
|
||
// 通道 B:ctx.get(name)
|
||
let tools = null;
|
||
try {
|
||
tools = typeof ctx.get === "function" ? ctx.get("tools") : null;
|
||
rec.channelB = { ok: !!tools, name: tools && tools.name };
|
||
} catch (e) {
|
||
rec.channelB = { ok: false, error: String(e) };
|
||
}
|
||
if (!tools) return rec;
|
||
|
||
// 通道 C:已知候选方法(不预设签名,逐个试)
|
||
rec.channelC = {};
|
||
for (const m of ["list", "view", "schemas", "sdkSchemas", "get", "names"]) {
|
||
try {
|
||
if (typeof tools[m] !== "function") { rec.channelC[m] = { skipped: "not a function" }; continue; }
|
||
const v = typeof tools[m] === "function" ? tools[m]() : undefined;
|
||
if (v && typeof v.then === "function") { rec.channelC[m] = { async: true }; continue; }
|
||
rec.channelC[m] = { type: typeof v, preview: JSON.stringify(v).slice(0, 400) };
|
||
} catch (e) {
|
||
rec.channelC[m] = { threw: String(e).slice(0, 200) };
|
||
}
|
||
}
|
||
|
||
// 通道 D:有界 BFS —— 不依赖任何 API 形状,直接找 mcp__ 命名
|
||
try {
|
||
rec.channelD = bfsFind(tools, "mcp__");
|
||
rec.hasMcpQualified = rec.channelD.hits.length > 0;
|
||
} catch (e) {
|
||
rec.channelD = { error: String(e) };
|
||
rec.hasMcpQualified = false;
|
||
}
|
||
|
||
// 通道 E:同上,只要 carbonprobe 片段(防命名前缀差异)
|
||
try {
|
||
rec.channelE = bfsFind(tools, "carbonprobe", 5, 600);
|
||
} catch (e) {
|
||
rec.channelE = { error: String(e) };
|
||
}
|
||
|
||
return rec;
|
||
}
|
||
|
||
export function apply(ctx) {
|
||
log(`apply pid=${process.pid} node=${process.version} dump=${DUMP}`);
|
||
const timer = setTimeout(() => {
|
||
let rec;
|
||
try { rec = probeTools(ctx); }
|
||
catch (e) { rec = { ts: new Date().toISOString(), fatal: String(e) }; }
|
||
try { writeFileSync(DUMP, JSON.stringify(rec, null, 2)); log(`dump written -> ${DUMP}`); }
|
||
catch (e) { log(`dump write failed: ${e}`); }
|
||
log(`SUMMARY hasMcpQualified=${rec.hasMcpQualified} D.hits=${JSON.stringify(rec.channelD?.hits || []).slice(0, 800)}`);
|
||
}, DELAY_MS);
|
||
timer.unref?.();
|
||
}
|