@ppdocs/mcp 3.2.11 → 3.2.12
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/storage/httpClient.d.ts +1 -1
- package/dist/storage/httpClient.js +1 -1
- package/dist/sync/beacon.js +2 -2
- package/dist/tools/analyzer.js +127 -0
- package/dist/tools/index.d.ts +2 -2
- package/dist/tools/index.js +2 -2
- package/dist/web/server.js +48 -6
- package/dist/web/ui.js +127 -13
- package/package.json +1 -1
|
@@ -84,7 +84,7 @@ export declare class PpdocsApiClient {
|
|
|
84
84
|
/** 清空项目文件存储区 */
|
|
85
85
|
clearFiles(): Promise<void>;
|
|
86
86
|
/** 上传已打包好的 zip 数据到本项目 files/ (供 Code Beacon 使用) */
|
|
87
|
-
uploadRawZip(zipData: Buffer, remoteDir?: string): Promise<{
|
|
87
|
+
uploadRawZip(zipData: Buffer | Uint8Array | any, remoteDir?: string): Promise<{
|
|
88
88
|
fileCount: number;
|
|
89
89
|
}>;
|
|
90
90
|
/** 跨项目: 列出文件 */
|
|
@@ -457,7 +457,7 @@ export class PpdocsApiClient {
|
|
|
457
457
|
const response = await fetch(`${this.baseUrl}/files${query}`, {
|
|
458
458
|
method: 'POST',
|
|
459
459
|
headers: { 'Content-Type': 'application/octet-stream' },
|
|
460
|
-
body: new Uint8Array(zipData),
|
|
460
|
+
body: Buffer.isBuffer(zipData) ? new Uint8Array(zipData) : zipData,
|
|
461
461
|
});
|
|
462
462
|
if (!response.ok) {
|
|
463
463
|
const error = await response.json().catch(() => ({ error: response.statusText }));
|
package/dist/sync/beacon.js
CHANGED
|
@@ -141,7 +141,7 @@ export class SyncBeacon {
|
|
|
141
141
|
});
|
|
142
142
|
output.on('close', async () => {
|
|
143
143
|
try {
|
|
144
|
-
const data = fs.
|
|
144
|
+
const data = await fs.promises.readFile(tmpFile);
|
|
145
145
|
// [C2 修复] 使用本项目标准上传接口,而非 crossUploadFiles
|
|
146
146
|
await client.uploadRawZip(data);
|
|
147
147
|
console.error(`[Code Beacon] Snapshot synced: ${archive.pointer()} bytes`);
|
|
@@ -152,7 +152,7 @@ export class SyncBeacon {
|
|
|
152
152
|
finally {
|
|
153
153
|
try {
|
|
154
154
|
if (fs.existsSync(tmpFile))
|
|
155
|
-
fs.
|
|
155
|
+
fs.promises.unlink(tmpFile);
|
|
156
156
|
}
|
|
157
157
|
catch { }
|
|
158
158
|
resolve();
|
package/dist/tools/analyzer.js
CHANGED
|
@@ -133,4 +133,131 @@ export function registerAnalyzerTools(server, projectId) {
|
|
|
133
133
|
}
|
|
134
134
|
return wrap(lines.join('\n'));
|
|
135
135
|
}));
|
|
136
|
+
// ===== code_path: 两点间链路追踪 =====
|
|
137
|
+
server.tool('code_path', '🔗 两点链路追踪 — 给定两个符号(如 fileA.funcA 和 fileB.funcB),自动搜索中间的引用链路,返回完整路径 + 绑定的知识图谱文档', {
|
|
138
|
+
projectPath: z.string().describe('项目源码的绝对路径'),
|
|
139
|
+
symbolA: z.string().describe('起点符号名(如 "handleLogin")'),
|
|
140
|
+
symbolB: z.string().describe('终点符号名(如 "AuthService")'),
|
|
141
|
+
maxDepth: z.number().optional().describe('最大搜索深度(1-5, 默认3)'),
|
|
142
|
+
}, async (args) => safeTool(async () => {
|
|
143
|
+
const depth = Math.min(5, Math.max(1, args.maxDepth ?? 3));
|
|
144
|
+
// 1. 获取两个符号的影响树
|
|
145
|
+
const [treeA, treeB] = await Promise.all([
|
|
146
|
+
client().analyzerImpactTree(args.projectPath, args.symbolA, depth),
|
|
147
|
+
client().analyzerImpactTree(args.projectPath, args.symbolB, depth),
|
|
148
|
+
]);
|
|
149
|
+
if (!treeA)
|
|
150
|
+
return wrap(`❌ 未找到起点符号 "${args.symbolA}"。请确认名称正确且已运行 code_scan`);
|
|
151
|
+
if (!treeB)
|
|
152
|
+
return wrap(`❌ 未找到终点符号 "${args.symbolB}"。请确认名称正确且已运行 code_scan`);
|
|
153
|
+
const graphA = new Map(); // 从 A 出发可达的文件
|
|
154
|
+
const graphB = new Map(); // 从 B 出发可达的文件
|
|
155
|
+
const buildGraph = (tree, graph) => {
|
|
156
|
+
graph.set(tree.definedIn, []);
|
|
157
|
+
for (const level of tree.levels) {
|
|
158
|
+
for (const entry of level.entries) {
|
|
159
|
+
if (!graph.has(entry.filePath))
|
|
160
|
+
graph.set(entry.filePath, []);
|
|
161
|
+
graph.get(entry.filePath).push({
|
|
162
|
+
file: entry.filePath,
|
|
163
|
+
line: entry.line,
|
|
164
|
+
depth: level.depth,
|
|
165
|
+
from: tree.symbolName,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
buildGraph(treeA, graphA);
|
|
171
|
+
buildGraph(treeB, graphB);
|
|
172
|
+
// 3. 查找交集: A 和 B 都能到达的文件
|
|
173
|
+
const intersection = [];
|
|
174
|
+
for (const [fileA] of graphA) {
|
|
175
|
+
if (graphB.has(fileA)) {
|
|
176
|
+
const refsA = graphA.get(fileA);
|
|
177
|
+
const refsB = graphB.get(fileA);
|
|
178
|
+
const minDepthA = refsA.length > 0 ? Math.min(...refsA.map(r => r.depth)) : 0;
|
|
179
|
+
const minDepthB = refsB.length > 0 ? Math.min(...refsB.map(r => r.depth)) : 0;
|
|
180
|
+
intersection.push({ file: fileA, depthFromA: minDepthA, depthFromB: minDepthB });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// 4. 尝试获取 KG 文档绑定
|
|
184
|
+
let docBindings = {};
|
|
185
|
+
try {
|
|
186
|
+
const docClient = client();
|
|
187
|
+
const allDocs = await docClient.listDocs();
|
|
188
|
+
for (const doc of allDocs) {
|
|
189
|
+
if (!doc.isDir && doc.summary) {
|
|
190
|
+
docBindings[doc.name] = doc.summary;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
catch { /* KG 不可用时忽略 */ }
|
|
195
|
+
// 5. 格式化输出
|
|
196
|
+
const lines = [
|
|
197
|
+
`🔗 两点链路追踪`,
|
|
198
|
+
``,
|
|
199
|
+
`📍 起点: ${treeA.symbolName} (${treeA.symbolKind}) → ${treeA.definedIn}:${treeA.lineStart}`,
|
|
200
|
+
`📍 终点: ${treeB.symbolName} (${treeB.symbolKind}) → ${treeB.definedIn}:${treeB.lineStart}`,
|
|
201
|
+
``,
|
|
202
|
+
];
|
|
203
|
+
// 直接引用关系
|
|
204
|
+
if (treeA.definedIn === treeB.definedIn) {
|
|
205
|
+
lines.push(`✅ 两个符号定义在同一文件中!`);
|
|
206
|
+
lines.push('');
|
|
207
|
+
}
|
|
208
|
+
// 检查 A 的影响树是否直接包含 B 的文件
|
|
209
|
+
const directAtoB = graphA.has(treeB.definedIn);
|
|
210
|
+
const directBtoA = graphB.has(treeA.definedIn);
|
|
211
|
+
if (directAtoB || directBtoA) {
|
|
212
|
+
lines.push(`### ✅ 发现直接链路`);
|
|
213
|
+
if (directAtoB) {
|
|
214
|
+
const refs = graphA.get(treeB.definedIn);
|
|
215
|
+
lines.push(` ${treeA.symbolName} → [L${refs[0]?.depth || 1}] → ${treeB.definedIn} (含 ${treeB.symbolName})`);
|
|
216
|
+
}
|
|
217
|
+
if (directBtoA) {
|
|
218
|
+
const refs = graphB.get(treeA.definedIn);
|
|
219
|
+
lines.push(` ${treeB.symbolName} → [L${refs[0]?.depth || 1}] → ${treeA.definedIn} (含 ${treeA.symbolName})`);
|
|
220
|
+
}
|
|
221
|
+
lines.push('');
|
|
222
|
+
}
|
|
223
|
+
if (intersection.length > 0) {
|
|
224
|
+
// 按总深度排序
|
|
225
|
+
intersection.sort((a, b) => (a.depthFromA + a.depthFromB) - (b.depthFromA + b.depthFromB));
|
|
226
|
+
lines.push(`### 🔗 共经文件 (${intersection.length}个)`);
|
|
227
|
+
lines.push(`| 文件 | 距${treeA.symbolName} | 距${treeB.symbolName} | 📚 绑定文档 |`);
|
|
228
|
+
lines.push(`|:---|:---:|:---:|:---|`);
|
|
229
|
+
for (const node of intersection.slice(0, 20)) {
|
|
230
|
+
const fileName = node.file.split('/').pop() || node.file;
|
|
231
|
+
const docBind = docBindings[fileName] || '—';
|
|
232
|
+
lines.push(`| ${node.file} | L${node.depthFromA} | L${node.depthFromB} | ${docBind} |`);
|
|
233
|
+
}
|
|
234
|
+
if (intersection.length > 20) {
|
|
235
|
+
lines.push(`| ... | | | (还有 ${intersection.length - 20} 个) |`);
|
|
236
|
+
}
|
|
237
|
+
lines.push('');
|
|
238
|
+
}
|
|
239
|
+
else if (!directAtoB && !directBtoA) {
|
|
240
|
+
lines.push(`⚠️ 在 ${depth} 层深度内未发现 ${treeA.symbolName} 与 ${treeB.symbolName} 的引用链路`);
|
|
241
|
+
lines.push(`建议: 增加 maxDepth 参数(当前=${depth}, 最大=5) 重试`);
|
|
242
|
+
lines.push('');
|
|
243
|
+
}
|
|
244
|
+
// 附加: 各符号的影响概要
|
|
245
|
+
lines.push(`### 📊 影响概要`);
|
|
246
|
+
lines.push(`- ${treeA.symbolName}: ${treeA.summary}`);
|
|
247
|
+
lines.push(`- ${treeB.symbolName}: ${treeB.summary}`);
|
|
248
|
+
// 附加: 绑定的 KG 文档
|
|
249
|
+
const relevantDocs = [];
|
|
250
|
+
const symbolNames = [treeA.symbolName, treeB.symbolName];
|
|
251
|
+
for (const [docName, docSummary] of Object.entries(docBindings)) {
|
|
252
|
+
if (symbolNames.some(s => docName.toLowerCase().includes(s.toLowerCase()) || s.toLowerCase().includes(docName.toLowerCase()))) {
|
|
253
|
+
relevantDocs.push(` 📚 ${docName}: ${docSummary}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (relevantDocs.length > 0) {
|
|
257
|
+
lines.push('');
|
|
258
|
+
lines.push(`### 📚 相关知识图谱文档`);
|
|
259
|
+
lines.push(...relevantDocs);
|
|
260
|
+
}
|
|
261
|
+
return wrap(lines.join('\n'));
|
|
262
|
+
}));
|
|
136
263
|
}
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* MCP 工具注册入口
|
|
3
|
-
* 精简后:
|
|
3
|
+
* 精简后: 14 个工具, 6 个子模块
|
|
4
4
|
*
|
|
5
5
|
* 🔗 初始化: kg_init (1个)
|
|
6
6
|
* 📊 导航: kg_status, kg_tree (2个)
|
|
7
7
|
* 📚 知识: kg_doc, kg_projects, kg_rules (3个)
|
|
8
8
|
* 📝 工作流: kg_task, kg_files, kg_discuss (3个)
|
|
9
|
-
* 🔬 代码分析: code_scan, code_query, code_impact, code_context (
|
|
9
|
+
* 🔬 代码分析: code_scan, code_query, code_impact, code_context, code_path (5个)
|
|
10
10
|
*/
|
|
11
11
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
12
12
|
export declare function registerTools(server: McpServer, projectId: string, user: string, onProjectChange?: (newProjectId: string, newApiUrl: string) => void): void;
|
package/dist/tools/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* MCP 工具注册入口
|
|
3
|
-
* 精简后:
|
|
3
|
+
* 精简后: 14 个工具, 6 个子模块
|
|
4
4
|
*
|
|
5
5
|
* 🔗 初始化: kg_init (1个)
|
|
6
6
|
* 📊 导航: kg_status, kg_tree (2个)
|
|
7
7
|
* 📚 知识: kg_doc, kg_projects, kg_rules (3个)
|
|
8
8
|
* 📝 工作流: kg_task, kg_files, kg_discuss (3个)
|
|
9
|
-
* 🔬 代码分析: code_scan, code_query, code_impact, code_context (
|
|
9
|
+
* 🔬 代码分析: code_scan, code_query, code_impact, code_context, code_path (5个)
|
|
10
10
|
*/
|
|
11
11
|
import { createContext } from './shared.js';
|
|
12
12
|
import { registerInitTool } from './init.js';
|
package/dist/web/server.js
CHANGED
|
@@ -333,11 +333,11 @@ export function startWebServer(webPort) {
|
|
|
333
333
|
const json = await resp.json();
|
|
334
334
|
const docs = json.data || [];
|
|
335
335
|
if (docs.length === 0) {
|
|
336
|
-
res.json({ tree: '知识库为空', docCount: 0, dirCount: 0, readOnly });
|
|
336
|
+
res.json({ tree: '知识库为空', docs: [], docCount: 0, dirCount: 0, readOnly });
|
|
337
337
|
return;
|
|
338
338
|
}
|
|
339
|
-
const
|
|
340
|
-
|
|
339
|
+
const sortedDocs = docs.sort((a, b) => a.path.localeCompare(b.path));
|
|
340
|
+
const lines = sortedDocs
|
|
341
341
|
.map(d => {
|
|
342
342
|
const depth = (d.path.match(/\//g) || []).length;
|
|
343
343
|
const indent = ' '.repeat(Math.max(0, depth - 1));
|
|
@@ -345,14 +345,56 @@ export function startWebServer(webPort) {
|
|
|
345
345
|
const summary = d.summary ? ` — ${d.summary}` : '';
|
|
346
346
|
return `${indent}${icon} ${d.name}${summary}`;
|
|
347
347
|
});
|
|
348
|
-
const docCount =
|
|
349
|
-
const dirCount =
|
|
350
|
-
res.json({ tree: lines.join('\n'), docCount, dirCount, readOnly });
|
|
348
|
+
const docCount = sortedDocs.filter(d => !d.isDir).length;
|
|
349
|
+
const dirCount = sortedDocs.filter(d => d.isDir).length;
|
|
350
|
+
res.json({ tree: lines.join('\n'), docs: sortedDocs, docCount, dirCount, readOnly });
|
|
351
351
|
}
|
|
352
352
|
catch (e) {
|
|
353
353
|
res.json({ tree: `连接失败: ${String(e).slice(0, 100)}` });
|
|
354
354
|
}
|
|
355
355
|
});
|
|
356
|
+
// GET /api/bind/:id/doc → 读取单篇文档内容
|
|
357
|
+
app.get('/api/bind/:id/doc', async (req, res) => {
|
|
358
|
+
const remoteId = req.params.id;
|
|
359
|
+
const docPath = req.query.path;
|
|
360
|
+
if (!docPath) {
|
|
361
|
+
res.json({ ok: false, error: '缺少 path 参数' });
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
const config = loadAgentConfig();
|
|
365
|
+
const proj = config?.projects.find(p => p.remote.id === remoteId);
|
|
366
|
+
if (!config || !proj) {
|
|
367
|
+
res.json({ ok: false, error: '项目不存在' });
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
let docUrl;
|
|
371
|
+
if (proj.remote.password) {
|
|
372
|
+
docUrl = `http://${config.host}:${config.port}/api/${proj.remote.id}/${proj.remote.password}/docs/${encodeURIComponent(docPath)}`;
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
const proxy = config.projects.find(p => p.remote.id !== remoteId && p.remote.password);
|
|
376
|
+
if (proxy) {
|
|
377
|
+
docUrl = `http://${config.host}:${config.port}/api/${proxy.remote.id}/${proxy.remote.password}/cross/${remoteId}/docs/${encodeURIComponent(docPath)}`;
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
res.json({ ok: false, error: '未授权' });
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
try {
|
|
385
|
+
const resp = await fetch(docUrl);
|
|
386
|
+
if (!resp.ok) {
|
|
387
|
+
res.json({ ok: false, error: `HTTP ${resp.status}` });
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
const json = await resp.json();
|
|
391
|
+
const d = json.data;
|
|
392
|
+
res.json({ ok: true, content: d?.content || '', summary: d?.summary || '', status: d?.status || '' });
|
|
393
|
+
}
|
|
394
|
+
catch (e) {
|
|
395
|
+
res.json({ ok: false, error: String(e).slice(0, 100) });
|
|
396
|
+
}
|
|
397
|
+
});
|
|
356
398
|
// GET /api/bind/:id/prompts → 读取项目提示词文件
|
|
357
399
|
app.get('/api/bind/:id/prompts', (req, res) => {
|
|
358
400
|
const config = loadAgentConfig();
|
package/dist/web/ui.js
CHANGED
|
@@ -41,7 +41,25 @@ label{font-size:12px;color:#64748b;font-weight:500;display:block;margin-bottom:4
|
|
|
41
41
|
.fg{margin-bottom:14px}
|
|
42
42
|
.section{margin-bottom:20px}
|
|
43
43
|
.section h2{font-size:14px;font-weight:600;color:#94a3b8;margin-bottom:12px;display:flex;align-items:center;gap:6px}
|
|
44
|
-
.tree-box{background:rgba(15,23,42,.8);border:1px solid rgba(100,116,139,.2);border-radius:10px;padding:14px;font-
|
|
44
|
+
.tree-box{background:rgba(15,23,42,.8);border:1px solid rgba(100,116,139,.2);border-radius:10px;padding:14px;font-size:13px;color:#94a3b8;max-height:400px;overflow-y:auto;line-height:1.4}
|
|
45
|
+
.kg-search{width:100%;padding:8px 12px;margin-bottom:10px;background:rgba(15,23,42,.6);border:1px solid rgba(100,116,139,.2);border-radius:8px;color:#e0e6ed;font-size:13px;outline:none}
|
|
46
|
+
.kg-search:focus{border-color:#60a5fa}
|
|
47
|
+
.kg-stats{display:flex;gap:12px;font-size:12px;color:#64748b;margin-bottom:10px;padding:6px 10px;background:rgba(96,165,250,.06);border-radius:6px}
|
|
48
|
+
.kg-stats span{display:inline-flex;align-items:center;gap:3px}
|
|
49
|
+
.kg-node{padding:5px 8px;border-radius:6px;cursor:pointer;transition:all .15s;display:flex;align-items:center;gap:6px;margin:1px 0}
|
|
50
|
+
.kg-node:hover{background:rgba(96,165,250,.1)}
|
|
51
|
+
.kg-node .icon{font-size:14px;flex-shrink:0;width:18px;text-align:center}
|
|
52
|
+
.kg-node .name{font-weight:500;color:#cbd5e1;font-size:13px;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
53
|
+
.kg-node .summary{font-size:11px;color:#64748b;margin-left:6px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:200px}
|
|
54
|
+
.kg-node.dir .name{color:#60a5fa;font-weight:600}
|
|
55
|
+
.kg-node .arrow{font-size:10px;color:#475569;transition:transform .2s;width:14px;text-align:center;flex-shrink:0}
|
|
56
|
+
.kg-node .arrow.open{transform:rotate(90deg)}
|
|
57
|
+
.kg-children{margin-left:16px;border-left:1px solid rgba(100,116,139,.12);padding-left:4px}
|
|
58
|
+
.kg-children.collapsed{display:none}
|
|
59
|
+
.kg-empty{text-align:center;padding:24px;color:#475569;font-size:13px}
|
|
60
|
+
.doc-modal-content{background:rgba(15,23,42,.9);border:1px solid rgba(100,116,139,.2);border-radius:8px;padding:14px;font-family:'Cascadia Code','Fira Code',monospace;font-size:12px;color:#94a3b8;max-height:50vh;overflow-y:auto;white-space:pre-wrap;line-height:1.6}
|
|
61
|
+
.doc-modal-meta{display:flex;gap:12px;margin-bottom:12px;font-size:12px;color:#64748b}
|
|
62
|
+
.doc-modal-meta span{padding:3px 8px;border-radius:6px;background:rgba(100,116,139,.1)}
|
|
45
63
|
.platform-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px}
|
|
46
64
|
.platform-card{padding:14px;border-radius:10px;border:1px solid rgba(100,116,139,.2);text-align:center;transition:all .2s}
|
|
47
65
|
.platform-card:hover{border-color:#a78bfa;background:rgba(167,139,250,.08)}
|
|
@@ -80,7 +98,7 @@ label{font-size:12px;color:#64748b;font-weight:500;display:block;margin-bottom:4
|
|
|
80
98
|
<div class="toast" id="toast"></div>
|
|
81
99
|
<script>
|
|
82
100
|
// ============ State ============
|
|
83
|
-
var S={host:'',port:20001,hostOk:false,projects:[],page:'list',addStep:1,addDir:'',detailIdx:-1,detailTab:'overview',mcpStatus:{},mcpAll:[],prompts:[],authRequestId:null,authStartTime:0,authStatus:'',authResult:null,modal:null};
|
|
101
|
+
var S={host:'',port:20001,hostOk:false,projects:[],page:'list',addStep:1,addDir:'',detailIdx:-1,detailTab:'overview',mcpStatus:{},mcpAll:[],prompts:[],authRequestId:null,authStartTime:0,authStatus:'',authResult:null,modal:null,kgDocs:[],kgDocCount:0,kgDirCount:0,kgReadOnly:false,kgLoaded:false,kgFilter:'',kgCollapsed:{}};
|
|
84
102
|
|
|
85
103
|
// ============ Router ============
|
|
86
104
|
function go(page,data){Object.assign(S,data||{});S.page=page;_lastDetailRid=null;cancelAuth();render()}
|
|
@@ -108,8 +126,7 @@ function init(){
|
|
|
108
126
|
var entry=items[i].webkitGetAsEntry&&items[i].webkitGetAsEntry();
|
|
109
127
|
if(entry&&entry.isDirectory){
|
|
110
128
|
go('add',{addStep:1,addDir:entry.name});
|
|
111
|
-
toast('
|
|
112
|
-
setTimeout(function(){var inp=document.getElementById('iDir');if(inp){inp.focus();inp.setSelectionRange(0,0)}},150);
|
|
129
|
+
toast('请补全绝对路径,例如 D:/projects/'+entry.name);
|
|
113
130
|
return;
|
|
114
131
|
}
|
|
115
132
|
}
|
|
@@ -201,7 +218,8 @@ function renderAddStep1(){
|
|
|
201
218
|
|
|
202
219
|
function doAuthStart(){
|
|
203
220
|
var dir=document.getElementById('iDir').value.trim();
|
|
204
|
-
if(dir
|
|
221
|
+
if(!dir){toast('请填写目录路径','err');return}
|
|
222
|
+
if(!/^[A-Za-z]:[\\\\\\/]|^\\//.test(dir)){toast('请输入绝对路径','err');return}
|
|
205
223
|
S.addDir=dir;
|
|
206
224
|
api('/api/auth/start',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({localDir:dir})}).then(function(r){
|
|
207
225
|
if(!r.ok){toast(r.error||'授权请求失败','err');return}
|
|
@@ -296,15 +314,84 @@ function renderDetail(){
|
|
|
296
314
|
function renderOverview(p){
|
|
297
315
|
var authBadge=p.hasPassword?'<span class="auth-badge ok">✅ 已授权 (读写)</span>':'<span class="auth-badge no">⚠️ 未授权 (只读)</span>';
|
|
298
316
|
var authBtn=p.hasPassword?'':'<button class="btn btn-p" style="font-size:12px;padding:5px 12px;margin-left:8px" onclick="doReAuth(\\x27'+p.remoteId+'\\x27,\\x27'+(p.localDir||'').replace(/\\\\/g,'/')+'\\x27)">📡 申请读写授权</button>';
|
|
299
|
-
|
|
317
|
+
var overviewCard='<div class="card no-hover"><div class="section"><h2>📊 概览</h2>'+
|
|
300
318
|
'<div class="stat"><span>本地: '+(p.localDir||'<em style="color:#ef4444">未指定</em>')+'</span></div>'+
|
|
301
319
|
'<div class="stat"><span>远程: '+p.remoteName+' ('+p.remoteId+')</span></div>'+
|
|
302
320
|
'<div class="stat"><span>授权: '+authBadge+authBtn+'</span></div>'+
|
|
303
321
|
'<div class="stat"><span>文档: '+(p.docCount||0)+' 个</span></div>'+
|
|
304
322
|
'<div class="stat"><span>同步: '+(p.connected?'🟢 运行中':'🔴 '+p.syncStatus)+' '+(p.lastSync?'('+p.lastSync+')':'')+'</span></div>'+
|
|
305
|
-
'</div></div>'
|
|
306
|
-
|
|
307
|
-
|
|
323
|
+
'</div></div>';
|
|
324
|
+
// ---- 知识图谱树 ----
|
|
325
|
+
var kgCard='';
|
|
326
|
+
if(!S.kgLoaded){
|
|
327
|
+
kgCard='<div class="card no-hover"><div class="section"><h2>📚 知识图谱</h2><div class="kg-empty" style="animation:pulse 1.5s infinite">⏳ 加载中...</div></div></div>';
|
|
328
|
+
}else if(!S.kgDocs||S.kgDocs.length===0){
|
|
329
|
+
kgCard='<div class="card no-hover"><div class="section"><h2>📚 知识图谱</h2><div class="kg-empty">📭 知识库为空</div></div></div>';
|
|
330
|
+
}else{
|
|
331
|
+
var readOnlyBadge=S.kgReadOnly?'<span style="font-size:11px;color:#f59e0b;margin-left:8px">🔒 只读</span>':'';
|
|
332
|
+
var statsHtml='<div class="kg-stats"><span>📄 '+S.kgDocCount+' 文档</span><span>📁 '+S.kgDirCount+' 目录</span></div>';
|
|
333
|
+
var searchHtml='<input class="kg-search" placeholder="搜索文档..." value="'+S.kgFilter+'" oninput="S.kgFilter=this.value;_render()">';
|
|
334
|
+
var treeHtml=renderKgTree(S.kgDocs,p.remoteId,'',0);
|
|
335
|
+
kgCard='<div class="card no-hover"><div class="section"><h2>📚 知识图谱'+readOnlyBadge+'</h2>'+statsHtml+'<div class="tree-box">'+searchHtml+treeHtml+'</div></div></div>';
|
|
336
|
+
}
|
|
337
|
+
return overviewCard+kgCard;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function esc(s){return s?s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'):''}
|
|
341
|
+
function renderKgTree(docs,rid,parentPath,depth){
|
|
342
|
+
var filter=S.kgFilter?S.kgFilter.toLowerCase():'';
|
|
343
|
+
// 获取当前层级的直接子节点
|
|
344
|
+
var children=docs.filter(function(d){
|
|
345
|
+
// 直接子级: 路径以 parentPath 开头, 且去掉 parentPath 后只剩一级
|
|
346
|
+
if(parentPath===''){
|
|
347
|
+
var parts=d.path.split('/').filter(Boolean);
|
|
348
|
+
return parts.length===1;
|
|
349
|
+
}
|
|
350
|
+
if(d.path.indexOf(parentPath+'/')!==0) return false;
|
|
351
|
+
var rest=d.path.slice(parentPath.length+1);
|
|
352
|
+
return rest.indexOf('/')===-1;
|
|
353
|
+
});
|
|
354
|
+
if(filter){
|
|
355
|
+
children=children.filter(function(d){
|
|
356
|
+
if(d.name.toLowerCase().indexOf(filter)>=0) return true;
|
|
357
|
+
if(d.summary&&d.summary.toLowerCase().indexOf(filter)>=0) return true;
|
|
358
|
+
// 目录: 检查子孙是否匹配
|
|
359
|
+
if(d.isDir) return docs.some(function(dd){ return dd.path.indexOf(d.path+'/')===0&&(dd.name.toLowerCase().indexOf(filter)>=0||(dd.summary&&dd.summary.toLowerCase().indexOf(filter)>=0)); });
|
|
360
|
+
return false;
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
if(children.length===0) return depth===0?'<div class="kg-empty">无匹配结果</div>':'';
|
|
364
|
+
var html='';
|
|
365
|
+
children.sort(function(a,b){ if(a.isDir!==b.isDir) return a.isDir?-1:1; return a.name.localeCompare(b.name); });
|
|
366
|
+
children.forEach(function(d){
|
|
367
|
+
var collapsed=S.kgCollapsed[d.path];
|
|
368
|
+
if(d.isDir){
|
|
369
|
+
var arrowCls=collapsed?'arrow':'arrow open';
|
|
370
|
+
// 计算子文档数
|
|
371
|
+
var subCount=docs.filter(function(dd){return !dd.isDir&&dd.path.indexOf(d.path+'/')===0}).length;
|
|
372
|
+
var badge=subCount>0?'<span style="font-size:10px;color:#475569;margin-left:4px">('+subCount+')</span>':'';
|
|
373
|
+
html+='<div class="kg-node dir" onclick="toggleKgDir(\\x27'+d.path.replace(/'/g,"\\x27")+'\\x27)">';
|
|
374
|
+
html+='<span class="'+arrowCls+'">▶</span>';
|
|
375
|
+
html+='<span class="icon">📁</span>';
|
|
376
|
+
html+='<span class="name">'+esc(d.name)+badge+'</span>';
|
|
377
|
+
html+='</div>';
|
|
378
|
+
var childHtml=renderKgTree(docs,rid,d.path,depth+1);
|
|
379
|
+
html+='<div class="kg-children'+(collapsed?' collapsed':'')+'">'+(childHtml||'')+'</div>';
|
|
380
|
+
}else{
|
|
381
|
+
var sum=d.summary?'<span class="summary">'+esc(d.summary)+'</span>':'';
|
|
382
|
+
html+='<div class="kg-node" onclick="doViewDoc(\\x27'+rid+'\\x27,\\x27'+d.path.replace(/'/g,"\\x27")+'\\x27)">';
|
|
383
|
+
html+='<span class="arrow" style="visibility:hidden">▶</span>';
|
|
384
|
+
html+='<span class="icon">📄</span>';
|
|
385
|
+
html+='<span class="name">'+esc(d.name)+'</span>'+sum;
|
|
386
|
+
html+='</div>';
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
return html;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function toggleKgDir(path){
|
|
393
|
+
S.kgCollapsed[path]=!S.kgCollapsed[path];
|
|
394
|
+
_render();
|
|
308
395
|
}
|
|
309
396
|
|
|
310
397
|
function renderMcpTab(p){
|
|
@@ -359,13 +446,40 @@ function renderPromptsTab(p){
|
|
|
359
446
|
return '<div class="card no-hover"><div class="section"><h2>📝 系统提示词</h2>'+(cards||'<div style="font-size:12px;color:#475569">加载中...</div>')+'</div></div>';
|
|
360
447
|
}
|
|
361
448
|
|
|
449
|
+
// ============ Doc Viewer ============
|
|
450
|
+
function doViewDoc(rid,docPath){
|
|
451
|
+
api('/api/bind/'+rid+'/doc?path='+encodeURIComponent(docPath)).then(function(r){
|
|
452
|
+
if(!r.ok){toast(r.error||'读取失败','err');return}
|
|
453
|
+
var name=esc(docPath.split('/').pop()||docPath);
|
|
454
|
+
var meta='';
|
|
455
|
+
if(r.summary||r.status){
|
|
456
|
+
meta='<div class="doc-modal-meta">';
|
|
457
|
+
if(r.status) meta+='<span>状态: '+r.status+'</span>';
|
|
458
|
+
if(r.summary) meta+='<span>'+r.summary+'</span>';
|
|
459
|
+
meta+='</div>';
|
|
460
|
+
}
|
|
461
|
+
var escaped=(r.content||'暂无内容').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
462
|
+
var div=document.createElement('div');div.className='modal-bg';
|
|
463
|
+
div.innerHTML='<div class="modal" style="max-width:650px"><h3>📄 '+name+'</h3>'+
|
|
464
|
+
'<div style="font-size:11px;color:#475569;margin:-12px 0 12px">'+docPath+'</div>'+
|
|
465
|
+
meta+
|
|
466
|
+
'<div class="doc-modal-content">'+escaped+'</div>'+
|
|
467
|
+
'<div class="btn-row" style="margin-top:12px"><button class="btn btn-s" onclick="this.closest(\\x27.modal-bg\\x27).remove()">关闭</button></div></div>';
|
|
468
|
+
document.body.appendChild(div);
|
|
469
|
+
div.addEventListener('click',function(e){if(e.target===div)div.remove()});
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
|
|
362
473
|
// ============ Data Loading ============
|
|
363
474
|
function loadTree(rid){
|
|
364
|
-
|
|
475
|
+
S.kgLoaded=false;
|
|
365
476
|
return api('/api/bind/'+rid+'/tree').then(function(r){
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
477
|
+
S.kgDocs=r.docs||[];
|
|
478
|
+
S.kgDocCount=r.docCount||0;
|
|
479
|
+
S.kgDirCount=r.dirCount||0;
|
|
480
|
+
S.kgReadOnly=!!r.readOnly;
|
|
481
|
+
S.kgLoaded=true;
|
|
482
|
+
}).catch(function(e){S.kgDocs=[];S.kgLoaded=true;console.error('loadTree:',e)});
|
|
369
483
|
}
|
|
370
484
|
function loadMcpStatus(rid){return api('/api/bind/'+rid+'/mcp').then(function(r){S.mcpStatus=r.platforms||{}}).catch(function(){S.mcpStatus={}})}
|
|
371
485
|
function loadMcpAll(rid){return api('/api/bind/'+rid+'/mcp/all').then(function(r){S.mcpAll=r.servers||[]}).catch(function(){S.mcpAll=[]})}
|