@ppdocs/mcp 3.2.35 → 3.2.36
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 +4 -48
- package/dist/storage/httpClient.d.ts +2 -27
- package/dist/storage/httpClient.js +3 -132
- package/dist/storage/types.d.ts +0 -28
- package/dist/storage/types.js +1 -1
- package/dist/tools/flowchart.d.ts +1 -5
- package/dist/tools/flowchart.js +406 -454
- package/dist/tools/index.d.ts +3 -3
- package/dist/tools/index.js +10 -13
- package/dist/tools/kg_status.d.ts +1 -1
- package/dist/tools/kg_status.js +6 -20
- package/dist/tools/projects.d.ts +2 -3
- package/dist/tools/projects.js +2 -3
- package/dist/tools/shared.d.ts +0 -12
- package/dist/tools/shared.js +0 -59
- package/package.json +1 -1
- package/templates/AGENT.md +63 -38
- package/templates/cursorrules.md +63 -64
- package/templates/kiro-rules/ppdocs.md +63 -142
- package/dist/agent.d.ts +0 -6
- package/dist/agent.js +0 -130
- package/dist/tools/docs.d.ts +0 -8
- package/dist/tools/docs.js +0 -285
- package/dist/tools/helpers.d.ts +0 -37
- package/dist/tools/helpers.js +0 -94
- package/dist/vector/index.d.ts +0 -56
- package/dist/vector/index.js +0 -228
- package/dist/vector/manager.d.ts +0 -48
- package/dist/vector/manager.js +0 -250
- package/dist/web/server.d.ts +0 -43
- package/dist/web/server.js +0 -808
- package/dist/web/ui.d.ts +0 -5
- package/dist/web/ui.js +0 -642
package/dist/web/ui.js
DELETED
|
@@ -1,642 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* PPDocs Agent V3 — Multi-Page SPA UI
|
|
3
|
-
* 页面: 项目列表 / 添加项目 / 项目详情 (标签页: 概览/MCP/提示词)
|
|
4
|
-
*/
|
|
5
|
-
export function getAgentHtml() {
|
|
6
|
-
return `<!DOCTYPE html>
|
|
7
|
-
<html lang="zh-CN">
|
|
8
|
-
<head>
|
|
9
|
-
<meta charset="UTF-8">
|
|
10
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
11
|
-
<title>PPDocs Agent</title>
|
|
12
|
-
<style>
|
|
13
|
-
*{margin:0;padding:0;box-sizing:border-box}
|
|
14
|
-
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif;background:#0a0e1a;color:#e0e6ed;min-height:100vh}
|
|
15
|
-
.app{max-width:700px;margin:0 auto;padding:32px 20px}
|
|
16
|
-
h1{font-size:24px;font-weight:700;background:linear-gradient(135deg,#60a5fa,#a78bfa);-webkit-background-clip:text;-webkit-text-fill-color:transparent}
|
|
17
|
-
.header{display:flex;justify-content:space-between;align-items:center;margin-bottom:24px}
|
|
18
|
-
.host-badge{font-size:12px;padding:4px 12px;border-radius:20px;background:rgba(34,197,94,.15);color:#22c55e;border:1px solid rgba(34,197,94,.3)}
|
|
19
|
-
.host-badge.off{background:rgba(239,68,68,.15);color:#ef4444;border-color:rgba(239,68,68,.3)}
|
|
20
|
-
.card{background:rgba(30,41,59,.7);backdrop-filter:blur(12px);border:1px solid rgba(100,116,139,.2);border-radius:14px;padding:20px;margin-bottom:12px;cursor:pointer;transition:all .2s}
|
|
21
|
-
.card:hover{border-color:rgba(96,165,250,.4);transform:translateY(-1px)}
|
|
22
|
-
.card.no-hover{cursor:default;transform:none}
|
|
23
|
-
.card h3{font-size:15px;font-weight:600;margin-bottom:4px;display:flex;align-items:center;gap:8px}
|
|
24
|
-
.card .meta{font-size:12px;color:#64748b;margin-top:4px}
|
|
25
|
-
.card .stat{display:flex;gap:16px;margin-top:8px;font-size:13px}
|
|
26
|
-
.card .stat span{color:#94a3b8}
|
|
27
|
-
.sync-dot{width:8px;height:8px;border-radius:50%;display:inline-block}
|
|
28
|
-
.sync-dot.on{background:#22c55e;box-shadow:0 0 6px #22c55e}
|
|
29
|
-
.sync-dot.off{background:#64748b}
|
|
30
|
-
.btn{padding:8px 16px;border:none;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;transition:all .2s;display:inline-flex;align-items:center;gap:5px}
|
|
31
|
-
.btn-p{background:linear-gradient(135deg,#3b82f6,#6366f1);color:#fff}
|
|
32
|
-
.btn-p:hover{box-shadow:0 4px 12px rgba(59,130,246,.4)}
|
|
33
|
-
.btn-s{background:rgba(51,65,85,.8);color:#94a3b8}
|
|
34
|
-
.btn-s:hover{background:rgba(71,85,105,.8)}
|
|
35
|
-
.btn-d{background:rgba(239,68,68,.15);color:#ef4444;border:1px solid rgba(239,68,68,.3)}
|
|
36
|
-
.btn-d:hover{background:rgba(239,68,68,.25)}
|
|
37
|
-
.btn-row{display:flex;gap:8px;margin-top:12px;flex-wrap:wrap}
|
|
38
|
-
input,select{background:rgba(15,23,42,.8);border:1px solid rgba(100,116,139,.3);border-radius:8px;padding:10px 14px;color:#e0e6ed;font-size:14px;outline:none;width:100%;transition:border-color .2s}
|
|
39
|
-
input:focus{border-color:#60a5fa}
|
|
40
|
-
label{font-size:12px;color:#64748b;font-weight:500;display:block;margin-bottom:4px}
|
|
41
|
-
.fg{margin-bottom:14px}
|
|
42
|
-
.section{margin-bottom:20px}
|
|
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-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)}
|
|
63
|
-
.platform-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px}
|
|
64
|
-
.platform-card{padding:14px;border-radius:10px;border:1px solid rgba(100,116,139,.2);text-align:center;transition:all .2s}
|
|
65
|
-
.platform-card:hover{border-color:#a78bfa;background:rgba(167,139,250,.08)}
|
|
66
|
-
.platform-card .icon{font-size:22px;margin-bottom:4px}
|
|
67
|
-
.platform-card .name{font-size:13px;font-weight:600}
|
|
68
|
-
.modal-bg{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.6);display:flex;justify-content:center;align-items:center;z-index:50;backdrop-filter:blur(4px)}
|
|
69
|
-
.modal{background:#1e293b;border:1px solid rgba(100,116,139,.3);border-radius:16px;padding:24px;width:90%;max-width:500px;max-height:80vh;overflow-y:auto}
|
|
70
|
-
.modal h3{font-size:16px;margin-bottom:16px}
|
|
71
|
-
.cmd-box{background:rgba(15,23,42,.9);border:1px solid rgba(100,116,139,.2);border-radius:8px;padding:12px;font-family:monospace;font-size:12px;color:#94a3b8;word-break:break-all;margin:10px 0;white-space:pre-wrap}
|
|
72
|
-
.toast{position:fixed;top:20px;right:20px;padding:10px 18px;border-radius:8px;font-size:13px;font-weight:500;opacity:0;transform:translateY(-10px);transition:all .3s;z-index:99}
|
|
73
|
-
.toast.show{opacity:1;transform:translateY(0)}
|
|
74
|
-
.toast.ok{background:rgba(34,197,94,.2);border:1px solid #22c55e;color:#22c55e}
|
|
75
|
-
.toast.err{background:rgba(239,68,68,.2);border:1px solid #ef4444;color:#ef4444}
|
|
76
|
-
.empty{text-align:center;padding:40px;color:#475569;font-size:14px}
|
|
77
|
-
.back{font-size:13px;color:#64748b;cursor:pointer;margin-bottom:16px;display:inline-block}
|
|
78
|
-
.back:hover{color:#94a3b8}
|
|
79
|
-
.row{display:flex;gap:12px}
|
|
80
|
-
.row>*{flex:1}
|
|
81
|
-
.drop-zone{border:2px dashed rgba(100,116,139,.3);border-radius:14px;padding:40px;text-align:center;color:#475569;font-size:14px;transition:all .3s;cursor:pointer;margin-top:12px}
|
|
82
|
-
.drop-zone.over{border-color:#60a5fa;background:rgba(96,165,250,.06);color:#60a5fa}
|
|
83
|
-
.auth-badge{font-size:12px;padding:3px 10px;border-radius:12px;font-weight:600;display:inline-flex;align-items:center;gap:4px}
|
|
84
|
-
.auth-badge.ok{background:rgba(34,197,94,.15);color:#22c55e}
|
|
85
|
-
.auth-badge.no{background:rgba(245,158,11,.15);color:#f59e0b}
|
|
86
|
-
.prompt-editor{width:100%;min-height:200px;background:rgba(15,23,42,.9);border:1px solid rgba(100,116,139,.2);border-radius:8px;padding:12px;font-family:'Cascadia Code','Fira Code',monospace;font-size:12px;color:#94a3b8;resize:vertical}
|
|
87
|
-
.mcp-list-item{display:flex;justify-content:space-between;align-items:center;padding:8px 12px;border-radius:8px;border:1px solid rgba(100,116,139,.15);margin-bottom:4px;font-size:13px}
|
|
88
|
-
.mcp-list-item .badges{display:flex;gap:4px}
|
|
89
|
-
.mcp-list-item .badges span{font-size:10px;padding:2px 6px;border-radius:8px;background:rgba(96,165,250,.15);color:#60a5fa}
|
|
90
|
-
.tab-row{display:flex;gap:0;margin-bottom:16px;border-bottom:1px solid rgba(100,116,139,.2)}
|
|
91
|
-
.tab{padding:8px 16px;font-size:13px;font-weight:600;color:#64748b;cursor:pointer;border-bottom:2px solid transparent;transition:all .2s}
|
|
92
|
-
.tab.active{color:#60a5fa;border-color:#60a5fa}
|
|
93
|
-
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
|
|
94
|
-
.dir-picker-list{max-height:50vh;overflow-y:auto}
|
|
95
|
-
.dir-item{padding:8px 12px;border-radius:6px;cursor:pointer;font-size:13px;color:#cbd5e1;display:flex;align-items:center;gap:8px;transition:background .15s}
|
|
96
|
-
.dir-item:hover{background:rgba(96,165,250,.12)}
|
|
97
|
-
.dir-item .icon{font-size:16px}
|
|
98
|
-
.dir-breadcrumb{font-size:12px;color:#64748b;padding:6px 10px;background:rgba(15,23,42,.6);border-radius:6px;margin-bottom:10px;word-break:break-all}
|
|
99
|
-
</style>
|
|
100
|
-
</head>
|
|
101
|
-
<body>
|
|
102
|
-
<div class="app" id="app"></div>
|
|
103
|
-
<div class="toast" id="toast"></div>
|
|
104
|
-
<script>
|
|
105
|
-
// ============ State ============
|
|
106
|
-
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:{}};
|
|
107
|
-
|
|
108
|
-
// ============ Router ============
|
|
109
|
-
function go(page,data){Object.assign(S,data||{});S.page=page;_lastDetailRid=null;cancelAuth();render()}
|
|
110
|
-
|
|
111
|
-
// ============ Toast ============
|
|
112
|
-
function toast(m,t){t=t||'ok';var e=document.getElementById('toast');e.textContent=m;e.className='toast '+t+' show';setTimeout(function(){e.className='toast'},3000)}
|
|
113
|
-
|
|
114
|
-
// ============ API ============
|
|
115
|
-
function api(path,opts){return fetch(path,opts).then(function(r){return r.json()})}
|
|
116
|
-
|
|
117
|
-
// ============ Init ============
|
|
118
|
-
function init(){
|
|
119
|
-
api('/api/status').then(function(st){
|
|
120
|
-
S.host=st.host||'';S.port=st.port||20001;S.hostOk=st.hostConnected;S.projects=st.projects||[];
|
|
121
|
-
render();
|
|
122
|
-
}).catch(function(){render()});
|
|
123
|
-
// Drag-drop
|
|
124
|
-
document.body.addEventListener('dragover',function(e){e.preventDefault();e.dataTransfer.dropEffect='copy';var dz=document.querySelector('.drop-zone');if(dz)dz.classList.add('over')});
|
|
125
|
-
document.body.addEventListener('dragleave',function(){var dz=document.querySelector('.drop-zone');if(dz)dz.classList.remove('over')});
|
|
126
|
-
document.body.addEventListener('drop',function(e){
|
|
127
|
-
e.preventDefault();
|
|
128
|
-
var dz=document.querySelector('.drop-zone');if(dz)dz.classList.remove('over');
|
|
129
|
-
var items=e.dataTransfer.items;
|
|
130
|
-
for(var i=0;i<items.length;i++){
|
|
131
|
-
var entry=items[i].webkitGetAsEntry&&items[i].webkitGetAsEntry();
|
|
132
|
-
if(entry&&entry.isDirectory){
|
|
133
|
-
go('add',{addStep:1,addDir:entry.name});
|
|
134
|
-
toast('请补全绝对路径,例如 D:/projects/'+entry.name);
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// ============ Render ============
|
|
142
|
-
function render(){
|
|
143
|
-
var app=document.getElementById('app');
|
|
144
|
-
if(!S.host&&S.page!=='setup'){S.page='setup'}
|
|
145
|
-
switch(S.page){
|
|
146
|
-
case 'setup':app.innerHTML=renderSetup();break;
|
|
147
|
-
case 'list':app.innerHTML=renderList();break;
|
|
148
|
-
case 'add':app.innerHTML=renderAdd();break;
|
|
149
|
-
case 'detail':app.innerHTML=renderDetail();break;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// ============ Setup ============
|
|
154
|
-
function renderSetup(){
|
|
155
|
-
return '<div class="header"><h1>📡 PPDocs Agent</h1></div>'+
|
|
156
|
-
'<div class="card no-hover"><div class="section"><h2>🏠 主机连接</h2>'+
|
|
157
|
-
'<div class="row"><div class="fg"><label>主机 IP</label><input id="iHost" value="'+S.host+'" placeholder="10.0.0.176"></div>'+
|
|
158
|
-
'<div class="fg" style="max-width:120px"><label>端口</label><input id="iPort" type="number" value="'+S.port+'" placeholder="20001"></div></div>'+
|
|
159
|
-
'<div class="btn-row"><button class="btn btn-p" onclick="doTestHost()">🔍 测试连接</button>'+
|
|
160
|
-
'<span id="hostResult" style="font-size:13px;line-height:32px;margin-left:8px"></span></div></div></div>';
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function doTestHost(){
|
|
164
|
-
var host=document.getElementById('iHost').value.trim();
|
|
165
|
-
var port=parseInt(document.getElementById('iPort').value)||20001;
|
|
166
|
-
var el=document.getElementById('hostResult');
|
|
167
|
-
el.textContent='连接中...';
|
|
168
|
-
api('/api/test',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({host:host,port:port})}).then(function(r){
|
|
169
|
-
if(r.ok){
|
|
170
|
-
el.innerHTML='<span style="color:#22c55e">✅ 连接成功</span>';
|
|
171
|
-
api('/api/host',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({host:host,port:port})});
|
|
172
|
-
S.host=host;S.port=port;S.hostOk=true;
|
|
173
|
-
setTimeout(function(){go('list')},800);
|
|
174
|
-
}else{
|
|
175
|
-
el.innerHTML='<span style="color:#ef4444">❌ '+(r.error||'连接失败')+'</span>';
|
|
176
|
-
}
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// ============ Project List ============
|
|
181
|
-
function renderList(){
|
|
182
|
-
var badge=S.hostOk?'<span class="host-badge">🟢 '+S.host+':'+S.port+'</span>':'<span class="host-badge off">🔴 未连接</span>';
|
|
183
|
-
var cards='';
|
|
184
|
-
if(S.projects.length===0){
|
|
185
|
-
cards='<div class="empty">暂无项目,点击上方按钮添加</div>';
|
|
186
|
-
}else{
|
|
187
|
-
cards=S.projects.map(function(p,i){
|
|
188
|
-
var dot=p.connected?'<span class="sync-dot on"></span>':'<span class="sync-dot off"></span>';
|
|
189
|
-
var sync=p.lastSync?'同步: '+p.lastSync:p.syncStatus||'—';
|
|
190
|
-
return '<div class="card" onclick="go(\\x27detail\\x27,{detailIdx:'+i+',detailTab:\\x27overview\\x27})">'+
|
|
191
|
-
'<h3>'+dot+' 📂 '+(p.localName||p.remoteName)+'</h3>'+
|
|
192
|
-
'<div class="meta">'+(p.localDir||'')+'</div>'+
|
|
193
|
-
'<div class="stat"><span>远程: '+p.remoteName+' ('+p.remoteId+')</span><span>'+(p.docCount?p.docCount+' 文档':'')+'</span></div>'+
|
|
194
|
-
'<div class="meta">'+sync+'</div></div>';
|
|
195
|
-
}).join('');
|
|
196
|
-
}
|
|
197
|
-
return '<div class="header"><h1>📡 PPDocs Agent</h1>'+badge+'</div>'+
|
|
198
|
-
'<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">'+
|
|
199
|
-
'<span style="font-size:15px;font-weight:600">我的项目 ('+S.projects.length+')</span>'+
|
|
200
|
-
'<div class="btn-row" style="margin:0"><button class="btn btn-p" onclick="go(\\x27add\\x27,{addStep:1,addDir:\\x27\\x27})">➕ 添加项目</button>'+
|
|
201
|
-
'<button class="btn btn-s" onclick="go(\\x27setup\\x27)">⚙️</button></div></div>'+
|
|
202
|
-
cards+
|
|
203
|
-
'<div class="drop-zone">📂 拖拽项目文件夹到此处快速添加</div>';
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// ============ Add Project ============
|
|
207
|
-
function renderAdd(){
|
|
208
|
-
if(S.addStep===1) return renderAddStep1();
|
|
209
|
-
return renderAddStep2();
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function renderAddStep1(){
|
|
213
|
-
var dirName=S.addDir?S.addDir.replace(/\\\\/g,'/').split('/').filter(Boolean).pop():'';
|
|
214
|
-
var namePreview=dirName?'<div style="font-size:12px;color:#60a5fa;margin:-8px 0 12px">📂 项目名: <strong>'+dirName+'</strong></div>':'';
|
|
215
|
-
return '<span class="back" onclick="cancelAuth();go(\\x27list\\x27)">← 返回项目列表</span>'+
|
|
216
|
-
'<div class="card no-hover"><div class="section"><h2>➕ 添加项目 — Step 1/2</h2>'+
|
|
217
|
-
'<p style="font-size:13px;color:#64748b;margin-bottom:12px">指定本地项目目录,以目录名作为项目名称</p>'+
|
|
218
|
-
'<div class="fg"><label>本地项目目录 (绝对路径)</label><div style="display:flex;gap:8px"><input id="iDir" style="flex:1" value="'+S.addDir+'" placeholder="例: /Users/me/projects/my-app" oninput="S.addDir=this.value;_render()"><button class="btn btn-p" style="white-space:nowrap;padding:6px 14px" onclick="nativePickDir()">📁 选择目录</button></div></div>'+
|
|
219
|
-
namePreview+
|
|
220
|
-
'<div class="btn-row"><button class="btn btn-p" onclick="doAuthStart()">📡 发送授权请求</button></div>'+
|
|
221
|
-
'</div></div>';
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function nativePickDir(){
|
|
225
|
-
toast('正在打开系统目录选择器...');
|
|
226
|
-
api('/api/pick-dir').then(function(r){
|
|
227
|
-
if(r.ok&&r.dir){S.addDir=r.dir;_render();toast('✅ 已选择: '+r.dir);}
|
|
228
|
-
else if(r.cancelled){toast('已取消');}
|
|
229
|
-
else{toast('系统选择器不可用,使用内置浏览器','err');openDirPicker();}
|
|
230
|
-
}).catch(function(){openDirPicker();});
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function openDirPicker(){
|
|
234
|
-
var currentDir='';
|
|
235
|
-
function browse(d){
|
|
236
|
-
api('/api/browse'+(d?'?dir='+encodeURIComponent(d):'')).then(function(r){
|
|
237
|
-
if(!r.ok){toast(r.error||'浏览失败','err');return}
|
|
238
|
-
currentDir=r.dir||'';
|
|
239
|
-
var dirs=r.dirs||[];
|
|
240
|
-
var shortcuts=r.shortcuts||[];
|
|
241
|
-
var sep=r.platform==='win32'?'\\\\':'/';
|
|
242
|
-
var html='<div class="dir-breadcrumb">📍 '+(currentDir||'根目录')+'</div>';
|
|
243
|
-
if(!currentDir&&shortcuts.length>0){
|
|
244
|
-
for(var s=0;s<shortcuts.length;s++){var sp=shortcuts[s].path.replace(/\\\\/g,'\\\\\\\\').replace(/'/g,"\\\\'");html+='<div class="dir-item" style="color:#60a5fa" onclick="dirPickerNav(\\x27'+sp+'\\x27)"><span class="icon">⚡</span> '+esc(shortcuts[s].name)+'</div>';}
|
|
245
|
-
html+='<div style="margin:8px 0;border-top:1px solid rgba(100,116,139,.2)"></div>';
|
|
246
|
-
}
|
|
247
|
-
if(currentDir){
|
|
248
|
-
var parts=currentDir.replace(/\\\\/g,'/').split('/').filter(Boolean);
|
|
249
|
-
var parent=parts.length>1?parts.slice(0,-1).join(sep):(r.platform==='win32'?'':'/');
|
|
250
|
-
if(r.platform==='win32'&&parts.length===1)parent='';
|
|
251
|
-
html+='<div class="dir-item" onclick="dirPickerNav(\\x27'+parent.replace(/\\\\/g,'\\\\\\\\').replace(/'/g,"\\\\x27")+'\\x27)"><span class="icon">⬆️</span> .. 上级目录</div>';
|
|
252
|
-
html+='<div class="dir-item" style="background:rgba(96,165,250,.15);border:1px solid rgba(96,165,250,.3)" onclick="dirPickerSelect(\\x27'+currentDir.replace(/\\\\/g,'\\\\\\\\').replace(/'/g,"\\\\x27")+'\\x27)"><span class="icon">✅</span> <strong>选择当前目录</strong></div>';
|
|
253
|
-
}
|
|
254
|
-
for(var i=0;i<dirs.length;i++){
|
|
255
|
-
var dn=dirs[i].replace(/\\\\/g,'/').split('/').filter(Boolean).pop();
|
|
256
|
-
html+='<div class="dir-item" onclick="dirPickerNav(\\x27'+dirs[i].replace(/\\\\/g,'\\\\\\\\').replace(/'/g,"\\\\x27")+'\\x27)"><span class="icon">📁</span> '+esc(dn)+'</div>';
|
|
257
|
-
}
|
|
258
|
-
if(dirs.length===0&¤tDir){html+='<div style="padding:12px;color:#475569;font-size:13px">(空目录)</div>';}
|
|
259
|
-
var el=document.getElementById('dirPickerBody');
|
|
260
|
-
if(el)el.innerHTML=html;
|
|
261
|
-
});
|
|
262
|
-
}
|
|
263
|
-
window.dirPickerNav=function(d){browse(d)};
|
|
264
|
-
window.dirPickerSelect=function(d){
|
|
265
|
-
S.addDir=d;var bg=document.querySelector('.modal-bg');if(bg)bg.remove();_render();
|
|
266
|
-
};
|
|
267
|
-
var div=document.createElement('div');div.className='modal-bg';
|
|
268
|
-
div.innerHTML='<div class="modal" style="max-width:550px"><h3>📁 选择项目目录</h3><div class="dir-picker-list" id="dirPickerBody"><div style="padding:20px;color:#64748b">加载中...</div></div><div class="btn-row"><button class="btn btn-s" onclick="closeModal()">取消</button></div></div>';
|
|
269
|
-
document.body.appendChild(div);div.addEventListener('click',function(e){if(e.target===div)div.remove()});
|
|
270
|
-
browse('');
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
function doAuthStart(){
|
|
274
|
-
var dir=document.getElementById('iDir').value.trim();
|
|
275
|
-
if(!dir){toast('请填写目录路径','err');return}
|
|
276
|
-
if(!/^[A-Za-z]:[\\\\\\/]|^\\//.test(dir)){toast('请输入绝对路径','err');return}
|
|
277
|
-
S.addDir=dir;
|
|
278
|
-
api('/api/auth/start',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({localDir:dir})}).then(function(r){
|
|
279
|
-
if(!r.ok){toast(r.error||'授权请求失败','err');return}
|
|
280
|
-
S.authRequestId=r.requestId;S.authStartTime=Date.now();S.authStatus='pending';S.addStep=2;
|
|
281
|
-
render();startAuthPoll();
|
|
282
|
-
});
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
var authPollTimer=null,authUiTimer=null;
|
|
286
|
-
function startAuthPoll(){
|
|
287
|
-
cancelAuth();
|
|
288
|
-
authPollTimer=setInterval(function(){
|
|
289
|
-
if(!S.authRequestId){cancelAuth();return}
|
|
290
|
-
var elapsed=Math.floor((Date.now()-S.authStartTime)/1000);
|
|
291
|
-
if(elapsed>=300){S.authStatus='expired';cancelAuth();_render();return}
|
|
292
|
-
api('/api/auth/status/'+S.authRequestId).then(function(r){
|
|
293
|
-
S.authStatus=r.status;
|
|
294
|
-
if(r.status==='approved'){
|
|
295
|
-
cancelAuth();
|
|
296
|
-
api('/api/bind',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({
|
|
297
|
-
localDir:S.addDir,remoteId:r.projectId,remoteName:r.projectName,password:r.password
|
|
298
|
-
})}).then(function(br){
|
|
299
|
-
if(br.ok){
|
|
300
|
-
S.authResult={name:r.projectName,id:r.projectId};_render();
|
|
301
|
-
toast('✅ 授权成功,已绑定 '+r.projectName);
|
|
302
|
-
setTimeout(function(){init();go('list')},2000);
|
|
303
|
-
}else{toast(br.error||'绑定失败','err')}
|
|
304
|
-
});
|
|
305
|
-
}else if(r.status==='rejected'){cancelAuth();_render()}
|
|
306
|
-
else{_render()}
|
|
307
|
-
}).catch(function(){});
|
|
308
|
-
},2000);
|
|
309
|
-
authUiTimer=setInterval(function(){if(S.page==='add'&&S.addStep===2&&S.authStatus==='pending')_render()},1000);
|
|
310
|
-
}
|
|
311
|
-
function cancelAuth(){if(authPollTimer){clearInterval(authPollTimer);authPollTimer=null}if(authUiTimer){clearInterval(authUiTimer);authUiTimer=null}}
|
|
312
|
-
|
|
313
|
-
function renderAddStep2(){
|
|
314
|
-
var elapsed=Math.floor((Date.now()-(S.authStartTime||Date.now()))/1000);
|
|
315
|
-
var pct=Math.min(100,Math.round(elapsed/300*100));
|
|
316
|
-
if(S.authStatus==='approved'&&S.authResult){
|
|
317
|
-
return '<div class="card no-hover" style="text-align:center;padding:40px">'+
|
|
318
|
-
'<div style="font-size:40px;margin-bottom:12px">✅</div>'+
|
|
319
|
-
'<h2 style="color:#22c55e;font-size:18px;margin-bottom:8px">授权成功!</h2>'+
|
|
320
|
-
'<p style="color:#94a3b8;font-size:14px">项目: '+S.authResult.name+' ('+S.authResult.id+')</p>'+
|
|
321
|
-
'<p style="color:#64748b;font-size:13px;margin-top:4px">权限: 读写 | 2秒后跳转...</p></div>';
|
|
322
|
-
}
|
|
323
|
-
if(S.authStatus==='rejected'){
|
|
324
|
-
return '<span class="back" onclick="go(\\x27list\\x27)">← 返回</span>'+
|
|
325
|
-
'<div class="card no-hover" style="text-align:center;padding:40px">'+
|
|
326
|
-
'<div style="font-size:40px;margin-bottom:12px">❌</div>'+
|
|
327
|
-
'<h2 style="color:#ef4444;font-size:18px;margin-bottom:8px">授权被拒绝</h2>'+
|
|
328
|
-
'<div class="btn-row" style="justify-content:center;margin-top:16px"><button class="btn btn-p" onclick="S.addStep=1;render()">重试</button></div></div>';
|
|
329
|
-
}
|
|
330
|
-
if(S.authStatus==='expired'){
|
|
331
|
-
return '<span class="back" onclick="go(\\x27list\\x27)">← 返回</span>'+
|
|
332
|
-
'<div class="card no-hover" style="text-align:center;padding:40px">'+
|
|
333
|
-
'<div style="font-size:40px;margin-bottom:12px">⏰</div>'+
|
|
334
|
-
'<h2 style="color:#f59e0b;font-size:18px;margin-bottom:8px">授权已超时</h2>'+
|
|
335
|
-
'<div class="btn-row" style="justify-content:center;margin-top:16px"><button class="btn btn-p" onclick="S.addStep=1;render()">重试</button></div></div>';
|
|
336
|
-
}
|
|
337
|
-
return '<span class="back" onclick="cancelAuth();S.addStep=1;render()">← 返回</span>'+
|
|
338
|
-
'<div class="card no-hover"><div class="section"><h2>➕ 添加项目 — Step 2/2</h2>'+
|
|
339
|
-
'<div style="text-align:center;padding:20px 0">'+
|
|
340
|
-
'<div style="font-size:36px;margin-bottom:10px;animation:pulse 2s infinite">📡</div>'+
|
|
341
|
-
'<p style="font-size:15px;font-weight:600;color:#60a5fa;margin-bottom:6px">等待主机端授权...</p>'+
|
|
342
|
-
'<p style="font-size:12px;color:#64748b;margin-bottom:4px">已向 '+S.host+':'+S.port+' 发送授权请求</p>'+
|
|
343
|
-
'<p style="font-size:12px;color:#475569">请在主机端 PPDocs 桌面应用中选择项目并批准</p>'+
|
|
344
|
-
'<div style="margin:16px auto;width:80%;height:4px;background:rgba(100,116,139,.2);border-radius:2px;overflow:hidden">'+
|
|
345
|
-
'<div style="width:'+pct+'%;height:100%;background:linear-gradient(90deg,#3b82f6,#6366f1);border-radius:2px;transition:width .5s"></div></div>'+
|
|
346
|
-
'<p style="font-size:11px;color:#475569">'+elapsed+'s / 300s</p>'+
|
|
347
|
-
'</div><div class="btn-row" style="justify-content:center"><button class="btn btn-s" onclick="cancelAuth();go(\\x27list\\x27)">取消</button></div></div></div>';
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
// ============ Project Detail ============
|
|
351
|
-
function renderDetail(){
|
|
352
|
-
var p=S.projects[S.detailIdx];
|
|
353
|
-
if(!p) return '<span class="back" onclick="go(\\x27list\\x27)">← 项目不存在</span>';
|
|
354
|
-
var tab=S.detailTab||'overview';
|
|
355
|
-
var tabs=[{key:'overview',label:'📊 概览'},{key:'mcp',label:'🤖 MCP'},{key:'prompts',label:'📝 提示词'}];
|
|
356
|
-
var tabHtml=tabs.map(function(t){return '<div class="tab '+(tab===t.key?'active':'')+'" onclick="S.detailTab=\\x27'+t.key+'\\x27;_lastDetailRid=null;render()">'+t.label+'</div>'}).join('');
|
|
357
|
-
var tabContent='';
|
|
358
|
-
if(tab==='overview') tabContent=renderOverview(p);
|
|
359
|
-
else if(tab==='mcp') tabContent=renderMcpTab(p);
|
|
360
|
-
else if(tab==='prompts') tabContent=renderPromptsTab(p);
|
|
361
|
-
return '<span class="back" onclick="go(\\x27list\\x27)">← 返回项目列表</span>'+
|
|
362
|
-
'<div class="header"><h1>📂 '+(p.localName||p.remoteName)+'</h1></div>'+
|
|
363
|
-
'<div class="tab-row">'+tabHtml+'</div>'+tabContent+
|
|
364
|
-
'<div class="card no-hover" style="border-color:rgba(239,68,68,.2)"><div class="section"><h2>⚠️ 危险区域</h2>'+
|
|
365
|
-
'<button class="btn btn-d" onclick="doUnbind(\\x27'+p.remoteId+'\\x27)">🗑️ 解除绑定</button></div></div>';
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
function renderOverview(p){
|
|
369
|
-
var authBadge=p.hasPassword?'<span class="auth-badge ok">✅ 已授权 (读写)</span>':'<span class="auth-badge no">⚠️ 未授权 (只读)</span>';
|
|
370
|
-
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>';
|
|
371
|
-
var overviewCard='<div class="card no-hover"><div class="section"><h2>📊 概览</h2>'+
|
|
372
|
-
'<div class="stat"><span>本地: '+(p.localDir||'<em style="color:#ef4444">未指定</em>')+'</span></div>'+
|
|
373
|
-
'<div class="stat"><span>远程: '+p.remoteName+' ('+p.remoteId+')</span></div>'+
|
|
374
|
-
'<div class="stat"><span>授权: '+authBadge+authBtn+'</span></div>'+
|
|
375
|
-
'<div class="stat"><span>文档: '+(p.docCount||0)+' 个</span></div>'+
|
|
376
|
-
'<div class="stat"><span>同步: '+(p.connected?'🟢 运行中':'🔴 '+p.syncStatus)+' '+(p.lastSync?'('+p.lastSync+')':'')+'</span></div>'+
|
|
377
|
-
'</div></div>';
|
|
378
|
-
// ---- 知识图谱树 ----
|
|
379
|
-
var kgCard='';
|
|
380
|
-
if(!S.kgLoaded){
|
|
381
|
-
kgCard='<div class="card no-hover"><div class="section"><h2>📚 知识图谱</h2><div class="kg-empty" style="animation:pulse 1.5s infinite">⏳ 加载中...</div></div></div>';
|
|
382
|
-
}else if(!S.kgDocs||S.kgDocs.length===0){
|
|
383
|
-
kgCard='<div class="card no-hover"><div class="section"><h2>📚 知识图谱</h2><div class="kg-empty">📭 知识库为空</div></div></div>';
|
|
384
|
-
}else{
|
|
385
|
-
var readOnlyBadge=S.kgReadOnly?'<span style="font-size:11px;color:#f59e0b;margin-left:8px">🔒 只读</span>':'';
|
|
386
|
-
var statsHtml='<div class="kg-stats"><span>📄 '+S.kgDocCount+' 文档</span><span>📁 '+S.kgDirCount+' 目录</span></div>';
|
|
387
|
-
var searchHtml='<input class="kg-search" placeholder="搜索文档..." value="'+S.kgFilter+'" oninput="S.kgFilter=this.value;_render()">';
|
|
388
|
-
var treeHtml=renderKgTree(S.kgDocs,p.remoteId,'',0);
|
|
389
|
-
kgCard='<div class="card no-hover"><div class="section"><h2>📚 知识图谱'+readOnlyBadge+'</h2>'+statsHtml+'<div class="tree-box">'+searchHtml+treeHtml+'</div></div></div>';
|
|
390
|
-
}
|
|
391
|
-
return overviewCard+kgCard;
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
function esc(s){return s?s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'):''}
|
|
395
|
-
function renderKgTree(docs,rid,parentPath,depth){
|
|
396
|
-
var filter=S.kgFilter?S.kgFilter.toLowerCase():'';
|
|
397
|
-
// 获取当前层级的直接子节点
|
|
398
|
-
var children=docs.filter(function(d){
|
|
399
|
-
// 直接子级: 路径以 parentPath 开头, 且去掉 parentPath 后只剩一级
|
|
400
|
-
if(parentPath===''){
|
|
401
|
-
var parts=d.path.split('/').filter(Boolean);
|
|
402
|
-
return parts.length===1;
|
|
403
|
-
}
|
|
404
|
-
if(d.path.indexOf(parentPath+'/')!==0) return false;
|
|
405
|
-
var rest=d.path.slice(parentPath.length+1);
|
|
406
|
-
return rest.indexOf('/')===-1;
|
|
407
|
-
});
|
|
408
|
-
if(filter){
|
|
409
|
-
children=children.filter(function(d){
|
|
410
|
-
if(d.name.toLowerCase().indexOf(filter)>=0) return true;
|
|
411
|
-
if(d.summary&&d.summary.toLowerCase().indexOf(filter)>=0) return true;
|
|
412
|
-
// 目录: 检查子孙是否匹配
|
|
413
|
-
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)); });
|
|
414
|
-
return false;
|
|
415
|
-
});
|
|
416
|
-
}
|
|
417
|
-
if(children.length===0) return depth===0?'<div class="kg-empty">无匹配结果</div>':'';
|
|
418
|
-
var html='';
|
|
419
|
-
children.sort(function(a,b){ if(a.isDir!==b.isDir) return a.isDir?-1:1; return a.name.localeCompare(b.name); });
|
|
420
|
-
children.forEach(function(d){
|
|
421
|
-
var collapsed=S.kgCollapsed[d.path];
|
|
422
|
-
if(d.isDir){
|
|
423
|
-
var arrowCls=collapsed?'arrow':'arrow open';
|
|
424
|
-
// 计算子文档数
|
|
425
|
-
var subCount=docs.filter(function(dd){return !dd.isDir&&dd.path.indexOf(d.path+'/')===0}).length;
|
|
426
|
-
var badge=subCount>0?'<span style="font-size:10px;color:#475569;margin-left:4px">('+subCount+')</span>':'';
|
|
427
|
-
html+='<div class="kg-node dir" onclick="toggleKgDir(\\x27'+d.path.replace(/'/g,"\\x27")+'\\x27)">';
|
|
428
|
-
html+='<span class="'+arrowCls+'">▶</span>';
|
|
429
|
-
html+='<span class="icon">📁</span>';
|
|
430
|
-
html+='<span class="name">'+esc(d.name)+badge+'</span>';
|
|
431
|
-
html+='</div>';
|
|
432
|
-
var childHtml=renderKgTree(docs,rid,d.path,depth+1);
|
|
433
|
-
html+='<div class="kg-children'+(collapsed?' collapsed':'')+'">'+(childHtml||'')+'</div>';
|
|
434
|
-
}else{
|
|
435
|
-
var sum=d.summary?'<span class="summary">'+esc(d.summary)+'</span>':'';
|
|
436
|
-
html+='<div class="kg-node" onclick="doViewDoc(\\x27'+rid+'\\x27,\\x27'+d.path.replace(/'/g,"\\x27")+'\\x27)">';
|
|
437
|
-
html+='<span class="arrow" style="visibility:hidden">▶</span>';
|
|
438
|
-
html+='<span class="icon">📄</span>';
|
|
439
|
-
html+='<span class="name">'+esc(d.name)+'</span>'+sum;
|
|
440
|
-
html+='</div>';
|
|
441
|
-
}
|
|
442
|
-
});
|
|
443
|
-
return html;
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
function toggleKgDir(path){
|
|
447
|
-
S.kgCollapsed[path]=!S.kgCollapsed[path];
|
|
448
|
-
_render();
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
function renderMcpTab(p){
|
|
452
|
-
var mcpState=S.mcpStatus||{};
|
|
453
|
-
var platforms=[
|
|
454
|
-
{key:'cursor',icon:'🟣',name:'Cursor'},
|
|
455
|
-
{key:'antigravity',icon:'🟢',name:'Antigravity'},
|
|
456
|
-
{key:'vscode',icon:'🔵',name:'OpenCode'},
|
|
457
|
-
{key:'claude',icon:'🟠',name:'Claude'},
|
|
458
|
-
{key:'lobechat',icon:'🦞',name:'LobeChat'}
|
|
459
|
-
];
|
|
460
|
-
var platformCards=platforms.map(function(pl){
|
|
461
|
-
var isManual=pl.key==='lobechat';
|
|
462
|
-
var installed=mcpState[pl.key];
|
|
463
|
-
var badge='',btn='';
|
|
464
|
-
if(isManual){
|
|
465
|
-
badge='<span style="font-size:11px;color:#94a3b8;display:block;margin:4px 0">手动配置</span>';
|
|
466
|
-
btn='<button class="btn btn-p" style="font-size:11px;padding:4px 10px;background:rgba(59,130,246,.5)" onclick="event.stopPropagation();doMcpInstall(\\x27'+p.remoteId+'\\x27,\\x27'+pl.key+'\\x27)">获取配置</button>';
|
|
467
|
-
}else{
|
|
468
|
-
badge=installed?'<span style="font-size:11px;color:#22c55e;display:block;margin:4px 0">✅ 已配置</span>':'<span style="font-size:11px;color:#64748b;display:block;margin:4px 0">未配置</span>';
|
|
469
|
-
btn=installed?'<button class="btn btn-d" style="font-size:11px;padding:4px 10px" onclick="event.stopPropagation();doMcpUninstall(\\x27'+p.remoteId+'\\x27,\\x27'+pl.key+'\\x27)">卸载</button>':'<button class="btn btn-p" style="font-size:11px;padding:4px 10px" onclick="event.stopPropagation();doMcpInstall(\\x27'+p.remoteId+'\\x27,\\x27'+pl.key+'\\x27)">安装/更新</button>';
|
|
470
|
-
}
|
|
471
|
-
return '<div class="platform-card"><div class="icon">'+pl.icon+'</div><div class="name" style="white-space:nowrap">'+pl.name+'</div>'+badge+btn+'</div>';
|
|
472
|
-
}).join('');
|
|
473
|
-
var noDir=!p.localDir;
|
|
474
|
-
var mcpListHtml='';
|
|
475
|
-
if(S.mcpAll&&S.mcpAll.length>0){
|
|
476
|
-
mcpListHtml=S.mcpAll.map(function(s){
|
|
477
|
-
var badges=s.platforms.map(function(pl){return '<span>'+pl+'</span>'}).join('');
|
|
478
|
-
return '<div class="mcp-list-item"><span>📦 '+s.name+'</span><div class="badges">'+badges+'</div></div>';
|
|
479
|
-
}).join('');
|
|
480
|
-
}else{
|
|
481
|
-
mcpListHtml='<div style="font-size:12px;color:#475569;padding:8px 0">未检测到任何 MCP 服务</div>';
|
|
482
|
-
}
|
|
483
|
-
return '<div class="card no-hover"><div class="section"><h2>🤖 PPDocs MCP 安装</h2>'+
|
|
484
|
-
(noDir?'<div style="font-size:13px;color:#f59e0b;margin-bottom:8px">⚠️ 未指定本地目录</div>':'')+
|
|
485
|
-
'<div class="platform-grid">'+platformCards+'</div></div></div>'+
|
|
486
|
-
'<div class="card no-hover"><div class="section"><h2>📦 项目内全部 MCP ('+(S.mcpAll?S.mcpAll.length:0)+')</h2>'+
|
|
487
|
-
mcpListHtml+'</div></div>';
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
function renderPromptsTab(p){
|
|
491
|
-
if(!p.localDir) return '<div class="card no-hover"><div class="section"><h2>📝 提示词</h2><div style="font-size:13px;color:#f59e0b">⚠️ 未指定本地目录</div></div></div>';
|
|
492
|
-
var files=S.prompts||[];
|
|
493
|
-
var cards=files.map(function(f){
|
|
494
|
-
var statusBadge=f.exists?'<span style="font-size:11px;color:#22c55e">'+f.lines+' 行</span>':'<span style="font-size:11px;color:#64748b">不存在</span>';
|
|
495
|
-
var btn=f.exists
|
|
496
|
-
?'<button class="btn btn-s" style="font-size:11px;padding:3px 8px" onclick="doViewPrompt(\\x27'+p.remoteId+'\\x27,\\x27'+f.name+'\\x27)">查看/编辑</button>'
|
|
497
|
-
:'<button class="btn btn-p" style="font-size:11px;padding:3px 8px" onclick="doCreatePrompt(\\x27'+p.remoteId+'\\x27,\\x27'+f.name+'\\x27)">创建</button>';
|
|
498
|
-
return '<div class="mcp-list-item"><span>📄 '+f.name+'</span><div style="display:flex;gap:8px;align-items:center">'+statusBadge+btn+'</div></div>';
|
|
499
|
-
}).join('');
|
|
500
|
-
return '<div class="card no-hover"><div class="section"><h2>📝 系统提示词</h2>'+(cards||'<div style="font-size:12px;color:#475569">加载中...</div>')+'</div></div>';
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
// ============ Doc Viewer ============
|
|
504
|
-
function doViewDoc(rid,docPath){
|
|
505
|
-
api('/api/bind/'+rid+'/doc?path='+encodeURIComponent(docPath)).then(function(r){
|
|
506
|
-
if(!r.ok){toast(r.error||'读取失败','err');return}
|
|
507
|
-
var name=esc(docPath.split('/').pop()||docPath);
|
|
508
|
-
var meta='';
|
|
509
|
-
if(r.summary||r.status){
|
|
510
|
-
meta='<div class="doc-modal-meta">';
|
|
511
|
-
if(r.status) meta+='<span>状态: '+r.status+'</span>';
|
|
512
|
-
if(r.summary) meta+='<span>'+r.summary+'</span>';
|
|
513
|
-
meta+='</div>';
|
|
514
|
-
}
|
|
515
|
-
var escaped=(r.content||'暂无内容').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
516
|
-
var div=document.createElement('div');div.className='modal-bg';
|
|
517
|
-
div.innerHTML='<div class="modal" style="max-width:650px"><h3>📄 '+name+'</h3>'+
|
|
518
|
-
'<div style="font-size:11px;color:#475569;margin:-12px 0 12px">'+docPath+'</div>'+
|
|
519
|
-
meta+
|
|
520
|
-
'<div class="doc-modal-content">'+escaped+'</div>'+
|
|
521
|
-
'<div class="btn-row" style="margin-top:12px"><button class="btn btn-s" onclick="this.closest(\\x27.modal-bg\\x27).remove()">关闭</button></div></div>';
|
|
522
|
-
document.body.appendChild(div);
|
|
523
|
-
div.addEventListener('click',function(e){if(e.target===div)div.remove()});
|
|
524
|
-
});
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
// ============ Data Loading ============
|
|
528
|
-
function loadTree(rid){
|
|
529
|
-
S.kgLoaded=false;
|
|
530
|
-
return api('/api/bind/'+rid+'/tree').then(function(r){
|
|
531
|
-
S.kgDocs=r.docs||[];
|
|
532
|
-
S.kgDocCount=r.docCount||0;
|
|
533
|
-
S.kgDirCount=r.dirCount||0;
|
|
534
|
-
S.kgReadOnly=!!r.readOnly;
|
|
535
|
-
S.kgLoaded=true;
|
|
536
|
-
}).catch(function(e){S.kgDocs=[];S.kgLoaded=true;console.error('loadTree:',e)});
|
|
537
|
-
}
|
|
538
|
-
function loadMcpStatus(rid){return api('/api/bind/'+rid+'/mcp').then(function(r){S.mcpStatus=r.platforms||{}}).catch(function(){S.mcpStatus={}})}
|
|
539
|
-
function loadMcpAll(rid){return api('/api/bind/'+rid+'/mcp/all').then(function(r){S.mcpAll=r.servers||[]}).catch(function(){S.mcpAll=[]})}
|
|
540
|
-
function loadPrompts(rid){return api('/api/bind/'+rid+'/prompts').then(function(r){S.prompts=r.files||[]}).catch(function(){S.prompts=[]})}
|
|
541
|
-
|
|
542
|
-
// ============ Auth from Detail ============
|
|
543
|
-
function doReAuth(remoteId,localDir){
|
|
544
|
-
cancelAuth();
|
|
545
|
-
api('/api/auth/start',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({localDir:localDir})}).then(function(r){
|
|
546
|
-
if(!r.ok){toast(r.error||'授权请求失败','err');return}
|
|
547
|
-
toast('📡 已发送授权请求,请在主机端批准');
|
|
548
|
-
S.authRequestId=r.requestId;S.authStartTime=Date.now();
|
|
549
|
-
authPollTimer=setInterval(function(){
|
|
550
|
-
var elapsed=Math.floor((Date.now()-S.authStartTime)/1000);
|
|
551
|
-
if(elapsed>=300){cancelAuth();toast('⏰ 授权已超时','err');return}
|
|
552
|
-
api('/api/auth/status/'+S.authRequestId).then(function(sr){
|
|
553
|
-
if(sr.status==='approved'){cancelAuth();
|
|
554
|
-
api('/api/bind/'+remoteId,{method:'PATCH',headers:{'Content-Type':'application/json'},body:JSON.stringify({password:sr.password,remoteName:sr.projectName})}).then(function(){
|
|
555
|
-
toast('✅ 授权成功!');_lastDetailRid=null;init();
|
|
556
|
-
});
|
|
557
|
-
}else if(sr.status==='rejected'){cancelAuth();toast('❌ 授权被拒绝','err')}
|
|
558
|
-
else if(sr.status==='expired'){cancelAuth();toast('⏰ 授权已超时','err')}
|
|
559
|
-
}).catch(function(){});
|
|
560
|
-
},2000);
|
|
561
|
-
});
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
// ============ MCP ============
|
|
565
|
-
function doMcpInstall(remoteId,platform){
|
|
566
|
-
api('/api/bind/'+remoteId+'/mcp',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({platform:platform})}).then(function(r){
|
|
567
|
-
if(r.ok){
|
|
568
|
-
if(r.action==='copy'){S.modal={title:platform.toUpperCase()+' MCP 配置',content:r.config};renderModal()}
|
|
569
|
-
else{toast(r.message||'已安装/更新');loadMcpStatus(remoteId).then(function(){return loadMcpAll(remoteId)}).then(function(){_render()})}
|
|
570
|
-
}else{toast(r.error||'操作失败','err')}
|
|
571
|
-
});
|
|
572
|
-
}
|
|
573
|
-
function renderModal(){
|
|
574
|
-
if(!S.modal)return;var div=document.createElement('div');div.className='modal-bg';
|
|
575
|
-
div.innerHTML='<div class="modal"><h3>'+S.modal.title+'</h3><div class="cmd-box" id="mcpCopyBox">'+S.modal.content+'</div>'+
|
|
576
|
-
'<div class="btn-row"><button class="btn btn-p" onclick="copyMcpConfig()">📋 复制</button>'+
|
|
577
|
-
'<button class="btn btn-s" onclick="closeModal()">关闭</button></div></div>';
|
|
578
|
-
document.body.appendChild(div);div.addEventListener('click',function(e){if(e.target===div)div.remove()});
|
|
579
|
-
}
|
|
580
|
-
function closeModal(){var bg=document.querySelector('.modal-bg');if(bg)bg.remove();S.modal=null}
|
|
581
|
-
function copyMcpConfig(){var el=document.getElementById('mcpCopyBox');if(el){navigator.clipboard.writeText(el.textContent);toast('已复制')}}
|
|
582
|
-
function doMcpUninstall(remoteId,platform){
|
|
583
|
-
if(!confirm('确定卸载 '+platform+' 的 MCP 配置?'))return;
|
|
584
|
-
api('/api/bind/'+remoteId+'/mcp',{method:'DELETE',headers:{'Content-Type':'application/json'},body:JSON.stringify({platform:platform})}).then(function(r){
|
|
585
|
-
if(r.ok){toast(r.message||'已卸载');loadMcpStatus(remoteId).then(function(){return loadMcpAll(remoteId)}).then(function(){_render()})}
|
|
586
|
-
else{toast(r.error||'卸载失败','err')}
|
|
587
|
-
});
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
// ============ Prompts ============
|
|
591
|
-
function doViewPrompt(remoteId,name){
|
|
592
|
-
api('/api/bind/'+remoteId+'/prompts/'+encodeURIComponent(name)).then(function(r){
|
|
593
|
-
if(!r.ok){toast(r.error||'读取失败','err');return}
|
|
594
|
-
S.modal={title:'📝 '+name,content:r.content,remoteId:remoteId,fileName:name};renderPromptEditor();
|
|
595
|
-
});
|
|
596
|
-
}
|
|
597
|
-
function doCreatePrompt(remoteId,name){S.modal={title:'📝 创建 '+name,content:'',remoteId:remoteId,fileName:name};renderPromptEditor()}
|
|
598
|
-
function renderPromptEditor(){
|
|
599
|
-
if(!S.modal)return;var div=document.createElement('div');div.className='modal-bg';
|
|
600
|
-
var escaped=(S.modal.content||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
601
|
-
div.innerHTML='<div class="modal" style="max-width:650px"><h3>'+S.modal.title+'</h3>'+
|
|
602
|
-
'<textarea class="prompt-editor" id="promptEditor">'+escaped+'</textarea>'+
|
|
603
|
-
'<div class="btn-row"><button class="btn btn-p" onclick="doSavePrompt()">💾 保存</button>'+
|
|
604
|
-
'<button class="btn btn-s" onclick="closeModal()">关闭</button></div></div>';
|
|
605
|
-
document.body.appendChild(div);div.addEventListener('click',function(e){if(e.target===div)div.remove()});
|
|
606
|
-
}
|
|
607
|
-
function doSavePrompt(){
|
|
608
|
-
var content=document.getElementById('promptEditor').value;
|
|
609
|
-
api('/api/bind/'+S.modal.remoteId+'/prompts/'+encodeURIComponent(S.modal.fileName),{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify({content:content})}).then(function(r){
|
|
610
|
-
if(r.ok){toast('✅ 已保存');var rid=S.modal.remoteId;closeModal();loadPrompts(rid).then(function(){_render()})}
|
|
611
|
-
else{toast(r.error||'保存失败','err')}
|
|
612
|
-
});
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
// ============ Unbind ============
|
|
616
|
-
function doUnbind(remoteId){
|
|
617
|
-
if(!confirm('确定解除绑定?'))return;
|
|
618
|
-
api('/api/bind/'+remoteId,{method:'DELETE'}).then(function(r){if(r.ok){toast('已解绑');init();go('list')}else{toast('解绑失败','err')}});
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
// ============ Auto-refresh ============
|
|
622
|
-
var _detailLoading=false,_lastDetailRid=null;
|
|
623
|
-
var _render=render;
|
|
624
|
-
render=function(){
|
|
625
|
-
_render();
|
|
626
|
-
var _rid=S.projects[S.detailIdx]?S.projects[S.detailIdx].remoteId:null;
|
|
627
|
-
if(S.page==='detail'&&_rid&&!_detailLoading&&_lastDetailRid!==_rid){
|
|
628
|
-
_detailLoading=true;_lastDetailRid=_rid;
|
|
629
|
-
Promise.all([loadTree(_rid),loadMcpStatus(_rid),loadMcpAll(_rid),loadPrompts(_rid)]).then(function(){_detailLoading=false;_render()}).catch(function(){_detailLoading=false});
|
|
630
|
-
}
|
|
631
|
-
};
|
|
632
|
-
setInterval(function(){
|
|
633
|
-
if(S.page==='list'){
|
|
634
|
-
api('/api/status').then(function(st){S.projects=st.projects||[];S.hostOk=st.hostConnected;_render()}).catch(function(){});
|
|
635
|
-
}
|
|
636
|
-
},10000);
|
|
637
|
-
|
|
638
|
-
init();
|
|
639
|
-
</script>
|
|
640
|
-
</body>
|
|
641
|
-
</html>`;
|
|
642
|
-
}
|