@ppdocs/mcp 3.4.0 → 3.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config.d.ts +1 -0
- package/dist/config.js +8 -2
- package/dist/index.js +2 -1
- package/dist/tools/discussion.js +3 -1
- package/dist/tools/doc_query.d.ts +10 -0
- package/dist/tools/doc_query.js +185 -0
- package/dist/tools/flowchart.js +185 -67
- package/dist/tools/index.d.ts +3 -2
- package/dist/tools/index.js +7 -3
- package/dist/tools/init.js +1 -0
- package/dist/tools/shared.d.ts +2 -1
- package/dist/tools/shared.js +2 -2
- package/package.json +1 -1
package/dist/config.d.ts
CHANGED
package/dist/config.js
CHANGED
|
@@ -16,7 +16,13 @@ function readEnvConfig() {
|
|
|
16
16
|
if (!apiUrl)
|
|
17
17
|
return null;
|
|
18
18
|
const match = apiUrl.match(/\/api\/([^/]+)\/[^/]+\/?$/);
|
|
19
|
-
return {
|
|
19
|
+
return {
|
|
20
|
+
apiUrl,
|
|
21
|
+
projectId: match?.[1] || 'unknown',
|
|
22
|
+
user: process.env.PPDOCS_USER || generateUser(),
|
|
23
|
+
agentId: process.env.PPDOCS_AGENT_ID || 'default',
|
|
24
|
+
source: 'env',
|
|
25
|
+
};
|
|
20
26
|
}
|
|
21
27
|
function readPpdocsFile() {
|
|
22
28
|
const configPath = path.join(process.cwd(), '.ppdocs');
|
|
@@ -26,7 +32,7 @@ function readPpdocsFile() {
|
|
|
26
32
|
const c = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
27
33
|
if (!c.api || !c.projectId || !c.key)
|
|
28
34
|
return null;
|
|
29
|
-
return { apiUrl: `${c.api}/api/${c.projectId}/${c.key}`, projectId: c.projectId, user: c.user || generateUser(), source: 'file' };
|
|
35
|
+
return { apiUrl: `${c.api}/api/${c.projectId}/${c.key}`, projectId: c.projectId, user: c.user || generateUser(), agentId: c.agentId || process.env.PPDOCS_AGENT_ID || 'default', source: 'file' };
|
|
30
36
|
}
|
|
31
37
|
catch {
|
|
32
38
|
return null;
|
package/dist/index.js
CHANGED
|
@@ -39,8 +39,9 @@ async function main() {
|
|
|
39
39
|
}
|
|
40
40
|
const projectId = config?.projectId || 'pending';
|
|
41
41
|
const user = config?.user || 'agent';
|
|
42
|
+
const agentId = config?.agentId || process.env.PPDOCS_AGENT_ID || 'default';
|
|
42
43
|
const server = new McpServer({ name: `ppdocs [${projectId}]`, version: VERSION }, { capabilities: { tools: {} } });
|
|
43
|
-
registerTools(server, projectId, user, (newProjectId, newApiUrl) => {
|
|
44
|
+
registerTools(server, projectId, user, agentId, (newProjectId, newApiUrl) => {
|
|
44
45
|
console.error(`[kg_init] 项目已切换: ${newProjectId}`);
|
|
45
46
|
});
|
|
46
47
|
const transport = new StdioServerTransport();
|
package/dist/tools/discussion.js
CHANGED
|
@@ -15,7 +15,9 @@ import { getClient } from '../storage/httpClient.js';
|
|
|
15
15
|
import { decodeObjectStrings } from '../utils.js';
|
|
16
16
|
import { wrap, safeTool } from './shared.js';
|
|
17
17
|
function sender(ctx) {
|
|
18
|
-
return
|
|
18
|
+
return ctx.agentId && ctx.agentId !== 'default'
|
|
19
|
+
? `${ctx.projectId}:${ctx.user}:${ctx.agentId}`
|
|
20
|
+
: `${ctx.projectId}:${ctx.user}`;
|
|
19
21
|
}
|
|
20
22
|
/** 提取 sender 中的 projectId */
|
|
21
23
|
function senderProjectId(senderStr) {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* kg_doc — 文档查询视图
|
|
3
|
+
* 纯查询工具,数据来源为流程图节点的 docEntries + description
|
|
4
|
+
* 不创建独立文档,只是提供面向文档的查询入口
|
|
5
|
+
*
|
|
6
|
+
* read 默认返回当前文档,history=true 返回历史列表,index=N 返回指定历史条目
|
|
7
|
+
*/
|
|
8
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
9
|
+
import { type McpContext } from './shared.js';
|
|
10
|
+
export declare function registerDocQueryTools(server: McpServer, _ctx: McpContext): void;
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* kg_doc — 文档查询视图
|
|
3
|
+
* 纯查询工具,数据来源为流程图节点的 docEntries + description
|
|
4
|
+
* 不创建独立文档,只是提供面向文档的查询入口
|
|
5
|
+
*
|
|
6
|
+
* read 默认返回当前文档,history=true 返回历史列表,index=N 返回指定历史条目
|
|
7
|
+
*/
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import { getClient } from '../storage/httpClient.js';
|
|
10
|
+
import { decodeObjectStrings } from '../utils.js';
|
|
11
|
+
import { wrap, safeTool } from './shared.js';
|
|
12
|
+
function normalizeText(value) {
|
|
13
|
+
return (value ?? '').trim().toLowerCase();
|
|
14
|
+
}
|
|
15
|
+
/** 文档专用评分 — 侧重 summary/content/description */
|
|
16
|
+
function scoreDocMatch(node, terms) {
|
|
17
|
+
const description = normalizeText(node.description);
|
|
18
|
+
const entries = node.docEntries ?? [];
|
|
19
|
+
const allSummaries = entries.map((e) => e.summary).join(' ').toLowerCase();
|
|
20
|
+
const allContents = entries.map((e) => e.content).join(' ').toLowerCase();
|
|
21
|
+
const label = normalizeText(node.label);
|
|
22
|
+
let score = 0;
|
|
23
|
+
let matched = 0;
|
|
24
|
+
for (const term of terms) {
|
|
25
|
+
let hit = false;
|
|
26
|
+
if (allSummaries.includes(term)) {
|
|
27
|
+
score += 40;
|
|
28
|
+
hit = true;
|
|
29
|
+
}
|
|
30
|
+
if (allContents.includes(term)) {
|
|
31
|
+
score += 30;
|
|
32
|
+
hit = true;
|
|
33
|
+
}
|
|
34
|
+
if (description.includes(term)) {
|
|
35
|
+
score += 20;
|
|
36
|
+
hit = true;
|
|
37
|
+
}
|
|
38
|
+
if (label.includes(term)) {
|
|
39
|
+
score += 10;
|
|
40
|
+
hit = true;
|
|
41
|
+
}
|
|
42
|
+
if (hit)
|
|
43
|
+
matched += 1;
|
|
44
|
+
}
|
|
45
|
+
return matched === 0 ? 0 : score + matched * 50;
|
|
46
|
+
}
|
|
47
|
+
/** 加载所有流程图的完整数据 */
|
|
48
|
+
async function loadAllCharts() {
|
|
49
|
+
const client = getClient();
|
|
50
|
+
const briefs = (await client.listFlowcharts());
|
|
51
|
+
const loaded = await Promise.all(briefs.map(async (b) => (await client.getFlowchart(b.id))));
|
|
52
|
+
return loaded.filter((c) => Boolean(c));
|
|
53
|
+
}
|
|
54
|
+
/** 格式化日期为简短形式 */
|
|
55
|
+
function shortDate(iso) {
|
|
56
|
+
try {
|
|
57
|
+
return iso.slice(0, 10);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return iso;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export function registerDocQueryTools(server, _ctx) {
|
|
64
|
+
server.tool('kg_doc', '📘 文档查询视图 — 跨流程图搜索和阅读节点内嵌文档(docEntries + description)。纯查询,不创建独立文档。\n' +
|
|
65
|
+
'调用方式:search(query:"关键词") 全文搜索 | list 列出有文档的节点 | read(nodeId) 读取当前文档。\n' +
|
|
66
|
+
'read(nodeId, history:true) 查看历史列表(标题+日期) | read(nodeId, index:N) 读取第N条历史详情(0=最新)。\n' +
|
|
67
|
+
'actions: search(全文搜索)|list(文档列表)|read(读取文档,支持history/index参数)', {
|
|
68
|
+
action: z.enum(['search', 'list', 'read']).describe('操作类型 (Allowed: search, list, read)'),
|
|
69
|
+
query: z.string().optional().describe('搜索关键词 (search)'),
|
|
70
|
+
nodeId: z.string().optional().describe('节点ID (read)'),
|
|
71
|
+
chartId: z.string().optional().describe('限定流程图ID'),
|
|
72
|
+
history: z.boolean().optional().describe('read时传true返回历史列表(标题+日期)'),
|
|
73
|
+
index: z.number().optional().describe('read时指定历史条目序号(0=最新, 1=次新...)'),
|
|
74
|
+
limit: z.number().optional().describe('搜索结果限制, default 10'),
|
|
75
|
+
}, async (args) => safeTool(async () => {
|
|
76
|
+
const decoded = decodeObjectStrings(args);
|
|
77
|
+
switch (decoded.action) {
|
|
78
|
+
case 'search': {
|
|
79
|
+
const rawQuery = decoded.query?.trim();
|
|
80
|
+
if (!rawQuery)
|
|
81
|
+
return wrap('search requires query.');
|
|
82
|
+
const terms = rawQuery.toLowerCase().split(/\s+/).filter(Boolean);
|
|
83
|
+
const charts = decoded.chartId
|
|
84
|
+
? [(await getClient().getFlowchart(decoded.chartId))].filter((c) => Boolean(c))
|
|
85
|
+
: await loadAllCharts();
|
|
86
|
+
const matches = charts
|
|
87
|
+
.flatMap((chart) => chart.nodes
|
|
88
|
+
.filter((n) => (n.docEntries && n.docEntries.length > 0) || n.description)
|
|
89
|
+
.map((node) => ({ chart, node, score: scoreDocMatch(node, terms) })))
|
|
90
|
+
.filter((m) => m.score > 0)
|
|
91
|
+
.sort((a, b) => b.score - a.score)
|
|
92
|
+
.slice(0, decoded.limit ?? 10);
|
|
93
|
+
if (matches.length === 0)
|
|
94
|
+
return wrap(`No docs matched: ${rawQuery}`);
|
|
95
|
+
const lines = [`Doc search: "${rawQuery}" (${matches.length} results)`, ''];
|
|
96
|
+
for (const { chart, node } of matches) {
|
|
97
|
+
const docCount = node.docEntries?.length ?? 0;
|
|
98
|
+
const latest = docCount > 0 ? node.docEntries[docCount - 1] : null;
|
|
99
|
+
lines.push(`- **${node.label}** [${chart.id}/${node.id}] history=${docCount}`);
|
|
100
|
+
if (latest)
|
|
101
|
+
lines.push(` current: ${shortDate(latest.date)} — ${latest.summary}`);
|
|
102
|
+
if (node.description)
|
|
103
|
+
lines.push(` desc: ${node.description.slice(0, 80)}${node.description.length > 80 ? '...' : ''}`);
|
|
104
|
+
}
|
|
105
|
+
return wrap(lines.join('\n'));
|
|
106
|
+
}
|
|
107
|
+
case 'list': {
|
|
108
|
+
const charts = decoded.chartId
|
|
109
|
+
? [(await getClient().getFlowchart(decoded.chartId))].filter((c) => Boolean(c))
|
|
110
|
+
: await loadAllCharts();
|
|
111
|
+
const nodesWithDocs = charts.flatMap((chart) => chart.nodes
|
|
112
|
+
.filter((n) => (n.docEntries && n.docEntries.length > 0) || n.description)
|
|
113
|
+
.map((n) => ({ chart, node: n })));
|
|
114
|
+
if (nodesWithDocs.length === 0)
|
|
115
|
+
return wrap('No nodes with documentation found.');
|
|
116
|
+
const lines = [`Documented nodes: ${nodesWithDocs.length}`, ''];
|
|
117
|
+
for (const { chart, node } of nodesWithDocs) {
|
|
118
|
+
const docCount = node.docEntries?.length ?? 0;
|
|
119
|
+
const latest = docCount > 0 ? node.docEntries[docCount - 1] : null;
|
|
120
|
+
const latestInfo = latest ? ` | ${shortDate(latest.date)} ${latest.summary}` : '';
|
|
121
|
+
lines.push(`- ${node.label} [${chart.id}/${node.id}] history=${docCount}${latestInfo}`);
|
|
122
|
+
}
|
|
123
|
+
return wrap(lines.join('\n'));
|
|
124
|
+
}
|
|
125
|
+
case 'read': {
|
|
126
|
+
if (!decoded.nodeId)
|
|
127
|
+
return wrap('read requires nodeId.');
|
|
128
|
+
const chartId = decoded.chartId || 'main';
|
|
129
|
+
const chart = (await getClient().getFlowchart(chartId));
|
|
130
|
+
if (!chart)
|
|
131
|
+
return wrap(`Flowchart not found: ${chartId}`);
|
|
132
|
+
const node = chart.nodes.find((n) => n.id === decoded.nodeId);
|
|
133
|
+
if (!node)
|
|
134
|
+
return wrap(`Node not found: ${decoded.nodeId} in ${chartId}`);
|
|
135
|
+
const entries = node.docEntries ?? [];
|
|
136
|
+
// history=true → 列出历史记录(标题+日期)
|
|
137
|
+
if (decoded.history) {
|
|
138
|
+
if (entries.length === 0)
|
|
139
|
+
return wrap(`No history for ${node.label} [${chartId}/${decoded.nodeId}]`);
|
|
140
|
+
const lines = [`## ${node.label} — history (${entries.length})`, ''];
|
|
141
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
142
|
+
const e = entries[i];
|
|
143
|
+
const tag = i === entries.length - 1 ? ' (current)' : '';
|
|
144
|
+
lines.push(`${entries.length - 1 - i}. ${shortDate(e.date)} — ${e.summary}${tag}`);
|
|
145
|
+
}
|
|
146
|
+
lines.push('', '_Use read(nodeId, index:N) to view specific entry._');
|
|
147
|
+
return wrap(lines.join('\n'));
|
|
148
|
+
}
|
|
149
|
+
// index=N → 读取指定历史条目 (0=最新)
|
|
150
|
+
if (decoded.index !== undefined) {
|
|
151
|
+
const idx = entries.length - 1 - decoded.index;
|
|
152
|
+
if (idx < 0 || idx >= entries.length) {
|
|
153
|
+
return wrap(`Index out of range: ${decoded.index} (total: ${entries.length})`);
|
|
154
|
+
}
|
|
155
|
+
const entry = entries[idx];
|
|
156
|
+
const lines = [
|
|
157
|
+
`## ${node.label} — #${decoded.index}`,
|
|
158
|
+
`${shortDate(entry.date)} — ${entry.summary}`,
|
|
159
|
+
'',
|
|
160
|
+
entry.content,
|
|
161
|
+
];
|
|
162
|
+
return wrap(lines.join('\n'));
|
|
163
|
+
}
|
|
164
|
+
// 默认: 返回当前文档 (description + 最新 docEntry)
|
|
165
|
+
const lines = [`## ${node.label} [${chartId}/${node.id}]`, ''];
|
|
166
|
+
if (node.description) {
|
|
167
|
+
lines.push(node.description, '');
|
|
168
|
+
}
|
|
169
|
+
if (entries.length > 0) {
|
|
170
|
+
const latest = entries[entries.length - 1];
|
|
171
|
+
lines.push(`---`, `**${latest.summary}** (${shortDate(latest.date)})`, '', latest.content);
|
|
172
|
+
if (entries.length > 1) {
|
|
173
|
+
lines.push('', `_${entries.length - 1} older entries. Use read(nodeId, history:true) to browse._`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
else if (!node.description) {
|
|
177
|
+
lines.push('_No documentation._');
|
|
178
|
+
}
|
|
179
|
+
return wrap(lines.join('\n'));
|
|
180
|
+
}
|
|
181
|
+
default:
|
|
182
|
+
return wrap(`Unknown action: ${String(decoded.action)}`);
|
|
183
|
+
}
|
|
184
|
+
}));
|
|
185
|
+
}
|
package/dist/tools/flowchart.js
CHANGED
|
@@ -32,7 +32,6 @@ function ensureNodeShape(node) {
|
|
|
32
32
|
boundDirs: ensureArray(node.boundDirs),
|
|
33
33
|
boundTasks: ensureArray(node.boundTasks),
|
|
34
34
|
docEntries: Array.isArray(node.docEntries) ? [...node.docEntries] : [],
|
|
35
|
-
versions: Array.isArray(node.versions) ? [...node.versions] : [],
|
|
36
35
|
};
|
|
37
36
|
}
|
|
38
37
|
function buildNode(input) {
|
|
@@ -52,7 +51,6 @@ function buildNode(input) {
|
|
|
52
51
|
boundTasks: [],
|
|
53
52
|
subFlowchart: input.subFlowchart || null,
|
|
54
53
|
docEntries: [],
|
|
55
|
-
versions: [{ version: 'v1.0', date: now, changes: 'initial' }],
|
|
56
54
|
lastUpdated: now,
|
|
57
55
|
lastQueried: '',
|
|
58
56
|
};
|
|
@@ -81,27 +79,98 @@ function formatNodeLine(node) {
|
|
|
81
79
|
function formatEdgeLine(edge) {
|
|
82
80
|
const label = edge.label ? ` [${edge.label}]` : '';
|
|
83
81
|
const type = edge.edgeType ? ` (${edge.edgeType})` : '';
|
|
84
|
-
return `- ${edge.from}
|
|
82
|
+
return `- ${edge.from} → ${edge.to}${label}${type}`;
|
|
85
83
|
}
|
|
84
|
+
/** 紧凑节点格式 (用于 get 视图) — 含内联 doc 摘要 */
|
|
85
|
+
function formatNodeCompact(node) {
|
|
86
|
+
const badges = [];
|
|
87
|
+
const fileCount = (node.boundFiles?.length ?? 0) + (node.boundDirs?.length ?? 0);
|
|
88
|
+
if (fileCount > 0)
|
|
89
|
+
badges.push(`files=${fileCount}`);
|
|
90
|
+
if (node.boundTasks?.length)
|
|
91
|
+
badges.push(`tasks=${node.boundTasks.length}`);
|
|
92
|
+
const docCount = node.docEntries?.length ?? 0;
|
|
93
|
+
if (docCount > 0)
|
|
94
|
+
badges.push(`docs=${docCount}`);
|
|
95
|
+
if (node.subFlowchart)
|
|
96
|
+
badges.push(`▶${node.subFlowchart}`);
|
|
97
|
+
const meta = `${node.nodeType ?? 'process'}/${node.domain ?? 'system'}`;
|
|
98
|
+
const suffix = badges.length > 0 ? ` [${badges.join(' ')}]` : '';
|
|
99
|
+
const desc = node.description ? `\n > ${node.description.slice(0, 100)}${node.description.length > 100 ? '…' : ''}` : '';
|
|
100
|
+
const latestDoc = docCount > 0 ? `\n doc: ${node.docEntries[docCount - 1].summary}` : '';
|
|
101
|
+
return `- **${node.label}** \`${node.id}\` (${meta})${suffix}${desc}${latestDoc}`;
|
|
102
|
+
}
|
|
103
|
+
/** 拓扑树 — DFS 从入度为0的节点开始, 展示调用结构 */
|
|
104
|
+
function buildTopologyTree(chart) {
|
|
105
|
+
const nodeMap = new Map(chart.nodes.map((n) => [n.id, n]));
|
|
106
|
+
const inDegree = new Map();
|
|
107
|
+
for (const n of chart.nodes)
|
|
108
|
+
inDegree.set(n.id, 0);
|
|
109
|
+
for (const e of chart.edges) {
|
|
110
|
+
inDegree.set(e.to, (inDegree.get(e.to) ?? 0) + 1);
|
|
111
|
+
}
|
|
112
|
+
// 按 from 分组出边
|
|
113
|
+
const outEdges = new Map();
|
|
114
|
+
for (const e of chart.edges) {
|
|
115
|
+
if (!outEdges.has(e.from))
|
|
116
|
+
outEdges.set(e.from, []);
|
|
117
|
+
outEdges.get(e.from).push(e);
|
|
118
|
+
}
|
|
119
|
+
const roots = chart.nodes.filter((n) => (inDegree.get(n.id) ?? 0) === 0);
|
|
120
|
+
if (roots.length === 0)
|
|
121
|
+
return ['(no root nodes — graph may contain cycles)'];
|
|
122
|
+
const lines = [];
|
|
123
|
+
const visited = new Set();
|
|
124
|
+
function dfs(nodeId, indent) {
|
|
125
|
+
if (visited.has(nodeId)) {
|
|
126
|
+
lines.push(`${' '.repeat(indent)}↺ ${nodeId} (cycle)`);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
visited.add(nodeId);
|
|
130
|
+
const node = nodeMap.get(nodeId);
|
|
131
|
+
const label = node ? node.label : nodeId;
|
|
132
|
+
lines.push(`${' '.repeat(indent)}${indent === 0 ? '◆' : '├'} ${label} [${nodeId}]`);
|
|
133
|
+
const children = outEdges.get(nodeId) ?? [];
|
|
134
|
+
for (const edge of children) {
|
|
135
|
+
const edgeLabel = edge.label ? ` —${edge.label}→ ` : ' → ';
|
|
136
|
+
if (visited.has(edge.to)) {
|
|
137
|
+
const target = nodeMap.get(edge.to);
|
|
138
|
+
lines.push(`${' '.repeat(indent + 1)}├${edgeLabel}↺ ${target?.label ?? edge.to}`);
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
if (children.length > 1 || edge.label) {
|
|
142
|
+
lines.push(`${' '.repeat(indent + 1)}${edgeLabel.trim()}`);
|
|
143
|
+
}
|
|
144
|
+
dfs(edge.to, indent + 1);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
for (const root of roots) {
|
|
149
|
+
dfs(root.id, 0);
|
|
150
|
+
}
|
|
151
|
+
// 未访问的孤立节点
|
|
152
|
+
const orphans = chart.nodes.filter((n) => !visited.has(n.id));
|
|
153
|
+
if (orphans.length > 0) {
|
|
154
|
+
lines.push('', '(orphans):');
|
|
155
|
+
for (const n of orphans) {
|
|
156
|
+
lines.push(` ○ ${n.label} [${n.id}]`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return lines;
|
|
160
|
+
}
|
|
161
|
+
/** 封装文档更新:自动将上一条归档,追加新条目 */
|
|
86
162
|
function appendDocEntry(node, summary, content) {
|
|
87
163
|
const nextNode = ensureNodeShape(node);
|
|
88
164
|
const entries = nextNode.docEntries ?? [];
|
|
89
|
-
|
|
90
|
-
if (entries.length > 0) {
|
|
91
|
-
const last = entries[entries.length - 1]?.version ?? '';
|
|
92
|
-
const parsed = Number.parseFloat(last.replace(/^v/, ''));
|
|
93
|
-
if (!Number.isNaN(parsed)) {
|
|
94
|
-
nextVer = Math.round((parsed + 0.1) * 10) / 10;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
165
|
+
const version = `v${entries.length + 1}.0`;
|
|
97
166
|
entries.push({
|
|
98
|
-
version: `v${nextVer.toFixed(1)}`,
|
|
99
167
|
date: new Date().toISOString(),
|
|
100
168
|
summary: summary.replace(/\\n/g, '\n'),
|
|
101
169
|
content: content.replace(/\\n/g, '\n'),
|
|
102
170
|
});
|
|
103
171
|
nextNode.docEntries = entries;
|
|
104
|
-
|
|
172
|
+
nextNode.lastUpdated = new Date().toISOString();
|
|
173
|
+
return { node: nextNode, version };
|
|
105
174
|
}
|
|
106
175
|
function collectNeighborLayers(chart, startNodeId, maxDepth) {
|
|
107
176
|
const nodeMap = new Map(chart.nodes.map((node) => [node.id, node]));
|
|
@@ -295,8 +364,10 @@ export function registerFlowchartTools(server, _ctx) {
|
|
|
295
364
|
return wrap('No flowcharts found.');
|
|
296
365
|
}
|
|
297
366
|
const lines = ['Flowcharts:', '', ...charts.map((chart) => {
|
|
298
|
-
const parent = chart.parentChart
|
|
299
|
-
|
|
367
|
+
const parent = chart.parentChart
|
|
368
|
+
? ` ← ${chart.parentChart}${chart.parentNode ? `/${chart.parentNode}` : ''}`
|
|
369
|
+
: '';
|
|
370
|
+
return `- ${chart.title} [${chart.id}] ${chart.nodeCount}n/${chart.edgeCount}e${parent}`;
|
|
300
371
|
})];
|
|
301
372
|
return wrap(lines.join('\n'));
|
|
302
373
|
}
|
|
@@ -307,16 +378,46 @@ export function registerFlowchartTools(server, _ctx) {
|
|
|
307
378
|
return wrap(`Flowchart not found: ${chartId}`);
|
|
308
379
|
}
|
|
309
380
|
const lines = [
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
''
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
381
|
+
`# ${chart.title} [${chart.id}]`,
|
|
382
|
+
];
|
|
383
|
+
if (chart.parentChart) {
|
|
384
|
+
lines.push(`parent: ${chart.parentChart}${chart.parentNode ? `/${chart.parentNode}` : ''}`);
|
|
385
|
+
}
|
|
386
|
+
// Mermaid 流程图
|
|
387
|
+
lines.push('', '```mermaid', 'graph TD');
|
|
388
|
+
// 节点声明
|
|
389
|
+
for (const n of chart.nodes) {
|
|
390
|
+
const safeLabel = n.label.replace(/"/g, '#quot;');
|
|
391
|
+
lines.push(` ${n.id}["${safeLabel}"]`);
|
|
392
|
+
}
|
|
393
|
+
// 边
|
|
394
|
+
for (const e of chart.edges) {
|
|
395
|
+
const arrow = e.label ? `-->|${e.label}|` : '-->';
|
|
396
|
+
lines.push(` ${e.from} ${arrow} ${e.to}`);
|
|
397
|
+
}
|
|
398
|
+
lines.push('```');
|
|
399
|
+
// 节点简介表 (MD table)
|
|
400
|
+
lines.push('', `## Nodes (${chart.nodes.length})`, '');
|
|
401
|
+
lines.push('| 节点 | 类型 | 简介 | 绑定文件 |');
|
|
402
|
+
lines.push('|:-----|:-----|:-----|:---------|');
|
|
403
|
+
for (const n of chart.nodes) {
|
|
404
|
+
const nameCol = n.subFlowchart
|
|
405
|
+
? `**${n.label}** \`${n.id}\` ▶${n.subFlowchart}`
|
|
406
|
+
: `**${n.label}** \`${n.id}\``;
|
|
407
|
+
const typeCol = `${n.nodeType ?? 'process'}/${n.domain ?? 'system'}`;
|
|
408
|
+
const descCol = n.description
|
|
409
|
+
? n.description.slice(0, 80).replace(/[\|\n]/g, ' ') + (n.description.length > 80 ? '…' : '')
|
|
410
|
+
: '';
|
|
411
|
+
const fileNames = [];
|
|
412
|
+
for (const f of (n.boundFiles ?? [])) {
|
|
413
|
+
fileNames.push(f.replace(/\\/g, '/').split('/').pop() ?? f);
|
|
414
|
+
}
|
|
415
|
+
for (const d of (n.boundDirs ?? [])) {
|
|
416
|
+
fileNames.push(d.replace(/\\/g, '/').split('/').pop() + '/');
|
|
417
|
+
}
|
|
418
|
+
const fileCol = fileNames.length > 0 ? fileNames.join(', ') : '-';
|
|
419
|
+
lines.push(`| ${nameCol} | ${typeCol} | ${descCol} | ${fileCol} |`);
|
|
420
|
+
}
|
|
320
421
|
return wrap(lines.join('\n'));
|
|
321
422
|
}
|
|
322
423
|
case 'search': {
|
|
@@ -463,63 +564,81 @@ export function registerFlowchartTools(server, _ctx) {
|
|
|
463
564
|
const showTasks = decoded.includeTasks !== false;
|
|
464
565
|
const showFiles = decoded.includeFiles !== false;
|
|
465
566
|
const showDoc = decoded.includeDoc !== false;
|
|
567
|
+
const relations = collectNodeRelations(chart, node.id);
|
|
466
568
|
const lines = [
|
|
467
|
-
|
|
468
|
-
|
|
569
|
+
`## ${node.label} \`${node.id}\``,
|
|
570
|
+
`${node.nodeType ?? 'process'} / ${node.domain ?? 'system'}`,
|
|
469
571
|
];
|
|
470
572
|
if (node.description) {
|
|
471
|
-
lines.push(
|
|
573
|
+
lines.push('', '### Description');
|
|
574
|
+
lines.push(...node.description.split('\n').map((l) => `> ${l}`));
|
|
472
575
|
}
|
|
473
576
|
if (node.input && node.input.length > 0) {
|
|
474
|
-
lines.push(
|
|
577
|
+
lines.push(`\nInput: ${node.input.join(', ')}`);
|
|
475
578
|
}
|
|
476
579
|
if (node.output && node.output.length > 0) {
|
|
477
580
|
lines.push(`Output: ${node.output.join(', ')}`);
|
|
478
581
|
}
|
|
479
582
|
if (node.subFlowchart) {
|
|
480
|
-
lines.push(`Sub
|
|
583
|
+
lines.push(`Sub-chart: ▶ ${node.subFlowchart}`);
|
|
481
584
|
}
|
|
482
585
|
if (node.lastUpdated) {
|
|
483
|
-
lines.push(`
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
}
|
|
499
|
-
else {
|
|
500
|
-
lines.push(`- ${entry.version} ${entry.date}: ${entry.summary}`);
|
|
501
|
-
}
|
|
502
|
-
});
|
|
503
|
-
lines.push('');
|
|
586
|
+
lines.push(`Updated: ${node.lastUpdated}`);
|
|
587
|
+
}
|
|
588
|
+
// 关系: →← 方向箭头
|
|
589
|
+
if (relations.incoming.length > 0 || relations.outgoing.length > 0) {
|
|
590
|
+
lines.push('', '### Relations');
|
|
591
|
+
for (const { edge, node: src } of relations.incoming) {
|
|
592
|
+
const lbl = edge.label ? ` [${edge.label}]` : '';
|
|
593
|
+
const typ = edge.edgeType ? ` (${edge.edgeType})` : '';
|
|
594
|
+
lines.push(`← ${src?.label ?? edge.from} \`${edge.from}\`${lbl}${typ}`);
|
|
595
|
+
}
|
|
596
|
+
for (const { edge, node: tgt } of relations.outgoing) {
|
|
597
|
+
const lbl = edge.label ? ` [${edge.label}]` : '';
|
|
598
|
+
const typ = edge.edgeType ? ` (${edge.edgeType})` : '';
|
|
599
|
+
lines.push(`→ ${tgt?.label ?? edge.to} \`${edge.to}\`${lbl}${typ}`);
|
|
600
|
+
}
|
|
504
601
|
}
|
|
602
|
+
// 邻居展开
|
|
505
603
|
if (expandDepth > 0) {
|
|
506
604
|
const layers = collectNeighborLayers(chart, node.id, expandDepth);
|
|
507
605
|
if (layers.length > 0) {
|
|
508
|
-
lines.push(
|
|
606
|
+
lines.push('', '### Neighbors');
|
|
509
607
|
layers.forEach((layer, index) => {
|
|
510
608
|
lines.push(`L${index + 1}:`);
|
|
511
609
|
lines.push(...layer.map((item) => ` ${item}`));
|
|
512
610
|
});
|
|
513
|
-
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
// 内嵌文档
|
|
614
|
+
if (showDoc && node.docEntries && node.docEntries.length > 0) {
|
|
615
|
+
lines.push('', `### Docs (${node.docEntries.length})`);
|
|
616
|
+
const showAll = decoded.fullDoc === true;
|
|
617
|
+
const latest = node.docEntries[node.docEntries.length - 1];
|
|
618
|
+
// 当前文档 (最新)
|
|
619
|
+
lines.push(`**${latest.summary}** (${latest.date.slice(0, 10)})`);
|
|
620
|
+
lines.push(latest.content);
|
|
621
|
+
// 历史记录
|
|
622
|
+
if (node.docEntries.length > 1) {
|
|
623
|
+
lines.push('', `History (${node.docEntries.length - 1}):`);
|
|
624
|
+
for (let i = node.docEntries.length - 2; i >= 0; i--) {
|
|
625
|
+
const entry = node.docEntries[i];
|
|
626
|
+
if (showAll) {
|
|
627
|
+
lines.push(`- ${entry.date.slice(0, 10)}: **${entry.summary}**`);
|
|
628
|
+
lines.push(entry.content);
|
|
629
|
+
}
|
|
630
|
+
else {
|
|
631
|
+
lines.push(`- ${entry.date.slice(0, 10)}: ${entry.summary}`);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
514
634
|
}
|
|
515
635
|
}
|
|
516
636
|
if (showDocs && node.boundDocs && node.boundDocs.length > 0) {
|
|
517
|
-
lines.push(
|
|
637
|
+
lines.push('', `### Bound docs (${node.boundDocs.length})`);
|
|
518
638
|
node.boundDocs.forEach((docPath) => lines.push(`- ${docPath}`));
|
|
519
|
-
lines.push('');
|
|
520
639
|
}
|
|
521
640
|
if (showTasks && node.boundTasks && node.boundTasks.length > 0) {
|
|
522
|
-
lines.push(
|
|
641
|
+
lines.push('', `### Tasks (${node.boundTasks.length})`);
|
|
523
642
|
for (const taskId of node.boundTasks) {
|
|
524
643
|
try {
|
|
525
644
|
const task = (await client().getTask(taskId));
|
|
@@ -534,24 +653,17 @@ export function registerFlowchartTools(server, _ctx) {
|
|
|
534
653
|
lines.push(`- [${taskId}]`);
|
|
535
654
|
}
|
|
536
655
|
}
|
|
537
|
-
lines.push('');
|
|
538
656
|
}
|
|
539
657
|
if (showFiles) {
|
|
540
658
|
const files = node.boundFiles ?? [];
|
|
541
659
|
const dirs = node.boundDirs ?? [];
|
|
542
660
|
if (files.length > 0 || dirs.length > 0) {
|
|
543
|
-
lines.push(
|
|
544
|
-
files.forEach((filePath) => lines.push(`-
|
|
545
|
-
dirs.forEach((dirPath) => lines.push(`-
|
|
546
|
-
lines.push('');
|
|
661
|
+
lines.push('', `### Code (${files.length} files / ${dirs.length} dirs)`);
|
|
662
|
+
files.forEach((filePath) => lines.push(`- ${filePath}`));
|
|
663
|
+
dirs.forEach((dirPath) => lines.push(`- ${dirPath}/`));
|
|
547
664
|
}
|
|
548
665
|
}
|
|
549
|
-
|
|
550
|
-
lines.push(`Versions (${node.versions.length}):`);
|
|
551
|
-
node.versions.slice(-5).forEach((version) => {
|
|
552
|
-
lines.push(`- ${version.version} ${version.date}: ${version.changes}`);
|
|
553
|
-
});
|
|
554
|
-
}
|
|
666
|
+
// Versions 已由 docEntries 历史替代,不再输出
|
|
555
667
|
return wrap(lines.join('\n').trim());
|
|
556
668
|
}
|
|
557
669
|
case 'update_node': {
|
|
@@ -574,6 +686,12 @@ export function registerFlowchartTools(server, _ctx) {
|
|
|
574
686
|
changes.push('label');
|
|
575
687
|
}
|
|
576
688
|
if (decoded.description !== undefined) {
|
|
689
|
+
// 自动归档旧 description 到 docEntries
|
|
690
|
+
if (nextNode.description && nextNode.description !== decoded.description) {
|
|
691
|
+
const archiveResult = appendDocEntry(nextNode, '[auto] description更新前快照', nextNode.description);
|
|
692
|
+
nextNode = archiveResult.node;
|
|
693
|
+
changes.push(`docArchive(${archiveResult.version})`);
|
|
694
|
+
}
|
|
577
695
|
nextNode.description = decoded.description;
|
|
578
696
|
changes.push('description');
|
|
579
697
|
}
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* MCP 工具注册入口
|
|
3
|
-
*
|
|
3
|
+
* 14 个工具, 8 个子模块
|
|
4
4
|
*
|
|
5
5
|
* 🔗 初始化: kg_init (1个)
|
|
6
6
|
* 📊 导航: kg_status (1个)
|
|
7
7
|
* 📚 知识: kg_projects, kg_workflow (2个)
|
|
8
8
|
* 📝 工作流: kg_task(任务记录), kg_files, kg_discuss(讨论区), kg_ref (4个)
|
|
9
9
|
* 🔀 关系核心: kg_flowchart(逻辑流程图 — 关系型知识锚点) (1个)
|
|
10
|
+
* 📘 文档查询: kg_doc(节点文档搜索/阅读/历史) (1个)
|
|
10
11
|
* 🔬 代码分析: code_scan, code_smart_context, code_full_path (3个)
|
|
11
12
|
* 🏛️ 协作: kg_meeting (1个)
|
|
12
13
|
*/
|
|
13
14
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
14
|
-
export declare function registerTools(server: McpServer, projectId: string, user: string, onProjectChange?: (newProjectId: string, newApiUrl: string) => void): void;
|
|
15
|
+
export declare function registerTools(server: McpServer, projectId: string, user: string, agentId: string, onProjectChange?: (newProjectId: string, newApiUrl: string) => void): void;
|
package/dist/tools/index.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* MCP 工具注册入口
|
|
3
|
-
*
|
|
3
|
+
* 14 个工具, 8 个子模块
|
|
4
4
|
*
|
|
5
5
|
* 🔗 初始化: kg_init (1个)
|
|
6
6
|
* 📊 导航: kg_status (1个)
|
|
7
7
|
* 📚 知识: kg_projects, kg_workflow (2个)
|
|
8
8
|
* 📝 工作流: kg_task(任务记录), kg_files, kg_discuss(讨论区), kg_ref (4个)
|
|
9
9
|
* 🔀 关系核心: kg_flowchart(逻辑流程图 — 关系型知识锚点) (1个)
|
|
10
|
+
* 📘 文档查询: kg_doc(节点文档搜索/阅读/历史) (1个)
|
|
10
11
|
* 🔬 代码分析: code_scan, code_smart_context, code_full_path (3个)
|
|
11
12
|
* 🏛️ 协作: kg_meeting (1个)
|
|
12
13
|
*/
|
|
@@ -22,8 +23,9 @@ import { registerReferenceTools } from './refs.js';
|
|
|
22
23
|
import { registerAnalyzerTools } from './analyzer.js';
|
|
23
24
|
import { registerMeetingTools } from './meeting.js';
|
|
24
25
|
import { registerFlowchartTools } from './flowchart.js';
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
import { registerDocQueryTools } from './doc_query.js';
|
|
27
|
+
export function registerTools(server, projectId, user, agentId, onProjectChange) {
|
|
28
|
+
const ctx = createContext(projectId, user, agentId);
|
|
27
29
|
// 🔗 初始化
|
|
28
30
|
registerInitTool(server, ctx, onProjectChange || (() => { }));
|
|
29
31
|
// 📊 导航
|
|
@@ -42,4 +44,6 @@ export function registerTools(server, projectId, user, onProjectChange) {
|
|
|
42
44
|
registerMeetingTools(server, ctx);
|
|
43
45
|
// 🔀 流程图
|
|
44
46
|
registerFlowchartTools(server, ctx);
|
|
47
|
+
// 📘 文档查询
|
|
48
|
+
registerDocQueryTools(server, ctx);
|
|
45
49
|
}
|
package/dist/tools/init.js
CHANGED
|
@@ -60,6 +60,7 @@ export function registerInitTool(server, ctx, onProjectChange) {
|
|
|
60
60
|
// 4. 更新共享上下文 — 所有工具立刻获得新 projectId
|
|
61
61
|
ctx.projectId = config.projectId;
|
|
62
62
|
ctx.user = config.user;
|
|
63
|
+
ctx.agentId = process.env.PPDOCS_AGENT_ID || 'default';
|
|
63
64
|
// 5. 通知主进程
|
|
64
65
|
onProjectChange(config.projectId, config.apiUrl);
|
|
65
66
|
return wrap(`✅ 项目上下文已初始化\n\n` +
|
package/dist/tools/shared.d.ts
CHANGED
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
export interface McpContext {
|
|
7
7
|
projectId: string;
|
|
8
8
|
user: string;
|
|
9
|
+
agentId: string;
|
|
9
10
|
}
|
|
10
11
|
/** 创建可变上下文对象 (工具闭包捕获此对象的引用,kg_init 更新其属性) */
|
|
11
|
-
export declare function createContext(projectId: string, user: string): McpContext;
|
|
12
|
+
export declare function createContext(projectId: string, user: string, agentId?: string): McpContext;
|
|
12
13
|
export declare function wrap(text: string): {
|
|
13
14
|
content: {
|
|
14
15
|
type: "text";
|
package/dist/tools/shared.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* 合并原 helpers.ts + 新增 safeTool / withCross 模式
|
|
4
4
|
*/
|
|
5
5
|
/** 创建可变上下文对象 (工具闭包捕获此对象的引用,kg_init 更新其属性) */
|
|
6
|
-
export function createContext(projectId, user) {
|
|
7
|
-
return { projectId, user };
|
|
6
|
+
export function createContext(projectId, user, agentId = 'default') {
|
|
7
|
+
return { projectId, user, agentId };
|
|
8
8
|
}
|
|
9
9
|
// ==================== MCP 返回包装 ====================
|
|
10
10
|
export function wrap(text) {
|