文档库:目录改为编号制(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/;交接单不入库(政策)。
This commit is contained in:
1 parent
3d8f50e366
commit
e6207aa691
239 files changed
+34477
-14633
No files matched your search
@@ -0,0 +1,23 @@
|
||||
# @dsh-local/carbon-mcp-probe — bundle patch(host 半区)
|
||||
#
|
||||
# 本 POC 的**全部挂载动作**都在这两行里:
|
||||
# ① 载入本包自己的 host 半区(lib/index.js,探针)
|
||||
# ② insert 一条**内核** mcp-client entry,用 streamable-http 连远程 MCP server
|
||||
#
|
||||
# name 解析:profile 文档规定 bundle 名**先从 dsh 安装目录解析**,再从 profile 目录
|
||||
# ⇒ 业务包无需自带 @deepseek-ai/dsh-mcp-client 依赖即可引用内核插件。
|
||||
#
|
||||
# ⛔ 生产形态里 url 不该硬编码在这里(应来自实例 env / 平台注入);
|
||||
# 本文件只服务 POC,值是本机 loopback 上的 stub。
|
||||
|
||||
- insert:
|
||||
- id: carbon-mcp-probe
|
||||
name: '@dsh-local/carbon-mcp-probe'
|
||||
- id: carbon-mcp-probe-server
|
||||
name: '@deepseek-ai/dsh-mcp-client'
|
||||
config:
|
||||
transport: streamable-http
|
||||
serverName: carbonprobe
|
||||
url: 'http://127.0.0.1:39877/mcp'
|
||||
toolCallTimeoutMs: 15000
|
||||
failOnStartupError: true
|
||||
@@ -0,0 +1,7 @@
|
||||
// @dsh-local/carbon-mcp-probe — client 半区(POC)
|
||||
// 本轮问题**只涉及 host 半区**(MCP 工具能否暴露给 agent),
|
||||
// 故 client 侧刻意留空 apply:不做任何界面、不注入任何槽位。
|
||||
|
||||
export function apply() {
|
||||
// 空 apply —— 见文件头说明
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
// @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?.();
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "@dsh-local/carbon-mcp-probe",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"description": "POC(非正式包):验证 dsh 0.1.5-rc.1 业务插件的 host 半区能否把远程 HTTP MCP server 的工具暴露给 agent。仅用于回答契约问题,不承载任何业务功能。",
|
||||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
"exports": {
|
||||
".": "./lib/index.js",
|
||||
"./client": "./lib/client.js"
|
||||
},
|
||||
"dsh": {
|
||||
"bundle": {
|
||||
"patch": "./cordis.patch.yml"
|
||||
},
|
||||
"client": {
|
||||
"platform": "web",
|
||||
"inject": []
|
||||
}
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh": "^0.1.5-rc.1"
|
||||
},
|
||||
"dependencies": {},
|
||||
"license": "MIT"
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* carbon MCP probe — stub MCP server(零依赖单文件)
|
||||
*
|
||||
* 用途:只回答「dsh 的 mcp-client 能否连上并同步工具」这一个问题。
|
||||
* 不实现任何业务语义,不做任何外部访问。
|
||||
*
|
||||
* 用法:
|
||||
* node stub-mcp-server.mjs # stdio(默认)
|
||||
* node stub-mcp-server.mjs --http 39877 # Streamable HTTP
|
||||
*
|
||||
* 环境变量:
|
||||
* PROBE_LOG 请求日志落点(默认 ./stub-mcp.log)
|
||||
*/
|
||||
|
||||
import http from "node:http";
|
||||
import { appendFileSync, writeFileSync } from "node:fs";
|
||||
|
||||
const LOG = process.env.PROBE_LOG || "./stub-mcp.log";
|
||||
const TOOL_NAME = "ping";
|
||||
const PONG_PREFIX = "pong-carbon-probe";
|
||||
|
||||
try { writeFileSync(LOG, ""); } catch { /* 日志不可写不致命 */ }
|
||||
|
||||
function log(line) {
|
||||
const row = `${new Date().toISOString()} ${line}\n`;
|
||||
try { appendFileSync(LOG, row); } catch { /* ignore */ }
|
||||
process.stderr.write(row);
|
||||
}
|
||||
|
||||
const PING_TOOL = {
|
||||
name: TOOL_NAME,
|
||||
description: "carbon MCP probe stub — 返回固定串与服务端时间",
|
||||
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
||||
};
|
||||
|
||||
function callTool(params) {
|
||||
if (!params || params.name !== TOOL_NAME) {
|
||||
return { content: [{ type: "text", text: `unknown tool: ${params && params.name}` }], isError: true };
|
||||
}
|
||||
const ts = new Date().toISOString();
|
||||
return { content: [{ type: "text", text: `${PONG_PREFIX} ${ts}` }] };
|
||||
}
|
||||
|
||||
/** 处理一条 JSON-RPC message,返回 response 或 null(notification)。 */
|
||||
function handle(msg) {
|
||||
const method = msg && msg.method;
|
||||
const id = msg && msg.id;
|
||||
log(`rpc method=${method} id=${id === undefined ? "-" : id}`);
|
||||
if (method === "initialize") {
|
||||
return {
|
||||
jsonrpc: "2.0",
|
||||
id,
|
||||
result: {
|
||||
protocolVersion: msg.params && msg.params.protocolVersion ? msg.params.protocolVersion : "2025-06-18",
|
||||
capabilities: { tools: { listChanged: false } },
|
||||
serverInfo: { name: "carbon-probe-stub", version: "0.0.1" },
|
||||
},
|
||||
};
|
||||
}
|
||||
if (method === "notifications/initialized" || String(method || "").startsWith("notifications/")) return null;
|
||||
if (method === "ping") return { jsonrpc: "2.0", id, result: {} };
|
||||
if (method === "tools/list") return { jsonrpc: "2.0", id, result: { tools: [PING_TOOL] } };
|
||||
if (method === "tools/call") return { jsonrpc: "2.0", id, result: callTool(msg.params) };
|
||||
if (id === undefined) return null;
|
||||
return { jsonrpc: "2.0", id, error: { code: -32601, message: `Method not found: ${method}` } };
|
||||
}
|
||||
|
||||
function dispatch(payload) {
|
||||
const msgs = Array.isArray(payload) ? payload : [payload];
|
||||
const out = [];
|
||||
for (const m of msgs) {
|
||||
const r = handle(m);
|
||||
if (r !== null) out.push(r);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/* ---------------- Streamable HTTP ---------------- */
|
||||
|
||||
function startHttp(port) {
|
||||
const server = http.createServer((req, res) => {
|
||||
if (req.method === "GET" && req.url.startsWith("/health")) {
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({ ok: true, server: "carbon-probe-stub" }));
|
||||
return;
|
||||
}
|
||||
if (req.method !== "POST") {
|
||||
res.writeHead(405).end();
|
||||
return;
|
||||
}
|
||||
let body = "";
|
||||
req.on("data", (c) => { body += c.toString("utf8"); });
|
||||
req.on("end", () => {
|
||||
let payload;
|
||||
try {
|
||||
payload = JSON.parse(body);
|
||||
} catch (e) {
|
||||
log(`http parse-error ${e.message} raw=${body.slice(0, 200)}`);
|
||||
res.writeHead(400, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "parse error" } }));
|
||||
return;
|
||||
}
|
||||
const out = dispatch(payload);
|
||||
if (out.length === 0) {
|
||||
log("http -> 202 (notification only)");
|
||||
res.writeHead(202, { "Mcp-Session-Id": "probe-session-1" });
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
const payloadOut = out.length === 1 ? out[0] : out;
|
||||
log(`http -> 200 ${JSON.stringify(payloadOut).slice(0, 300)}`);
|
||||
res.writeHead(200, {
|
||||
"Content-Type": "application/json",
|
||||
"Mcp-Session-Id": "probe-session-1",
|
||||
});
|
||||
res.end(JSON.stringify(payloadOut));
|
||||
});
|
||||
});
|
||||
server.listen(port, "127.0.0.1", () => {
|
||||
log(`stub MCP server listening on http://127.0.0.1:${port}/mcp (transport=streamable-http)`);
|
||||
});
|
||||
server.on("error", (e) => { log(`listen error ${e.message}`); process.exit(1); });
|
||||
}
|
||||
|
||||
/* ---------------- stdio ---------------- */
|
||||
|
||||
function startStdio() {
|
||||
log("stub MCP server on stdio (transport=stdio)");
|
||||
let buf = "";
|
||||
process.stdin.setEncoding("utf8");
|
||||
process.stdin.on("data", (chunk) => {
|
||||
buf += chunk;
|
||||
let idx;
|
||||
while ((idx = buf.indexOf("\n")) >= 0) {
|
||||
const line = buf.slice(0, idx).trim();
|
||||
buf = buf.slice(idx + 1);
|
||||
if (!line) continue;
|
||||
let msg;
|
||||
try { msg = JSON.parse(line); } catch (e) { log(`stdio parse-error ${e.message}`); continue; }
|
||||
const out = dispatch(msg);
|
||||
for (const r of out) process.stdout.write(JSON.stringify(r) + "\n");
|
||||
}
|
||||
});
|
||||
process.stdin.on("end", () => process.exit(0));
|
||||
}
|
||||
|
||||
const argv = process.argv.slice(2);
|
||||
const httpFlag = argv.indexOf("--http");
|
||||
if (httpFlag >= 0) {
|
||||
const port = Number(argv[httpFlag + 1] || 39877);
|
||||
startHttp(port);
|
||||
} else {
|
||||
startStdio();
|
||||
}
|
||||
Reference in new issue
Block a user