@ppdocs/mcp 3.2.21 → 3.2.23
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/cli.js +21 -22
- package/dist/storage/httpClient.d.ts +13 -12
- package/dist/storage/httpClient.js +60 -32
- package/dist/tools/analyzer.d.ts +4 -3
- package/dist/tools/analyzer.js +7 -236
- package/dist/tools/discussion.d.ts +8 -2
- package/dist/tools/discussion.js +123 -34
- package/dist/tools/docs.js +41 -3
- package/dist/tools/flowchart.d.ts +7 -0
- package/dist/tools/flowchart.js +483 -0
- package/dist/tools/index.d.ts +8 -7
- package/dist/tools/index.js +12 -8
- package/dist/tools/rules.js +19 -1
- package/dist/tools/tasks.d.ts +2 -2
- package/dist/tools/tasks.js +112 -24
- package/package.json +1 -1
package/dist/tools/discussion.js
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 💬 kg_discuss (
|
|
3
|
-
* 合并: list, read, create, reply, close, delete
|
|
2
|
+
* 💬 kg_discuss (8 actions)
|
|
3
|
+
* 合并: list, read, create, reply, close, delete, complete, history
|
|
4
4
|
* 统一走 HTTP → Rust 后端 (单一写入者)
|
|
5
5
|
* sender 格式: "projectId:user" (如 "p-ca3sgejg:张三")
|
|
6
|
+
*
|
|
7
|
+
* 权限模型:
|
|
8
|
+
* list/history — 任何人可列出(全局可见)
|
|
9
|
+
* create — 任何人可发起(指定参与项目ID)
|
|
10
|
+
* read/reply — 仅讨论组内成员(initiator + participants)
|
|
11
|
+
* complete/close/delete — 仅发起人
|
|
6
12
|
*/
|
|
7
13
|
import { z } from 'zod';
|
|
8
14
|
import { getClient } from '../storage/httpClient.js';
|
|
@@ -11,6 +17,34 @@ import { wrap, safeTool } from './shared.js';
|
|
|
11
17
|
function sender(ctx) {
|
|
12
18
|
return `${ctx.projectId}:${ctx.user}`;
|
|
13
19
|
}
|
|
20
|
+
/** 提取 sender 中的 projectId */
|
|
21
|
+
function senderProjectId(senderStr) {
|
|
22
|
+
return senderStr.split(':')[0] || senderStr;
|
|
23
|
+
}
|
|
24
|
+
/** 检查项目是否是讨论的成员 (发起方 或 参与方) */
|
|
25
|
+
function isMember(brief, projectId) {
|
|
26
|
+
const initiatorPid = senderProjectId(brief.initiator);
|
|
27
|
+
if (initiatorPid === projectId)
|
|
28
|
+
return true;
|
|
29
|
+
return brief.participants.some(p => {
|
|
30
|
+
// participants 可能是 "projectId" 或 "projectId:user"
|
|
31
|
+
return senderProjectId(p) === projectId;
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
/** 检查是否是讨论的发起人 */
|
|
35
|
+
function isInitiator(brief, me, projectId) {
|
|
36
|
+
return brief.initiator === me || senderProjectId(brief.initiator) === projectId;
|
|
37
|
+
}
|
|
38
|
+
/** 根据 ID 获取讨论摘要 (用于权限检查) */
|
|
39
|
+
async function getBrief(discussionId) {
|
|
40
|
+
try {
|
|
41
|
+
const all = await getClient().discussionList();
|
|
42
|
+
return all.find(d => d.id === discussionId) || null;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
14
48
|
function relativeTime(iso) {
|
|
15
49
|
try {
|
|
16
50
|
const diff = Date.now() - new Date(iso).getTime();
|
|
@@ -37,8 +71,11 @@ function formatList(items, ctx) {
|
|
|
37
71
|
];
|
|
38
72
|
for (const d of items) {
|
|
39
73
|
const others = d.participants.filter(p => p !== d.initiator).join(', ') || '—';
|
|
40
|
-
|
|
74
|
+
const memberTag = isMember(d, ctx.projectId) ? '✅' : '🔒';
|
|
75
|
+
lines.push(`| ${d.id} | ${memberTag} ${d.title} | ${d.initiator} | ${others} | ${d.summary} | ${d.status} | ${relativeTime(d.updatedAt)} |`);
|
|
41
76
|
}
|
|
77
|
+
lines.push('');
|
|
78
|
+
lines.push('✅ = 你是成员(可读写) | 🔒 = 非成员(仅可查看标题)');
|
|
42
79
|
return lines.join('\n');
|
|
43
80
|
}
|
|
44
81
|
function formatDetailView(d) {
|
|
@@ -66,7 +103,7 @@ function formatDetailView(d) {
|
|
|
66
103
|
}
|
|
67
104
|
export function registerDiscussionTools(server, ctx) {
|
|
68
105
|
const client = () => getClient();
|
|
69
|
-
server.tool('kg_discuss', '💬 跨项目讨论 —
|
|
106
|
+
server.tool('kg_discuss', '💬 跨项目讨论 — action: list|read|create|reply|complete|close|delete|history。权限: 仅成员可读写,仅发起人可删除/归档', {
|
|
70
107
|
action: z.enum(['list', 'read', 'create', 'reply', 'complete', 'close', 'delete', 'history'])
|
|
71
108
|
.describe('操作类型'),
|
|
72
109
|
id: z.string().optional()
|
|
@@ -90,41 +127,15 @@ export function registerDiscussionTools(server, ctx) {
|
|
|
90
127
|
}, async (args) => safeTool(async () => {
|
|
91
128
|
const decoded = decodeObjectStrings(args);
|
|
92
129
|
const me = sender(ctx);
|
|
130
|
+
const myPid = ctx.projectId;
|
|
93
131
|
switch (decoded.action) {
|
|
132
|
+
// ============ 公开操作 ============
|
|
94
133
|
case 'list': {
|
|
95
134
|
const active = await client().discussionList();
|
|
96
135
|
if (!Array.isArray(active) || active.length === 0)
|
|
97
136
|
return wrap(`当前无活跃的讨论 (本项目: ${me})`);
|
|
98
137
|
return wrap(formatList(active, ctx));
|
|
99
138
|
}
|
|
100
|
-
case 'read': {
|
|
101
|
-
const readIds = decoded.ids || (decoded.id ? [decoded.id] : []);
|
|
102
|
-
if (readIds.length === 0)
|
|
103
|
-
return wrap('❌ read 需要 id 或 ids');
|
|
104
|
-
const readMode = decoded.mode || 'auto';
|
|
105
|
-
const discussions = await client().discussionReadByIds(readIds, me, readMode);
|
|
106
|
-
if (!Array.isArray(discussions) || discussions.length === 0)
|
|
107
|
-
return wrap('未找到对应的讨论记录');
|
|
108
|
-
return wrap(discussions.map(formatDetailView).join('\n\n━━━━━━━━━━━━━━━━━━━━\n\n'));
|
|
109
|
-
}
|
|
110
|
-
case 'create': {
|
|
111
|
-
if (!decoded.title)
|
|
112
|
-
return wrap('❌ create 需要 title');
|
|
113
|
-
if (!decoded.participants)
|
|
114
|
-
return wrap('❌ create 需要 participants');
|
|
115
|
-
if (!decoded.content)
|
|
116
|
-
return wrap('❌ create 需要 content');
|
|
117
|
-
const result = await client().discussionCreate(decoded.title, me, decoded.participants, decoded.content, decoded.summary);
|
|
118
|
-
return wrap(`✅ 讨论已发起\nID: ${result.id}\n发起方: ${me}\n参与方: ${decoded.participants.join(', ')}`);
|
|
119
|
-
}
|
|
120
|
-
case 'reply': {
|
|
121
|
-
if (!decoded.id)
|
|
122
|
-
return wrap('❌ reply 需要 id');
|
|
123
|
-
if (!decoded.content)
|
|
124
|
-
return wrap('❌ reply 需要 content');
|
|
125
|
-
await client().discussionReply(decoded.id, me, decoded.content, decoded.summary, decoded.newSummary);
|
|
126
|
-
return wrap(`✅ 回复成功 (ID: ${decoded.id}, 身份: ${me})`);
|
|
127
|
-
}
|
|
128
139
|
case 'history': {
|
|
129
140
|
const all = await client().discussionListAll(me);
|
|
130
141
|
if (!Array.isArray(all) || all.length === 0)
|
|
@@ -143,9 +154,72 @@ export function registerDiscussionTools(server, ctx) {
|
|
|
143
154
|
}
|
|
144
155
|
return wrap(lines.join('\n'));
|
|
145
156
|
}
|
|
157
|
+
case 'create': {
|
|
158
|
+
if (!decoded.title)
|
|
159
|
+
return wrap('❌ create 需要 title');
|
|
160
|
+
if (!decoded.participants)
|
|
161
|
+
return wrap('❌ create 需要 participants');
|
|
162
|
+
if (!decoded.content)
|
|
163
|
+
return wrap('❌ create 需要 content');
|
|
164
|
+
const result = await client().discussionCreate(decoded.title, me, decoded.participants, decoded.content, decoded.summary);
|
|
165
|
+
return wrap(`✅ 讨论已发起\nID: ${result.id}\n发起方: ${me}\n参与方: ${decoded.participants.join(', ')}\n\n🔒 权限: 仅以上项目可读写此讨论,仅发起方可删除/归档`);
|
|
166
|
+
}
|
|
167
|
+
// ============ 成员操作 (需验证成员身份) ============
|
|
168
|
+
case 'read': {
|
|
169
|
+
const readIds = decoded.ids || (decoded.id ? [decoded.id] : []);
|
|
170
|
+
if (readIds.length === 0)
|
|
171
|
+
return wrap('❌ read 需要 id 或 ids');
|
|
172
|
+
// 权限检查: 逐个验证成员身份
|
|
173
|
+
const allActive = await client().discussionList();
|
|
174
|
+
const deniedIds = [];
|
|
175
|
+
const allowedIds = [];
|
|
176
|
+
for (const rid of readIds) {
|
|
177
|
+
const brief = allActive.find(d => d.id === rid);
|
|
178
|
+
if (!brief) {
|
|
179
|
+
allowedIds.push(rid); // 找不到就放行(可能是历史讨论)
|
|
180
|
+
}
|
|
181
|
+
else if (isMember(brief, myPid)) {
|
|
182
|
+
allowedIds.push(rid);
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
deniedIds.push(rid);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (deniedIds.length > 0 && allowedIds.length === 0) {
|
|
189
|
+
return wrap(`🔒 无权限: 你 (${myPid}) 不是这些讨论的成员\n拒绝: ${deniedIds.join(', ')}`);
|
|
190
|
+
}
|
|
191
|
+
const readMode = decoded.mode || 'auto';
|
|
192
|
+
const discussions = await client().discussionReadByIds(allowedIds, me, readMode);
|
|
193
|
+
if (!Array.isArray(discussions) || discussions.length === 0)
|
|
194
|
+
return wrap('未找到对应的讨论记录');
|
|
195
|
+
let result = discussions.map(formatDetailView).join('\n\n━━━━━━━━━━━━━━━━━━━━\n\n');
|
|
196
|
+
if (deniedIds.length > 0) {
|
|
197
|
+
result += `\n\n🔒 已跳过 ${deniedIds.length} 个无权限的讨论: ${deniedIds.join(', ')}`;
|
|
198
|
+
}
|
|
199
|
+
return wrap(result);
|
|
200
|
+
}
|
|
201
|
+
case 'reply': {
|
|
202
|
+
if (!decoded.id)
|
|
203
|
+
return wrap('❌ reply 需要 id');
|
|
204
|
+
if (!decoded.content)
|
|
205
|
+
return wrap('❌ reply 需要 content');
|
|
206
|
+
// 权限检查: 仅成员可回复
|
|
207
|
+
const brief = await getBrief(decoded.id);
|
|
208
|
+
if (brief && !isMember(brief, myPid)) {
|
|
209
|
+
return wrap(`🔒 无权限: 你 (${myPid}) 不是讨论 "${brief.title}" 的成员\n成员: ${brief.initiator}, ${brief.participants.join(', ')}`);
|
|
210
|
+
}
|
|
211
|
+
await client().discussionReply(decoded.id, me, decoded.content, decoded.summary, decoded.newSummary);
|
|
212
|
+
return wrap(`✅ 回复成功 (ID: ${decoded.id}, 身份: ${me})`);
|
|
213
|
+
}
|
|
214
|
+
// ============ 发起人操作 (需验证发起人身份) ============
|
|
146
215
|
case 'complete': {
|
|
147
216
|
if (!decoded.id)
|
|
148
217
|
return wrap('❌ complete 需要 id');
|
|
218
|
+
// 权限检查: 仅发起人可标记完成
|
|
219
|
+
const brief = await getBrief(decoded.id);
|
|
220
|
+
if (brief && !isInitiator(brief, me, myPid)) {
|
|
221
|
+
return wrap(`🔒 无权限: 仅发起人 (${brief.initiator}) 可标记完成\n你的身份: ${me}`);
|
|
222
|
+
}
|
|
149
223
|
await client().discussionComplete(decoded.id);
|
|
150
224
|
return wrap(`✅ 讨论已标记完成 (ID: ${decoded.id})`);
|
|
151
225
|
}
|
|
@@ -154,12 +228,27 @@ export function registerDiscussionTools(server, ctx) {
|
|
|
154
228
|
return wrap('❌ close 需要 id');
|
|
155
229
|
if (!decoded.conclusion)
|
|
156
230
|
return wrap('❌ close 需要 conclusion');
|
|
157
|
-
|
|
158
|
-
|
|
231
|
+
// 权限检查: 仅发起人可归档
|
|
232
|
+
const brief = await getBrief(decoded.id);
|
|
233
|
+
if (brief && !isInitiator(brief, me, myPid)) {
|
|
234
|
+
return wrap(`🔒 无权限: 仅发起人 (${brief.initiator}) 可归档讨论\n你的身份: ${me}`);
|
|
235
|
+
}
|
|
236
|
+
// 先追加结案总结
|
|
237
|
+
try {
|
|
238
|
+
await client().discussionReply(decoded.id, me, `📋 结案总结:\n\n${decoded.conclusion}`, '结案总结');
|
|
239
|
+
}
|
|
240
|
+
catch { /* 忽略回复失败 */ }
|
|
241
|
+
await client().discussionComplete(decoded.id);
|
|
242
|
+
return wrap(`✅ 讨论已结案 (ID: ${decoded.id})\n📋 ${decoded.conclusion}`);
|
|
159
243
|
}
|
|
160
244
|
case 'delete': {
|
|
161
245
|
if (!decoded.id)
|
|
162
246
|
return wrap('❌ delete 需要 id');
|
|
247
|
+
// 权限检查: 仅发起人可删除
|
|
248
|
+
const brief = await getBrief(decoded.id);
|
|
249
|
+
if (brief && !isInitiator(brief, me, myPid)) {
|
|
250
|
+
return wrap(`🔒 无权限: 仅发起人 (${brief.initiator}) 可删除讨论\n你的身份: ${me}`);
|
|
251
|
+
}
|
|
163
252
|
await client().discussionDelete(decoded.id);
|
|
164
253
|
return wrap(`✅ 讨论已删除 (ID: ${decoded.id})`);
|
|
165
254
|
}
|
package/dist/tools/docs.js
CHANGED
|
@@ -10,7 +10,7 @@ import { wrap, safeTool, crossPrefix, formatDocMarkdown, formatTreeText, countTr
|
|
|
10
10
|
export function registerDocTools(server, ctx) {
|
|
11
11
|
const client = () => getClient();
|
|
12
12
|
// ========== kg_doc: 文档 CRUD 统一入口 ==========
|
|
13
|
-
server.tool('kg_doc', '📄
|
|
13
|
+
server.tool('kg_doc', '📄 参考文档 — action: create(创建,建议bindTo关联节点)|read|update|delete|batch_update|copy。⚠️ 不绑定节点的文档会成为孤岛!', {
|
|
14
14
|
action: z.enum(['create', 'read', 'update', 'delete', 'batch_update', 'copy'])
|
|
15
15
|
.describe('操作类型'),
|
|
16
16
|
path: z.string().optional()
|
|
@@ -47,6 +47,10 @@ export function registerDocTools(server, ctx) {
|
|
|
47
47
|
.describe('复制源路径 (copy, 单个或数组)'),
|
|
48
48
|
targetPath: z.string().optional()
|
|
49
49
|
.describe('复制目标路径前缀 (copy)'),
|
|
50
|
+
bindTo: z.string().optional()
|
|
51
|
+
.describe('绑定到流程图节点ID (create/update, 如"n_frontend")'),
|
|
52
|
+
bindChart: z.string().optional()
|
|
53
|
+
.describe('绑定目标流程图ID (create/update, 默认"main")'),
|
|
50
54
|
}, async (args) => safeTool(async () => {
|
|
51
55
|
const d = decodeObjectStrings(args);
|
|
52
56
|
switch (d.action) {
|
|
@@ -63,7 +67,39 @@ export function registerDocTools(server, ctx) {
|
|
|
63
67
|
bugfixes: [],
|
|
64
68
|
status: d.status || '已完成'
|
|
65
69
|
});
|
|
66
|
-
|
|
70
|
+
// 绑定到流程图节点
|
|
71
|
+
let bindMsg = '';
|
|
72
|
+
if (d.bindTo) {
|
|
73
|
+
try {
|
|
74
|
+
const chartId = d.bindChart || 'main';
|
|
75
|
+
const chart = await client().getFlowchart(chartId);
|
|
76
|
+
if (chart) {
|
|
77
|
+
const node = chart.nodes?.find((n) => n.id === d.bindTo);
|
|
78
|
+
if (node) {
|
|
79
|
+
if (!node.boundDocs)
|
|
80
|
+
node.boundDocs = [];
|
|
81
|
+
if (!node.boundDocs.includes(d.path)) {
|
|
82
|
+
node.boundDocs.push(d.path);
|
|
83
|
+
}
|
|
84
|
+
// Save back via PUT (use the existing save method)
|
|
85
|
+
chart.id = chartId;
|
|
86
|
+
await client().saveFlowchart(chartId, chart);
|
|
87
|
+
bindMsg = `\n🔗 已绑定到节点: ${d.bindTo} [${chartId}]`;
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
bindMsg = `\n⚠️ 节点 "${d.bindTo}" 不存在于流程图 "${chartId}"`;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
bindMsg = `\n⚠️ 绑定失败(流程图操作异常)`;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// 约束: 未绑定流程图节点 → 强制警告
|
|
99
|
+
if (!d.bindTo) {
|
|
100
|
+
bindMsg = `\n\n⚠️ 【未关联知识锚点】文档未绑定到流程图节点!\n💡 建议: 使用 kg_flowchart(bind, nodeId, docs:["${d.path}"]) 绑定\n📌 孤立文档无法被 AI 通过节点关联发现`;
|
|
101
|
+
}
|
|
102
|
+
return wrap(`✅ 文档已创建: ${d.path}${bindMsg}`);
|
|
67
103
|
}
|
|
68
104
|
// ---- READ ----
|
|
69
105
|
case 'read': {
|
|
@@ -109,7 +145,9 @@ export function registerDocTools(server, ctx) {
|
|
|
109
145
|
if (d.bugfixes !== undefined)
|
|
110
146
|
updates.bugfixes = d.bugfixes;
|
|
111
147
|
const doc = await client().updateDoc(d.path, updates);
|
|
112
|
-
|
|
148
|
+
if (!doc)
|
|
149
|
+
return wrap('更新失败');
|
|
150
|
+
return wrap(`✅ 文档已更新: ${d.path}\n状态: ${doc.status || '已完成'}\n摘要: ${doc.summary || ''}`);
|
|
113
151
|
}
|
|
114
152
|
// ---- DELETE ----
|
|
115
153
|
case 'delete': {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🔀 kg_flowchart — 逻辑流程图批量操作
|
|
3
|
+
* AI 一次提交所有节点+边, 后端原子处理, 返回孤立检测结果
|
|
4
|
+
*/
|
|
5
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
6
|
+
import { type McpContext } from './shared.js';
|
|
7
|
+
export declare function registerFlowchartTools(server: McpServer, ctx: McpContext): void;
|