@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.
@@ -1,8 +1,14 @@
1
1
  /**
2
- * 💬 kg_discuss (6→1)
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
- lines.push(`| ${d.id} | ${d.title} | ${d.initiator} | ${others} | ${d.summary} | ${d.status} | ${relativeTime(d.updatedAt)} |`);
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', '💬 跨项目讨论 — 发起、回复、归档协同讨论。action: list(列出活跃讨论)|read(读取详情,自动追踪已读)|create(发起)|reply(回复)|complete(标记完成)|close(结案归档)|delete(删除)|history(查看历史讨论)', {
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
- const result = await client().discussionClose(decoded.id, decoded.conclusion);
158
- return wrap(`✅ 讨论已结案并归档: \`${result.archived_path}\``);
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
  }
@@ -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', '📄 知识文档操作创建、读取、更新、删除、复制文档。action: create(创建)|read(读取)|update(更新)|delete(删除)|batch_update(批量更新)|copy(跨项目复制)', {
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
- return wrap(`✅ 文档已创建: ${d.path}\n\n${JSON.stringify(doc, null, 2)}`);
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
- return wrap(doc ? JSON.stringify(doc, null, 2) : '更新失败');
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;