@ppdocs/mcp 3.2.4 → 3.2.6

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 CHANGED
@@ -346,8 +346,8 @@ function autoRegisterMcp(apiUrl, user, skipGemini = false) {
346
346
  console.log(`✅ Registered MCP for: ${detected.join(', ')}`);
347
347
  return true;
348
348
  }
349
- /** 在指定路径创建或更新 mcp.json 配置 */
350
- function createMcpConfigAt(mcpPath, apiUrl, options) {
349
+ /** 在指定路径创建或更新 mcp.json 配置 (不传 PPDOCS_API_URL,依赖 cwd/.ppdocs 文件) */
350
+ function createMcpConfigAt(mcpPath, _apiUrl, _options) {
351
351
  let mcpConfig = {};
352
352
  if (fs.existsSync(mcpPath)) {
353
353
  try {
@@ -362,24 +362,17 @@ function createMcpConfigAt(mcpPath, apiUrl, options) {
362
362
  // Windows 需要 cmd /c 包装才能执行 npx
363
363
  const isWindows = process.platform === 'win32';
364
364
  // 对于 Mac/Linux,IDE 可能没有用户的完整 PATH,导致找不到 npx
365
- // 使用实际 PATH 值拼接,JSON 中的 $PATH 不会被展开
366
365
  const macExtraPaths = '/usr/local/bin:/opt/homebrew/bin:/home/linuxbrew/.linuxbrew/bin';
367
366
  const actualPath = `${process.env.PATH || '/usr/bin:/bin'}:${macExtraPaths}`;
368
367
  const ppdocsServer = isWindows
369
368
  ? {
370
369
  command: 'cmd',
371
370
  args: ['/c', 'npx', '-y', '@ppdocs/mcp@latest'],
372
- ...(options?.noEnv ? {} : { env: { "PPDOCS_API_URL": apiUrl } })
373
371
  }
374
372
  : {
375
373
  command: 'npx',
376
374
  args: ['-y', '@ppdocs/mcp@latest'],
377
- ...(options?.noEnv ? { env: { "PATH": actualPath } } : {
378
- env: {
379
- "PPDOCS_API_URL": apiUrl,
380
- "PATH": actualPath
381
- }
382
- })
375
+ env: { "PATH": actualPath }
383
376
  };
384
377
  mcpConfig.mcpServers = {
385
378
  ...(mcpConfig.mcpServers || {}),
@@ -76,8 +76,11 @@ export function setProjectStatus(remoteId, status) {
76
76
  export function startWebServer(webPort) {
77
77
  const app = express();
78
78
  app.use(express.json());
79
- // GET / UI
79
+ app.disable('etag'); // 禁用 ETag,避免 304 缓存导致前端数据不更新
80
+ // GET / → UI (禁止浏览器缓存,确保加载最新版本)
80
81
  app.get('/', (_req, res) => {
82
+ res.set('Cache-Control', 'no-store, no-cache, must-revalidate');
83
+ res.set('Pragma', 'no-cache');
81
84
  res.type('html').send(getAgentHtml());
82
85
  });
83
86
  // GET /api/status → 全局+各项目状态
@@ -159,10 +162,7 @@ export function startWebServer(webPort) {
159
162
  // POST /api/auth/start → 发起授权请求 (代理转发到主机)
160
163
  app.post('/api/auth/start', async (req, res) => {
161
164
  const { localDir } = req.body;
162
- if (!localDir) {
163
- res.json({ ok: false, error: '缺少本地目录' });
164
- return;
165
- }
165
+ // localDir 可选,用于 APP 端显示来源信息
166
166
  const config = loadAgentConfig();
167
167
  if (!config?.host) {
168
168
  res.json({ ok: false, error: '请先配置主机' });
@@ -229,8 +229,8 @@ export function startWebServer(webPort) {
229
229
  // POST /api/bind → 绑定项目 (授权通过后调用)
230
230
  app.post('/api/bind', async (req, res) => {
231
231
  const { localDir, remoteId, remoteName, password } = req.body;
232
- if (!localDir || !remoteId) {
233
- res.json({ ok: false, error: '缺少参数' });
232
+ if (!remoteId) {
233
+ res.json({ ok: false, error: '缺少 remoteId' });
234
234
  return;
235
235
  }
236
236
  const config = loadAgentConfig();
@@ -244,16 +244,17 @@ export function startWebServer(webPort) {
244
244
  return;
245
245
  }
246
246
  const binding = {
247
- localDir,
248
- localName: path.basename(localDir),
247
+ localDir: localDir || '',
248
+ localName: localDir ? path.basename(localDir) : (remoteName || remoteId),
249
249
  remote: { id: remoteId, name: remoteName || remoteId, password: password || '' },
250
250
  sync: { enabled: true, intervalSec: 15 },
251
251
  createdAt: new Date().toISOString(),
252
252
  };
253
253
  config.projects.push(binding);
254
254
  saveAgentConfig(config);
255
- // 写入 .ppdocs 到项目目录,供 MCP 直接读取
256
- writePpdocsToProject(binding, config.host, config.port);
255
+ // 有本地目录时才写 .ppdocs
256
+ if (localDir)
257
+ writePpdocsToProject(binding, config.host, config.port);
257
258
  // 通知 Agent 主进程启动同步
258
259
  if (state.onBind)
259
260
  state.onBind(binding);
@@ -452,7 +453,7 @@ export function startWebServer(webPort) {
452
453
  // 获取各平台 MCP 配置的路径和服务器名称
453
454
  const getMcpSpecs = (dir, projId) => ({
454
455
  cursor: { path: path.join(dir, '.cursor', 'mcp.json'), name: 'ppdocs-kg' },
455
- antigravity: { path: path.join(dir, '.gemini', 'settings.json'), name: 'ppdocs-kg' },
456
+ antigravity: { path: path.join(dir, '.gemini', 'settings.json'), name: `ppdocs-kg-${projId}` },
456
457
  vscode: { path: path.join(dir, '.vscode', 'cline_mcp_settings.json'), name: 'ppdocs-kg' },
457
458
  claude: { path: getClaudeGlobalPath(), name: `ppdocs-kg-${projId}` },
458
459
  lobechat: { path: '', name: 'ppdocs-kg' }, // 无本地文件,靠复制配置
@@ -578,18 +579,20 @@ function hasMcpInJson(filePath, serverName) {
578
579
  return false;
579
580
  }
580
581
  }
581
- /** 构建 MCP server 配置对象 */
582
+ /** 构建 MCP server 配置对象 (通过 PPDOCS_API_URL 环境变量隔离项目) */
582
583
  function buildMcpServerConfig(apiUrl, isWindows) {
583
584
  return isWindows ? {
584
585
  command: 'cmd',
585
586
  args: ['/c', 'npx', '-y', '@ppdocs/mcp@latest'],
586
- env: { PPDOCS_API_URL: apiUrl },
587
+ env: {
588
+ PPDOCS_API_URL: apiUrl,
589
+ },
587
590
  } : {
588
591
  command: 'npx',
589
592
  args: ['-y', '@ppdocs/mcp@latest'],
590
593
  env: {
591
- PPDOCS_API_URL: apiUrl,
592
594
  PATH: `${process.env.PATH || '/usr/bin:/bin'}:/usr/local/bin:/opt/homebrew/bin`,
595
+ PPDOCS_API_URL: apiUrl,
593
596
  },
594
597
  };
595
598
  }
package/dist/web/ui.js CHANGED
@@ -3,472 +3,486 @@
3
3
  * 页面: 项目列表 / 添加项目 / 项目详情 (标签页: 概览/MCP/提示词)
4
4
  */
5
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-family:'Cascadia Code','Fira Code',monospace;font-size:12px;color:#94a3b8;max-height:280px;overflow-y:auto;line-height:1.7;white-space:pre}
45
- .platform-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px}
46
- .platform-card{padding:14px;border-radius:10px;border:1px solid rgba(100,116,139,.2);text-align:center;transition:all .2s}
47
- .platform-card:hover{border-color:#a78bfa;background:rgba(167,139,250,.08)}
48
- .platform-card .icon{font-size:22px;margin-bottom:4px}
49
- .platform-card .name{font-size:13px;font-weight:600}
50
- .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)}
51
- .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}
52
- .modal h3{font-size:16px;margin-bottom:16px}
53
- .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}
54
- .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}
55
- .toast.show{opacity:1;transform:translateY(0)}
56
- .toast.ok{background:rgba(34,197,94,.2);border:1px solid #22c55e;color:#22c55e}
57
- .toast.err{background:rgba(239,68,68,.2);border:1px solid #ef4444;color:#ef4444}
58
- .empty{text-align:center;padding:40px;color:#475569;font-size:14px}
59
- .back{font-size:13px;color:#64748b;cursor:pointer;margin-bottom:16px;display:inline-block}
60
- .back:hover{color:#94a3b8}
61
- .row{display:flex;gap:12px}
62
- .row>*{flex:1}
63
- .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}
64
- .drop-zone.over{border-color:#60a5fa;background:rgba(96,165,250,.06);color:#60a5fa}
65
- .auth-badge{font-size:12px;padding:3px 10px;border-radius:12px;font-weight:600;display:inline-flex;align-items:center;gap:4px}
66
- .auth-badge.ok{background:rgba(34,197,94,.15);color:#22c55e}
67
- .auth-badge.no{background:rgba(245,158,11,.15);color:#f59e0b}
68
- .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}
69
- .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}
70
- .mcp-list-item .badges{display:flex;gap:4px}
71
- .mcp-list-item .badges span{font-size:10px;padding:2px 6px;border-radius:8px;background:rgba(96,165,250,.15);color:#60a5fa}
72
- .tab-row{display:flex;gap:0;margin-bottom:16px;border-bottom:1px solid rgba(100,116,139,.2)}
73
- .tab{padding:8px 16px;font-size:13px;font-weight:600;color:#64748b;cursor:pointer;border-bottom:2px solid transparent;transition:all .2s}
74
- .tab.active{color:#60a5fa;border-color:#60a5fa}
75
- @keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
76
- </style>
77
- </head>
78
- <body>
79
- <div class="app" id="app"></div>
80
- <div class="toast" id="toast"></div>
81
- <script>
82
- // ============ 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};
84
-
85
- // ============ Router ============
86
- function go(page,data){Object.assign(S,data||{});S.page=page;_lastDetailRid=null;cancelAuth();render()}
87
-
88
- // ============ Toast ============
89
- 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)}
90
-
91
- // ============ API ============
92
- function api(path,opts){return fetch(path,opts).then(function(r){return r.json()})}
93
-
94
- // ============ Init ============
95
- function init(){
96
- api('/api/status').then(function(st){
97
- S.host=st.host||'';S.port=st.port||20001;S.hostOk=st.hostConnected;S.projects=st.projects||[];
98
- render();
99
- }).catch(function(){render()});
100
- // Drag-drop
101
- document.body.addEventListener('dragover',function(e){e.preventDefault();e.dataTransfer.dropEffect='copy';var dz=document.querySelector('.drop-zone');if(dz)dz.classList.add('over')});
102
- document.body.addEventListener('dragleave',function(){var dz=document.querySelector('.drop-zone');if(dz)dz.classList.remove('over')});
103
- document.body.addEventListener('drop',function(e){
104
- e.preventDefault();
105
- var dz=document.querySelector('.drop-zone');if(dz)dz.classList.remove('over');
106
- var items=e.dataTransfer.items;
107
- for(var i=0;i<items.length;i++){
108
- var entry=items[i].webkitGetAsEntry&&items[i].webkitGetAsEntry();
109
- if(entry&&entry.isDirectory){
110
- go('add',{addStep:1,addDir:entry.name});
111
- toast('请补全绝对路径,例如 D:/projects/'+entry.name);
112
- return;
113
- }
114
- }
115
- });
116
- }
117
-
118
- // ============ Render ============
119
- function render(){
120
- var app=document.getElementById('app');
121
- if(!S.host&&S.page!=='setup'){S.page='setup'}
122
- switch(S.page){
123
- case 'setup':app.innerHTML=renderSetup();break;
124
- case 'list':app.innerHTML=renderList();break;
125
- case 'add':app.innerHTML=renderAdd();break;
126
- case 'detail':app.innerHTML=renderDetail();break;
127
- }
128
- }
129
-
130
- // ============ Setup ============
131
- function renderSetup(){
132
- return '<div class="header"><h1>📡 PPDocs Agent</h1></div>'+
133
- '<div class="card no-hover"><div class="section"><h2>🏠 主机连接</h2>'+
134
- '<div class="row"><div class="fg"><label>主机 IP</label><input id="iHost" value="'+S.host+'" placeholder="10.0.0.176"></div>'+
135
- '<div class="fg" style="max-width:120px"><label>端口</label><input id="iPort" type="number" value="'+S.port+'" placeholder="20001"></div></div>'+
136
- '<div class="btn-row"><button class="btn btn-p" onclick="doTestHost()">🔍 测试连接</button>'+
137
- '<span id="hostResult" style="font-size:13px;line-height:32px;margin-left:8px"></span></div></div></div>';
138
- }
139
-
140
- function doTestHost(){
141
- var host=document.getElementById('iHost').value.trim();
142
- var port=parseInt(document.getElementById('iPort').value)||20001;
143
- var el=document.getElementById('hostResult');
144
- el.textContent='连接中...';
145
- api('/api/test',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({host:host,port:port})}).then(function(r){
146
- if(r.ok){
147
- el.innerHTML='<span style="color:#22c55e">✅ 连接成功</span>';
148
- api('/api/host',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({host:host,port:port})});
149
- S.host=host;S.port=port;S.hostOk=true;
150
- setTimeout(function(){go('list')},800);
151
- }else{
152
- el.innerHTML='<span style="color:#ef4444">❌ '+(r.error||'连接失败')+'</span>';
153
- }
154
- });
155
- }
156
-
157
- // ============ Project List ============
158
- function renderList(){
159
- var badge=S.hostOk?'<span class="host-badge">🟢 '+S.host+':'+S.port+'</span>':'<span class="host-badge off">🔴 未连接</span>';
160
- var cards='';
161
- if(S.projects.length===0){
162
- cards='<div class="empty">暂无项目,点击上方按钮添加</div>';
163
- }else{
164
- cards=S.projects.map(function(p,i){
165
- var dot=p.connected?'<span class="sync-dot on"></span>':'<span class="sync-dot off"></span>';
166
- var sync=p.lastSync?'同步: '+p.lastSync:p.syncStatus||'—';
167
- return '<div class="card" onclick="go(\\x27detail\\x27,{detailIdx:'+i+',detailTab:\\x27overview\\x27})">'+
168
- '<h3>'+dot+' 📂 '+(p.localName||p.remoteName)+'</h3>'+
169
- '<div class="meta">'+(p.localDir||'')+'</div>'+
170
- '<div class="stat"><span>远程: '+p.remoteName+' ('+p.remoteId+')</span><span>'+(p.docCount?p.docCount+' 文档':'')+'</span></div>'+
171
- '<div class="meta">'+sync+'</div></div>';
172
- }).join('');
173
- }
174
- return '<div class="header"><h1>📡 PPDocs Agent</h1>'+badge+'</div>'+
175
- '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">'+
176
- '<span style="font-size:15px;font-weight:600">我的项目 ('+S.projects.length+')</span>'+
177
- '<div class="btn-row" style="margin:0"><button class="btn btn-p" onclick="go(\\x27add\\x27,{addStep:1,addDir:\\x27\\x27})">➕ 添加项目</button>'+
178
- '<button class="btn btn-s" onclick="go(\\x27setup\\x27)">⚙️</button></div></div>'+
179
- cards+
180
- '<div class="drop-zone">📂 拖拽项目文件夹到此处快速添加</div>';
181
- }
182
-
183
- // ============ Add Project ============
184
- function renderAdd(){
185
- if(S.addStep===1) return renderAddStep1();
186
- return renderAddStep2();
187
- }
188
-
189
- function renderAddStep1(){
190
- var dirName=S.addDir?S.addDir.replace(/\\\\/g,'/').split('/').filter(Boolean).pop():'';
191
- var namePreview=dirName?'<div style="font-size:12px;color:#60a5fa;margin:-8px 0 12px">📂 项目名: <strong>'+dirName+'</strong></div>':'';
192
- return '<span class="back" onclick="cancelAuth();go(\\x27list\\x27)">← 返回项目列表</span>'+
193
- '<div class="card no-hover"><div class="section"><h2>➕ 添加项目 — Step 1/2</h2>'+
194
- '<p style="font-size:13px;color:#64748b;margin-bottom:12px">指定本地项目目录,以目录名作为项目名称</p>'+
195
- '<div class="fg"><label>本地项目目录 (绝对路径)</label><input id="iDir" value="'+S.addDir+'" placeholder="例: D:/projects/my-app" oninput="S.addDir=this.value;_render()"></div>'+
196
- namePreview+
197
- '<div class="btn-row"><button class="btn btn-p" onclick="doAuthStart()">📡 发送授权请求</button></div>'+
198
- '</div></div>';
199
- }
200
-
201
- function doAuthStart(){
202
- var dir=document.getElementById('iDir').value.trim();
203
- if(!dir){toast('请填写目录路径','err');return}
204
- if(!/^[A-Za-z]:[\\\\\\/]|^\\//.test(dir)){toast('请输入绝对路径','err');return}
205
- S.addDir=dir;
206
- api('/api/auth/start',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({localDir:dir})}).then(function(r){
207
- if(!r.ok){toast(r.error||'授权请求失败','err');return}
208
- S.authRequestId=r.requestId;S.authStartTime=Date.now();S.authStatus='pending';S.addStep=2;
209
- render();startAuthPoll();
210
- });
211
- }
212
-
213
- var authPollTimer=null,authUiTimer=null;
214
- function startAuthPoll(){
215
- cancelAuth();
216
- authPollTimer=setInterval(function(){
217
- if(!S.authRequestId){cancelAuth();return}
218
- var elapsed=Math.floor((Date.now()-S.authStartTime)/1000);
219
- if(elapsed>=300){S.authStatus='expired';cancelAuth();_render();return}
220
- api('/api/auth/status/'+S.authRequestId).then(function(r){
221
- S.authStatus=r.status;
222
- if(r.status==='approved'){
223
- cancelAuth();
224
- api('/api/bind',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({
225
- localDir:S.addDir,remoteId:r.projectId,remoteName:r.projectName,password:r.password
226
- })}).then(function(br){
227
- if(br.ok){
228
- S.authResult={name:r.projectName,id:r.projectId};_render();
229
- toast('✅ 授权成功,已绑定 '+r.projectName);
230
- setTimeout(function(){init();go('list')},2000);
231
- }else{toast(br.error||'绑定失败','err')}
232
- });
233
- }else if(r.status==='rejected'){cancelAuth();_render()}
234
- else{_render()}
235
- }).catch(function(){});
236
- },2000);
237
- authUiTimer=setInterval(function(){if(S.page==='add'&&S.addStep===2&&S.authStatus==='pending')_render()},1000);
238
- }
239
- function cancelAuth(){if(authPollTimer){clearInterval(authPollTimer);authPollTimer=null}if(authUiTimer){clearInterval(authUiTimer);authUiTimer=null}}
240
-
241
- function renderAddStep2(){
242
- var elapsed=Math.floor((Date.now()-(S.authStartTime||Date.now()))/1000);
243
- var pct=Math.min(100,Math.round(elapsed/300*100));
244
- if(S.authStatus==='approved'&&S.authResult){
245
- return '<div class="card no-hover" style="text-align:center;padding:40px">'+
246
- '<div style="font-size:40px;margin-bottom:12px">✅</div>'+
247
- '<h2 style="color:#22c55e;font-size:18px;margin-bottom:8px">授权成功!</h2>'+
248
- '<p style="color:#94a3b8;font-size:14px">项目: '+S.authResult.name+' ('+S.authResult.id+')</p>'+
249
- '<p style="color:#64748b;font-size:13px;margin-top:4px">权限: 读写 | 2秒后跳转...</p></div>';
250
- }
251
- if(S.authStatus==='rejected'){
252
- return '<span class="back" onclick="go(\\x27list\\x27)">← 返回</span>'+
253
- '<div class="card no-hover" style="text-align:center;padding:40px">'+
254
- '<div style="font-size:40px;margin-bottom:12px">❌</div>'+
255
- '<h2 style="color:#ef4444;font-size:18px;margin-bottom:8px">授权被拒绝</h2>'+
256
- '<div class="btn-row" style="justify-content:center;margin-top:16px"><button class="btn btn-p" onclick="S.addStep=1;render()">重试</button></div></div>';
257
- }
258
- if(S.authStatus==='expired'){
259
- return '<span class="back" onclick="go(\\x27list\\x27)">← 返回</span>'+
260
- '<div class="card no-hover" style="text-align:center;padding:40px">'+
261
- '<div style="font-size:40px;margin-bottom:12px">⏰</div>'+
262
- '<h2 style="color:#f59e0b;font-size:18px;margin-bottom:8px">授权已超时</h2>'+
263
- '<div class="btn-row" style="justify-content:center;margin-top:16px"><button class="btn btn-p" onclick="S.addStep=1;render()">重试</button></div></div>';
264
- }
265
- return '<span class="back" onclick="cancelAuth();S.addStep=1;render()">← 返回</span>'+
266
- '<div class="card no-hover"><div class="section"><h2>➕ 添加项目 — Step 2/2</h2>'+
267
- '<div style="text-align:center;padding:20px 0">'+
268
- '<div style="font-size:36px;margin-bottom:10px;animation:pulse 2s infinite">📡</div>'+
269
- '<p style="font-size:15px;font-weight:600;color:#60a5fa;margin-bottom:6px">等待主机端授权...</p>'+
270
- '<p style="font-size:12px;color:#64748b;margin-bottom:4px">已向 '+S.host+':'+S.port+' 发送授权请求</p>'+
271
- '<p style="font-size:12px;color:#475569">请在主机端 PPDocs 桌面应用中选择项目并批准</p>'+
272
- '<div style="margin:16px auto;width:80%;height:4px;background:rgba(100,116,139,.2);border-radius:2px;overflow:hidden">'+
273
- '<div style="width:'+pct+'%;height:100%;background:linear-gradient(90deg,#3b82f6,#6366f1);border-radius:2px;transition:width .5s"></div></div>'+
274
- '<p style="font-size:11px;color:#475569">'+elapsed+'s / 300s</p>'+
275
- '</div><div class="btn-row" style="justify-content:center"><button class="btn btn-s" onclick="cancelAuth();go(\\x27list\\x27)">取消</button></div></div></div>';
276
- }
277
-
278
- // ============ Project Detail ============
279
- function renderDetail(){
280
- var p=S.projects[S.detailIdx];
281
- if(!p) return '<span class="back" onclick="go(\\x27list\\x27)">← 项目不存在</span>';
282
- var tab=S.detailTab||'overview';
283
- var tabs=[{key:'overview',label:'📊 概览'},{key:'mcp',label:'🤖 MCP'},{key:'prompts',label:'📝 提示词'}];
284
- 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('');
285
- var tabContent='';
286
- if(tab==='overview') tabContent=renderOverview(p);
287
- else if(tab==='mcp') tabContent=renderMcpTab(p);
288
- else if(tab==='prompts') tabContent=renderPromptsTab(p);
289
- return '<span class="back" onclick="go(\\x27list\\x27)">← 返回项目列表</span>'+
290
- '<div class="header"><h1>📂 '+(p.localName||p.remoteName)+'</h1></div>'+
291
- '<div class="tab-row">'+tabHtml+'</div>'+tabContent+
292
- '<div class="card no-hover" style="border-color:rgba(239,68,68,.2)"><div class="section"><h2>⚠️ 危险区域</h2>'+
293
- '<button class="btn btn-d" onclick="doUnbind(\\x27'+p.remoteId+'\\x27)">🗑️ 解除绑定</button></div></div>';
294
- }
295
-
296
- function renderOverview(p){
297
- var authBadge=p.hasPassword?'<span class="auth-badge ok">✅ 已授权 (读写)</span>':'<span class="auth-badge no">⚠️ 未授权 (只读)</span>';
298
- 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
- return '<div class="card no-hover"><div class="section"><h2>📊 概览</h2>'+
300
- '<div class="stat"><span>本地: '+(p.localDir||'<em style="color:#ef4444">未指定</em>')+'</span></div>'+
301
- '<div class="stat"><span>远程: '+p.remoteName+' ('+p.remoteId+')</span></div>'+
302
- '<div class="stat"><span>授权: '+authBadge+authBtn+'</span></div>'+
303
- '<div class="stat"><span>文档: '+(p.docCount||0)+' 个</span></div>'+
304
- '<div class="stat"><span>同步: '+(p.connected?'🟢 运行中':'🔴 '+p.syncStatus)+' '+(p.lastSync?'('+p.lastSync+')':'')+'</span></div>'+
305
- '</div></div>'+
306
- '<div class="card no-hover"><div class="section"><h2>📚 知识图谱</h2>'+
307
- '<div class="tree-box" id="treeBox">加载中...</div></div></div>';
308
- }
309
-
310
- function renderMcpTab(p){
311
- var mcpState=S.mcpStatus||{};
312
- var platforms=[
313
- {key:'cursor',icon:'🟣',name:'Cursor'},
314
- {key:'antigravity',icon:'🟢',name:'Antigravity'},
315
- {key:'vscode',icon:'🔵',name:'OpenCode'},
316
- {key:'claude',icon:'🟠',name:'Claude'},
317
- {key:'lobechat',icon:'🦞',name:'LobeChat'}
318
- ];
319
- var platformCards=platforms.map(function(pl){
320
- var isManual=pl.key==='lobechat';
321
- var installed=mcpState[pl.key];
322
- var badge='',btn='';
323
- if(isManual){
324
- badge='<span style="font-size:11px;color:#94a3b8;display:block;margin:4px 0">手动配置</span>';
325
- 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>';
326
- }else{
327
- 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>';
328
- 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>';
329
- }
330
- return '<div class="platform-card"><div class="icon">'+pl.icon+'</div><div class="name" style="white-space:nowrap">'+pl.name+'</div>'+badge+btn+'</div>';
331
- }).join('');
332
- var noDir=!p.localDir;
333
- var mcpListHtml='';
334
- if(S.mcpAll&&S.mcpAll.length>0){
335
- mcpListHtml=S.mcpAll.map(function(s){
336
- var badges=s.platforms.map(function(pl){return '<span>'+pl+'</span>'}).join('');
337
- return '<div class="mcp-list-item"><span>📦 '+s.name+'</span><div class="badges">'+badges+'</div></div>';
338
- }).join('');
339
- }else{
340
- mcpListHtml='<div style="font-size:12px;color:#475569;padding:8px 0">未检测到任何 MCP 服务</div>';
341
- }
342
- return '<div class="card no-hover"><div class="section"><h2>🤖 PPDocs MCP 安装</h2>'+
343
- (noDir?'<div style="font-size:13px;color:#f59e0b;margin-bottom:8px">⚠️ 未指定本地目录</div>':'')+
344
- '<div class="platform-grid">'+platformCards+'</div></div></div>'+
345
- '<div class="card no-hover"><div class="section"><h2>📦 项目内全部 MCP ('+(S.mcpAll?S.mcpAll.length:0)+')</h2>'+
346
- mcpListHtml+'</div></div>';
347
- }
348
-
349
- function renderPromptsTab(p){
350
- if(!p.localDir) return '<div class="card no-hover"><div class="section"><h2>📝 提示词</h2><div style="font-size:13px;color:#f59e0b">⚠️ 未指定本地目录</div></div></div>';
351
- var files=S.prompts||[];
352
- var cards=files.map(function(f){
353
- var statusBadge=f.exists?'<span style="font-size:11px;color:#22c55e">'+f.lines+' 行</span>':'<span style="font-size:11px;color:#64748b">不存在</span>';
354
- var btn=f.exists
355
- ?'<button class="btn btn-s" style="font-size:11px;padding:3px 8px" onclick="doViewPrompt(\\x27'+p.remoteId+'\\x27,\\x27'+f.name+'\\x27)">查看/编辑</button>'
356
- :'<button class="btn btn-p" style="font-size:11px;padding:3px 8px" onclick="doCreatePrompt(\\x27'+p.remoteId+'\\x27,\\x27'+f.name+'\\x27)">创建</button>';
357
- return '<div class="mcp-list-item"><span>📄 '+f.name+'</span><div style="display:flex;gap:8px;align-items:center">'+statusBadge+btn+'</div></div>';
358
- }).join('');
359
- return '<div class="card no-hover"><div class="section"><h2>📝 系统提示词</h2>'+(cards||'<div style="font-size:12px;color:#475569">加载中...</div>')+'</div></div>';
360
- }
361
-
362
- // ============ Data Loading ============
363
- function loadTree(rid){
364
- var box=document.getElementById('treeBox');if(!box)return Promise.resolve();
365
- return api('/api/bind/'+rid+'/tree').then(function(r){
366
- var prefix=r.readOnly?'🔒 只读模式\\n\\n':'';
367
- box.textContent=r.tree?(prefix+(r.docCount>0?'共 '+r.docCount+' 文档, '+r.dirCount+' 目录\\n\\n':'')+r.tree):'知识库为空';
368
- }).catch(function(e){box.textContent='加载失败: '+e});
369
- }
370
- function loadMcpStatus(rid){return api('/api/bind/'+rid+'/mcp').then(function(r){S.mcpStatus=r.platforms||{}}).catch(function(){S.mcpStatus={}})}
371
- function loadMcpAll(rid){return api('/api/bind/'+rid+'/mcp/all').then(function(r){S.mcpAll=r.servers||[]}).catch(function(){S.mcpAll=[]})}
372
- function loadPrompts(rid){return api('/api/bind/'+rid+'/prompts').then(function(r){S.prompts=r.files||[]}).catch(function(){S.prompts=[]})}
373
-
374
- // ============ Auth from Detail ============
375
- function doReAuth(remoteId,localDir){
376
- cancelAuth();
377
- api('/api/auth/start',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({localDir:localDir})}).then(function(r){
378
- if(!r.ok){toast(r.error||'授权请求失败','err');return}
379
- toast('📡 已发送授权请求,请在主机端批准');
380
- S.authRequestId=r.requestId;S.authStartTime=Date.now();
381
- authPollTimer=setInterval(function(){
382
- var elapsed=Math.floor((Date.now()-S.authStartTime)/1000);
383
- if(elapsed>=300){cancelAuth();toast('⏰ 授权已超时','err');return}
384
- api('/api/auth/status/'+S.authRequestId).then(function(sr){
385
- if(sr.status==='approved'){cancelAuth();
386
- api('/api/bind/'+remoteId,{method:'PATCH',headers:{'Content-Type':'application/json'},body:JSON.stringify({password:sr.password,remoteName:sr.projectName})}).then(function(){
387
- toast('✅ 授权成功!');_lastDetailRid=null;init();
388
- });
389
- }else if(sr.status==='rejected'){cancelAuth();toast(' 授权被拒绝','err')}
390
- else if(sr.status==='expired'){cancelAuth();toast('⏰ 授权已超时','err')}
391
- }).catch(function(){});
392
- },2000);
393
- });
394
- }
395
-
396
- // ============ MCP ============
397
- function doMcpInstall(remoteId,platform){
398
- api('/api/bind/'+remoteId+'/mcp',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({platform:platform})}).then(function(r){
399
- if(r.ok){
400
- if(r.action==='copy'){S.modal={title:platform.toUpperCase()+' MCP 配置',content:r.config};renderModal()}
401
- else{toast(r.message||'已安装/更新');loadMcpStatus(remoteId).then(function(){return loadMcpAll(remoteId)}).then(function(){_render()})}
402
- }else{toast(r.error||'操作失败','err')}
403
- });
404
- }
405
- function renderModal(){
406
- if(!S.modal)return;var div=document.createElement('div');div.className='modal-bg';
407
- div.innerHTML='<div class="modal"><h3>'+S.modal.title+'</h3><div class="cmd-box" id="mcpCopyBox">'+S.modal.content+'</div>'+
408
- '<div class="btn-row"><button class="btn btn-p" onclick="copyMcpConfig()">📋 复制</button>'+
409
- '<button class="btn btn-s" onclick="closeModal()">关闭</button></div></div>';
410
- document.body.appendChild(div);div.addEventListener('click',function(e){if(e.target===div)div.remove()});
411
- }
412
- function closeModal(){var bg=document.querySelector('.modal-bg');if(bg)bg.remove();S.modal=null}
413
- function copyMcpConfig(){var el=document.getElementById('mcpCopyBox');if(el){navigator.clipboard.writeText(el.textContent);toast('已复制')}}
414
- function doMcpUninstall(remoteId,platform){
415
- if(!confirm('确定卸载 '+platform+' 的 MCP 配置?'))return;
416
- api('/api/bind/'+remoteId+'/mcp',{method:'DELETE',headers:{'Content-Type':'application/json'},body:JSON.stringify({platform:platform})}).then(function(r){
417
- if(r.ok){toast(r.message||'已卸载');loadMcpStatus(remoteId).then(function(){return loadMcpAll(remoteId)}).then(function(){_render()})}
418
- else{toast(r.error||'卸载失败','err')}
419
- });
420
- }
421
-
422
- // ============ Prompts ============
423
- function doViewPrompt(remoteId,name){
424
- api('/api/bind/'+remoteId+'/prompts/'+encodeURIComponent(name)).then(function(r){
425
- if(!r.ok){toast(r.error||'读取失败','err');return}
426
- S.modal={title:'📝 '+name,content:r.content,remoteId:remoteId,fileName:name};renderPromptEditor();
427
- });
428
- }
429
- function doCreatePrompt(remoteId,name){S.modal={title:'📝 创建 '+name,content:'',remoteId:remoteId,fileName:name};renderPromptEditor()}
430
- function renderPromptEditor(){
431
- if(!S.modal)return;var div=document.createElement('div');div.className='modal-bg';
432
- var escaped=(S.modal.content||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
433
- div.innerHTML='<div class="modal" style="max-width:650px"><h3>'+S.modal.title+'</h3>'+
434
- '<textarea class="prompt-editor" id="promptEditor">'+escaped+'</textarea>'+
435
- '<div class="btn-row"><button class="btn btn-p" onclick="doSavePrompt()">💾 保存</button>'+
436
- '<button class="btn btn-s" onclick="closeModal()">关闭</button></div></div>';
437
- document.body.appendChild(div);div.addEventListener('click',function(e){if(e.target===div)div.remove()});
438
- }
439
- function doSavePrompt(){
440
- var content=document.getElementById('promptEditor').value;
441
- 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){
442
- if(r.ok){toast('✅ 已保存');var rid=S.modal.remoteId;closeModal();loadPrompts(rid).then(function(){_render()})}
443
- else{toast(r.error||'保存失败','err')}
444
- });
445
- }
446
-
447
- // ============ Unbind ============
448
- function doUnbind(remoteId){
449
- if(!confirm('确定解除绑定?'))return;
450
- api('/api/bind/'+remoteId,{method:'DELETE'}).then(function(r){if(r.ok){toast('已解绑');init();go('list')}else{toast('解绑失败','err')}});
451
- }
452
-
453
- // ============ Auto-refresh ============
454
- var _detailLoading=false,_lastDetailRid=null;
455
- var _render=render;
456
- render=function(){
457
- _render();
458
- var _rid=S.projects[S.detailIdx]?S.projects[S.detailIdx].remoteId:null;
459
- if(S.page==='detail'&&_rid&&!_detailLoading&&_lastDetailRid!==_rid){
460
- _detailLoading=true;_lastDetailRid=_rid;
461
- Promise.all([loadTree(_rid),loadMcpStatus(_rid),loadMcpAll(_rid),loadPrompts(_rid)]).then(function(){_detailLoading=false;_render()}).catch(function(){_detailLoading=false});
462
- }
463
- };
464
- setInterval(function(){
465
- if(S.page==='list'){
466
- api('/api/status').then(function(st){S.projects=st.projects||[];S.hostOk=st.hostConnected;_render()}).catch(function(){});
467
- }
468
- },10000);
469
-
470
- init();
471
- </script>
472
- </body>
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-family:'Cascadia Code','Fira Code',monospace;font-size:12px;color:#94a3b8;max-height:280px;overflow-y:auto;line-height:1.7;white-space:pre}
45
+ .platform-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px}
46
+ .platform-card{padding:14px;border-radius:10px;border:1px solid rgba(100,116,139,.2);text-align:center;transition:all .2s}
47
+ .platform-card:hover{border-color:#a78bfa;background:rgba(167,139,250,.08)}
48
+ .platform-card .icon{font-size:22px;margin-bottom:4px}
49
+ .platform-card .name{font-size:13px;font-weight:600}
50
+ .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)}
51
+ .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}
52
+ .modal h3{font-size:16px;margin-bottom:16px}
53
+ .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}
54
+ .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}
55
+ .toast.show{opacity:1;transform:translateY(0)}
56
+ .toast.ok{background:rgba(34,197,94,.2);border:1px solid #22c55e;color:#22c55e}
57
+ .toast.err{background:rgba(239,68,68,.2);border:1px solid #ef4444;color:#ef4444}
58
+ .empty{text-align:center;padding:40px;color:#475569;font-size:14px}
59
+ .back{font-size:13px;color:#64748b;cursor:pointer;margin-bottom:16px;display:inline-block}
60
+ .back:hover{color:#94a3b8}
61
+ .row{display:flex;gap:12px}
62
+ .row>*{flex:1}
63
+ .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}
64
+ .drop-zone.over{border-color:#60a5fa;background:rgba(96,165,250,.06);color:#60a5fa}
65
+ .auth-badge{font-size:12px;padding:3px 10px;border-radius:12px;font-weight:600;display:inline-flex;align-items:center;gap:4px}
66
+ .auth-badge.ok{background:rgba(34,197,94,.15);color:#22c55e}
67
+ .auth-badge.no{background:rgba(245,158,11,.15);color:#f59e0b}
68
+ .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}
69
+ .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}
70
+ .mcp-list-item .badges{display:flex;gap:4px}
71
+ .mcp-list-item .badges span{font-size:10px;padding:2px 6px;border-radius:8px;background:rgba(96,165,250,.15);color:#60a5fa}
72
+ .tab-row{display:flex;gap:0;margin-bottom:16px;border-bottom:1px solid rgba(100,116,139,.2)}
73
+ .tab{padding:8px 16px;font-size:13px;font-weight:600;color:#64748b;cursor:pointer;border-bottom:2px solid transparent;transition:all .2s}
74
+ .tab.active{color:#60a5fa;border-color:#60a5fa}
75
+ @keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
76
+ </style>
77
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
78
+ <meta http-equiv="Pragma" content="no-cache">
79
+ <meta http-equiv="Expires" content="0">
80
+ </head>
81
+ <body>
82
+ <div class="app" id="app"></div>
83
+ <div class="toast" id="toast"></div>
84
+ <script>
85
+ // ============ State ============
86
+ 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};
87
+
88
+ // ============ Router ============
89
+ function go(page,data){Object.assign(S,data||{});S.page=page;_lastDetailRid=null;cancelAuth();render()}
90
+
91
+ // ============ Toast ============
92
+ 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)}
93
+
94
+ // ============ API ============
95
+ function api(path,opts){return fetch(path,opts).then(function(r){return r.json()}).then(function(d){console.log('[api]',path,d);return d})}
96
+
97
+ // ============ Init ============
98
+ function init(){
99
+ api('/api/status').then(function(st){
100
+ S.host=st.host||'';S.port=st.port||20001;S.hostOk=st.hostConnected;S.projects=st.projects||[];
101
+ render();
102
+ }).catch(function(){render()});
103
+ // Drag-drop
104
+ document.body.addEventListener('dragover',function(e){e.preventDefault();e.dataTransfer.dropEffect='copy';var dz=document.querySelector('.drop-zone');if(dz)dz.classList.add('over')});
105
+ document.body.addEventListener('dragleave',function(){var dz=document.querySelector('.drop-zone');if(dz)dz.classList.remove('over')});
106
+ document.body.addEventListener('drop',function(e){
107
+ e.preventDefault();
108
+ var dz=document.querySelector('.drop-zone');if(dz)dz.classList.remove('over');
109
+ var items=e.dataTransfer.items;
110
+ for(var i=0;i<items.length;i++){
111
+ var entry=items[i].webkitGetAsEntry&&items[i].webkitGetAsEntry();
112
+ if(entry&&entry.isDirectory){
113
+ go('add',{addStep:1,addDir:entry.name});
114
+ toast('📂 检测到 '+entry.name+',可直接发送授权或补全路径');
115
+ return;
116
+ }
117
+ }
118
+ });
119
+ }
120
+
121
+ // ============ Render ============
122
+ function render(){
123
+ var app=document.getElementById('app');
124
+ if(!S.host&&S.page!=='setup'){S.page='setup'}
125
+ switch(S.page){
126
+ case 'setup':app.innerHTML=renderSetup();break;
127
+ case 'list':app.innerHTML=renderList();break;
128
+ case 'add':app.innerHTML=renderAdd();break;
129
+ case 'detail':app.innerHTML=renderDetail();break;
130
+ }
131
+ }
132
+
133
+ // ============ Setup ============
134
+ function renderSetup(){
135
+ return '<div class="header"><h1>📡 PPDocs Agent</h1></div>'+
136
+ '<div class="card no-hover"><div class="section"><h2>🏠 主机连接</h2>'+
137
+ '<div class="row"><div class="fg"><label>主机 IP</label><input id="iHost" value="'+S.host+'" placeholder="10.0.0.176"></div>'+
138
+ '<div class="fg" style="max-width:120px"><label>端口</label><input id="iPort" type="number" value="'+S.port+'" placeholder="20001"></div></div>'+
139
+ '<div class="btn-row"><button class="btn btn-p" onclick="doTestHost()">🔍 测试连接</button>'+
140
+ '<span id="hostResult" style="font-size:13px;line-height:32px;margin-left:8px"></span></div></div></div>';
141
+ }
142
+
143
+ function doTestHost(){
144
+ var host=document.getElementById('iHost').value.trim();
145
+ var port=parseInt(document.getElementById('iPort').value)||20001;
146
+ var el=document.getElementById('hostResult');
147
+ el.textContent='连接中...';
148
+ api('/api/test',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({host:host,port:port})}).then(function(r){
149
+ if(r.ok){
150
+ el.innerHTML='<span style="color:#22c55e">✅ 连接成功</span>';
151
+ api('/api/host',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({host:host,port:port})});
152
+ S.host=host;S.port=port;S.hostOk=true;
153
+ setTimeout(function(){go('list')},800);
154
+ }else{
155
+ el.innerHTML='<span style="color:#ef4444">❌ '+(r.error||'连接失败')+'</span>';
156
+ }
157
+ });
158
+ }
159
+
160
+ // ============ Project List ============
161
+ function renderList(){
162
+ var badge=S.hostOk?'<span class="host-badge">🟢 '+S.host+':'+S.port+'</span>':'<span class="host-badge off">🔴 未连接</span>';
163
+ var cards='';
164
+ if(S.projects.length===0){
165
+ cards='<div class="empty">暂无项目,点击上方按钮添加</div>';
166
+ }else{
167
+ cards=S.projects.map(function(p,i){
168
+ var dot=p.connected?'<span class="sync-dot on"></span>':'<span class="sync-dot off"></span>';
169
+ var sync=p.lastSync?'同步: '+p.lastSync:p.syncStatus||'';
170
+ return '<div class="card" onclick="go(\\x27detail\\x27,{detailIdx:'+i+',detailTab:\\x27overview\\x27})">'+
171
+ '<h3>'+dot+' 📂 '+(p.localName||p.remoteName)+'</h3>'+
172
+ '<div class="meta">'+(p.localDir||'')+'</div>'+
173
+ '<div class="stat"><span>远程: '+p.remoteName+' ('+p.remoteId+')</span><span>'+(p.docCount?p.docCount+' 文档':'')+'</span></div>'+
174
+ '<div class="meta">'+sync+'</div></div>';
175
+ }).join('');
176
+ }
177
+ return '<div class="header"><h1>📡 PPDocs Agent</h1>'+badge+'</div>'+
178
+ '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">'+
179
+ '<span style="font-size:15px;font-weight:600">我的项目 ('+S.projects.length+')</span>'+
180
+ '<div class="btn-row" style="margin:0"><button class="btn btn-p" onclick="go(\\x27add\\x27,{addStep:1,addDir:\\x27\\x27})">➕ 添加项目</button>'+
181
+ '<button class="btn btn-s" onclick="go(\\x27setup\\x27)">⚙️</button></div></div>'+
182
+ cards+
183
+ '<div class="drop-zone">📂 拖拽项目文件夹到此处快速添加</div>';
184
+ }
185
+
186
+ // ============ Add Project ============
187
+ function renderAdd(){
188
+ if(S.addStep===1) return renderAddStep1();
189
+ return renderAddStep2();
190
+ }
191
+
192
+ function renderAddStep1(){
193
+ return '<span class="back" onclick="cancelAuth();go(\\x27list\\x27)">← 返回项目列表</span>'+
194
+ '<div class="card no-hover"><div class="section"><h2>➕ 添加项目</h2>'+
195
+ '<p style="font-size:13px;color:#64748b;margin-bottom:12px">点击发送授权,在主机端 PPDocs 应用中选择项目并批准</p>'+
196
+ '<div class="fg"><label>本地项目目录 (可选,MCP安装时需要)</label><input id="iDir" value="'+S.addDir+'" placeholder="(可选) D:/projects/my-app" oninput="S.addDir=this.value"></div>'+
197
+ '<div class="btn-row"><button class="btn btn-p" onclick="doAuthStart()">📡 发送授权请求</button></div>'+
198
+ '</div></div>';
199
+ }
200
+
201
+ function doAuthStart(){
202
+ var dir=(document.getElementById('iDir')?document.getElementById('iDir').value:'').trim();
203
+ if(dir&&!/^[A-Za-z]:[\\\\\\/]|^\\//.test(dir)){toast('路径需为绝对路径或留空','err');return}
204
+ S.addDir=dir;
205
+ api('/api/auth/start',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({localDir:dir||''})}).then(function(r){
206
+ if(!r.ok){toast(r.error||'授权请求失败','err');return}
207
+ S.authRequestId=r.requestId;S.authStartTime=Date.now();S.authStatus='pending';S.addStep=2;
208
+ render();startAuthPoll();
209
+ });
210
+ }
211
+
212
+ var authPollTimer=null,authUiTimer=null;
213
+ function startAuthPoll(){
214
+ cancelAuth();
215
+ authPollTimer=setInterval(function(){
216
+ if(!S.authRequestId){cancelAuth();return}
217
+ var elapsed=Math.floor((Date.now()-S.authStartTime)/1000);
218
+ if(elapsed>=300){S.authStatus='expired';cancelAuth();_render();return}
219
+ api('/api/auth/status/'+S.authRequestId).then(function(r){
220
+ S.authStatus=r.status;
221
+ if(r.status==='approved'){
222
+ cancelAuth();
223
+ api('/api/bind',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({
224
+ localDir:S.addDir,remoteId:r.projectId,remoteName:r.projectName,password:r.password
225
+ })}).then(function(br){
226
+ if(br.ok){
227
+ S.authResult={name:r.projectName,id:r.projectId};_render();
228
+ toast('✅ 授权成功,已绑定 '+r.projectName);
229
+ setTimeout(function(){init();go('list')},2000);
230
+ }else{toast(br.error||'绑定失败','err')}
231
+ });
232
+ }else if(r.status==='rejected'){cancelAuth();_render()}
233
+ else{_render()}
234
+ }).catch(function(){});
235
+ },2000);
236
+ authUiTimer=setInterval(function(){if(S.page==='add'&&S.addStep===2&&S.authStatus==='pending')_render()},1000);
237
+ }
238
+ function cancelAuth(){if(authPollTimer){clearInterval(authPollTimer);authPollTimer=null}if(authUiTimer){clearInterval(authUiTimer);authUiTimer=null}}
239
+
240
+ function renderAddStep2(){
241
+ var elapsed=Math.floor((Date.now()-(S.authStartTime||Date.now()))/1000);
242
+ var pct=Math.min(100,Math.round(elapsed/300*100));
243
+ if(S.authStatus==='approved'&&S.authResult){
244
+ return '<div class="card no-hover" style="text-align:center;padding:40px">'+
245
+ '<div style="font-size:40px;margin-bottom:12px">✅</div>'+
246
+ '<h2 style="color:#22c55e;font-size:18px;margin-bottom:8px">授权成功!</h2>'+
247
+ '<p style="color:#94a3b8;font-size:14px">项目: '+S.authResult.name+' ('+S.authResult.id+')</p>'+
248
+ '<p style="color:#64748b;font-size:13px;margin-top:4px">权限: 读写 | 2秒后跳转...</p></div>';
249
+ }
250
+ if(S.authStatus==='rejected'){
251
+ return '<span class="back" onclick="go(\\x27list\\x27)">← 返回</span>'+
252
+ '<div class="card no-hover" style="text-align:center;padding:40px">'+
253
+ '<div style="font-size:40px;margin-bottom:12px">❌</div>'+
254
+ '<h2 style="color:#ef4444;font-size:18px;margin-bottom:8px">授权被拒绝</h2>'+
255
+ '<div class="btn-row" style="justify-content:center;margin-top:16px"><button class="btn btn-p" onclick="S.addStep=1;render()">重试</button></div></div>';
256
+ }
257
+ if(S.authStatus==='expired'){
258
+ return '<span class="back" onclick="go(\\x27list\\x27)">← 返回</span>'+
259
+ '<div class="card no-hover" style="text-align:center;padding:40px">'+
260
+ '<div style="font-size:40px;margin-bottom:12px">⏰</div>'+
261
+ '<h2 style="color:#f59e0b;font-size:18px;margin-bottom:8px">授权已超时</h2>'+
262
+ '<div class="btn-row" style="justify-content:center;margin-top:16px"><button class="btn btn-p" onclick="S.addStep=1;render()">重试</button></div></div>';
263
+ }
264
+ return '<span class="back" onclick="cancelAuth();S.addStep=1;render()">← 返回</span>'+
265
+ '<div class="card no-hover"><div class="section"><h2>➕ 添加项目 — Step 2/2</h2>'+
266
+ '<div style="text-align:center;padding:20px 0">'+
267
+ '<div style="font-size:36px;margin-bottom:10px;animation:pulse 2s infinite">📡</div>'+
268
+ '<p style="font-size:15px;font-weight:600;color:#60a5fa;margin-bottom:6px">等待主机端授权...</p>'+
269
+ '<p style="font-size:12px;color:#64748b;margin-bottom:4px">已向 '+S.host+':'+S.port+' 发送授权请求</p>'+
270
+ '<p style="font-size:12px;color:#475569">请在主机端 PPDocs 桌面应用中选择项目并批准</p>'+
271
+ '<div style="margin:16px auto;width:80%;height:4px;background:rgba(100,116,139,.2);border-radius:2px;overflow:hidden">'+
272
+ '<div style="width:'+pct+'%;height:100%;background:linear-gradient(90deg,#3b82f6,#6366f1);border-radius:2px;transition:width .5s"></div></div>'+
273
+ '<p style="font-size:11px;color:#475569">'+elapsed+'s / 300s</p>'+
274
+ '</div><div class="btn-row" style="justify-content:center"><button class="btn btn-s" onclick="cancelAuth();go(\\x27list\\x27)">取消</button></div></div></div>';
275
+ }
276
+
277
+ // ============ Project Detail ============
278
+ function renderDetail(){
279
+ var p=S.projects[S.detailIdx];
280
+ if(!p) return '<span class="back" onclick="go(\\x27list\\x27)">← 项目不存在</span>';
281
+ var tab=S.detailTab||'overview';
282
+ var tabs=[{key:'overview',label:'📊 概览'},{key:'mcp',label:'🤖 MCP'},{key:'prompts',label:'📝 提示词'}];
283
+ 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('');
284
+ var tabContent='';
285
+ if(tab==='overview') tabContent=renderOverview(p);
286
+ else if(tab==='mcp') tabContent=renderMcpTab(p);
287
+ else if(tab==='prompts') tabContent=renderPromptsTab(p);
288
+ return '<span class="back" onclick="go(\\x27list\\x27)">← 返回项目列表</span>'+
289
+ '<div class="header"><h1>📂 '+(p.localName||p.remoteName)+'</h1></div>'+
290
+ '<div class="tab-row">'+tabHtml+'</div>'+tabContent+
291
+ '<div class="card no-hover" style="border-color:rgba(239,68,68,.2)"><div class="section"><h2>⚠️ 危险区域</h2>'+
292
+ '<button class="btn btn-d" onclick="doUnbind(\\x27'+p.remoteId+'\\x27)">🗑️ 解除绑定</button></div></div>';
293
+ }
294
+
295
+ function renderOverview(p){
296
+ var authBadge=p.hasPassword?'<span class="auth-badge ok">✅ 已授权 (读写)</span>':'<span class="auth-badge no">⚠️ 未授权 (只读)</span>';
297
+ 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>';
298
+ return '<div class="card no-hover"><div class="section"><h2>📊 概览</h2>'+
299
+ '<div class="stat"><span>本地: '+(p.localDir||'<em style="color:#ef4444">未指定</em>')+'</span></div>'+
300
+ '<div class="stat"><span>远程: '+p.remoteName+' ('+p.remoteId+')</span></div>'+
301
+ '<div class="stat"><span>授权: '+authBadge+authBtn+'</span></div>'+
302
+ '<div class="stat"><span>文档: '+(p.docCount||0)+' 个</span></div>'+
303
+ '<div class="stat"><span>同步: '+(p.connected?'🟢 运行中':'🔴 '+p.syncStatus)+' '+(p.lastSync?'('+p.lastSync+')':'')+'</span></div>'+
304
+ '</div></div>'+
305
+ '<div class="card no-hover"><div class="section"><h2>📚 知识图谱</h2>'+
306
+ '<div class="tree-box" id="treeBox">加载中 ...</div></div></div>';
307
+ }
308
+
309
+ function renderMcpTab(p){
310
+ var mcpState=S.mcpStatus||{};
311
+ var platforms=[
312
+ {key:'cursor',icon:'🟣',name:'Cursor'},
313
+ {key:'antigravity',icon:'🟢',name:'Antigravity'},
314
+ {key:'vscode',icon:'🔵',name:'OpenCode'},
315
+ {key:'claude',icon:'🟠',name:'Claude'},
316
+ {key:'lobechat',icon:'🦞',name:'LobeChat'}
317
+ ];
318
+ var platformCards=platforms.map(function(pl){
319
+ var isManual=pl.key==='lobechat';
320
+ var installed=mcpState[pl.key];
321
+ var badge='',btn='';
322
+ if(isManual){
323
+ badge='<span style="font-size:11px;color:#94a3b8;display:block;margin:4px 0">手动配置</span>';
324
+ 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>';
325
+ }else{
326
+ 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>';
327
+ 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>';
328
+ }
329
+ return '<div class="platform-card"><div class="icon">'+pl.icon+'</div><div class="name" style="white-space:nowrap">'+pl.name+'</div>'+badge+btn+'</div>';
330
+ }).join('');
331
+ var noDir=!p.localDir;
332
+ var mcpListHtml='';
333
+ if(S.mcpAll&&S.mcpAll.length>0){
334
+ mcpListHtml=S.mcpAll.map(function(s){
335
+ var badges=s.platforms.map(function(pl){return '<span>'+pl+'</span>'}).join('');
336
+ return '<div class="mcp-list-item"><span>📦 '+s.name+'</span><div class="badges">'+badges+'</div></div>';
337
+ }).join('');
338
+ }else{
339
+ mcpListHtml='<div style="font-size:12px;color:#475569;padding:8px 0">未检测到任何 MCP 服务</div>';
340
+ }
341
+ return '<div class="card no-hover"><div class="section"><h2>🤖 PPDocs MCP 安装</h2>'+
342
+ (noDir?'<div style="font-size:13px;color:#f59e0b;margin-bottom:8px">⚠️ 未指定本地目录</div>':'')+
343
+ '<div class="platform-grid">'+platformCards+'</div></div></div>'+
344
+ '<div class="card no-hover"><div class="section"><h2>📦 项目内全部 MCP ('+(S.mcpAll?S.mcpAll.length:0)+')</h2>'+
345
+ mcpListHtml+'</div></div>';
346
+ }
347
+
348
+ function renderPromptsTab(p){
349
+ if(!p.localDir) return '<div class="card no-hover"><div class="section"><h2>📝 提示词</h2><div style="font-size:13px;color:#f59e0b">⚠️ 未指定本地目录</div></div></div>';
350
+ var files=S.prompts||[];
351
+ var cards=files.map(function(f){
352
+ var statusBadge=f.exists?'<span style="font-size:11px;color:#22c55e">'+f.lines+' 行</span>':'<span style="font-size:11px;color:#64748b">不存在</span>';
353
+ var btn=f.exists
354
+ ?'<button class="btn btn-s" style="font-size:11px;padding:3px 8px" onclick="doViewPrompt(\\x27'+p.remoteId+'\\x27,\\x27'+f.name+'\\x27)">查看/编辑</button>'
355
+ :'<button class="btn btn-p" style="font-size:11px;padding:3px 8px" onclick="doCreatePrompt(\\x27'+p.remoteId+'\\x27,\\x27'+f.name+'\\x27)">创建</button>';
356
+ return '<div class="mcp-list-item"><span>📄 '+f.name+'</span><div style="display:flex;gap:8px;align-items:center">'+statusBadge+btn+'</div></div>';
357
+ }).join('');
358
+ return '<div class="card no-hover"><div class="section"><h2>📝 系统提示词</h2>'+(cards||'<div style="font-size:12px;color:#475569">加载中...</div>')+'</div></div>';
359
+ }
360
+
361
+ // ============ Data Loading ============
362
+ function loadTree(rid){
363
+ var box=document.getElementById('treeBox');
364
+ if(!box) return;
365
+ var xhr=new XMLHttpRequest();
366
+ xhr.open('GET','/api/bind/'+rid+'/tree?_='+Date.now(),true);
367
+ xhr.onload=function(){
368
+ try{
369
+ var r=JSON.parse(xhr.responseText);
370
+ var txt=r.tree||'知识库为空';
371
+ var NL=String.fromCharCode(10);
372
+ if(r.docCount>0) txt=''+r.docCount+' 文档, '+r.dirCount+' 目录'+NL+NL+txt;
373
+ if(r.readOnly) txt='🔒 只读模式'+NL+NL+txt;
374
+ box.textContent=txt;
375
+ }catch(e){box.textContent='解析失败: '+e}
376
+ };
377
+ xhr.onerror=function(){box.textContent='请求失败'};
378
+ xhr.send();
379
+ }
380
+ function loadMcpStatus(rid){return api('/api/bind/'+rid+'/mcp').then(function(r){S.mcpStatus=r.platforms||{}}).catch(function(){S.mcpStatus={}})}
381
+ function loadMcpAll(rid){return api('/api/bind/'+rid+'/mcp/all').then(function(r){S.mcpAll=r.servers||[]}).catch(function(){S.mcpAll=[]})}
382
+ function loadPrompts(rid){return api('/api/bind/'+rid+'/prompts').then(function(r){S.prompts=r.files||[]}).catch(function(){S.prompts=[]})}
383
+
384
+ // ============ Auth from Detail ============
385
+ function doReAuth(remoteId,localDir){
386
+ cancelAuth();
387
+ api('/api/auth/start',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({localDir:localDir})}).then(function(r){
388
+ if(!r.ok){toast(r.error||'授权请求失败','err');return}
389
+ toast('📡 已发送授权请求,请在主机端批准');
390
+ S.authRequestId=r.requestId;S.authStartTime=Date.now();
391
+ authPollTimer=setInterval(function(){
392
+ var elapsed=Math.floor((Date.now()-S.authStartTime)/1000);
393
+ if(elapsed>=300){cancelAuth();toast('⏰ 授权已超时','err');return}
394
+ api('/api/auth/status/'+S.authRequestId).then(function(sr){
395
+ if(sr.status==='approved'){cancelAuth();
396
+ api('/api/bind/'+remoteId,{method:'PATCH',headers:{'Content-Type':'application/json'},body:JSON.stringify({password:sr.password,remoteName:sr.projectName})}).then(function(){
397
+ toast('✅ 授权成功!');_lastDetailRid=null;init();
398
+ });
399
+ }else if(sr.status==='rejected'){cancelAuth();toast('❌ 授权被拒绝','err')}
400
+ else if(sr.status==='expired'){cancelAuth();toast(' 授权已超时','err')}
401
+ }).catch(function(){});
402
+ },2000);
403
+ });
404
+ }
405
+
406
+ // ============ MCP ============
407
+ function doMcpInstall(remoteId,platform){
408
+ api('/api/bind/'+remoteId+'/mcp',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({platform:platform})}).then(function(r){
409
+ if(r.ok){
410
+ if(r.action==='copy'){S.modal={title:platform.toUpperCase()+' MCP 配置',content:r.config};renderModal()}
411
+ else{toast(r.message||'已安装/更新');loadMcpStatus(remoteId).then(function(){return loadMcpAll(remoteId)}).then(function(){_render()})}
412
+ }else{toast(r.error||'操作失败','err')}
413
+ });
414
+ }
415
+ function renderModal(){
416
+ if(!S.modal)return;var div=document.createElement('div');div.className='modal-bg';
417
+ div.innerHTML='<div class="modal"><h3>'+S.modal.title+'</h3><div class="cmd-box" id="mcpCopyBox">'+S.modal.content+'</div>'+
418
+ '<div class="btn-row"><button class="btn btn-p" onclick="copyMcpConfig()">📋 复制</button>'+
419
+ '<button class="btn btn-s" onclick="closeModal()">关闭</button></div></div>';
420
+ document.body.appendChild(div);div.addEventListener('click',function(e){if(e.target===div)div.remove()});
421
+ }
422
+ function closeModal(){var bg=document.querySelector('.modal-bg');if(bg)bg.remove();S.modal=null}
423
+ function copyMcpConfig(){var el=document.getElementById('mcpCopyBox');if(el){navigator.clipboard.writeText(el.textContent);toast('已复制')}}
424
+ function doMcpUninstall(remoteId,platform){
425
+ if(!confirm('确定卸载 '+platform+' 的 MCP 配置?'))return;
426
+ api('/api/bind/'+remoteId+'/mcp',{method:'DELETE',headers:{'Content-Type':'application/json'},body:JSON.stringify({platform:platform})}).then(function(r){
427
+ if(r.ok){toast(r.message||'已卸载');loadMcpStatus(remoteId).then(function(){return loadMcpAll(remoteId)}).then(function(){_render()})}
428
+ else{toast(r.error||'卸载失败','err')}
429
+ });
430
+ }
431
+
432
+ // ============ Prompts ============
433
+ function doViewPrompt(remoteId,name){
434
+ api('/api/bind/'+remoteId+'/prompts/'+encodeURIComponent(name)).then(function(r){
435
+ if(!r.ok){toast(r.error||'读取失败','err');return}
436
+ S.modal={title:'📝 '+name,content:r.content,remoteId:remoteId,fileName:name};renderPromptEditor();
437
+ });
438
+ }
439
+ function doCreatePrompt(remoteId,name){S.modal={title:'📝 创建 '+name,content:'',remoteId:remoteId,fileName:name};renderPromptEditor()}
440
+ function renderPromptEditor(){
441
+ if(!S.modal)return;var div=document.createElement('div');div.className='modal-bg';
442
+ var escaped=(S.modal.content||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
443
+ div.innerHTML='<div class="modal" style="max-width:650px"><h3>'+S.modal.title+'</h3>'+
444
+ '<textarea class="prompt-editor" id="promptEditor">'+escaped+'</textarea>'+
445
+ '<div class="btn-row"><button class="btn btn-p" onclick="doSavePrompt()">💾 保存</button>'+
446
+ '<button class="btn btn-s" onclick="closeModal()">关闭</button></div></div>';
447
+ document.body.appendChild(div);div.addEventListener('click',function(e){if(e.target===div)div.remove()});
448
+ }
449
+ function doSavePrompt(){
450
+ var content=document.getElementById('promptEditor').value;
451
+ 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){
452
+ if(r.ok){toast('✅ 已保存');var rid=S.modal.remoteId;closeModal();loadPrompts(rid).then(function(){_render()})}
453
+ else{toast(r.error||'保存失败','err')}
454
+ });
455
+ }
456
+
457
+ // ============ Unbind ============
458
+ function doUnbind(remoteId){
459
+ if(!confirm('确定解除绑定?'))return;
460
+ api('/api/bind/'+remoteId,{method:'DELETE'}).then(function(r){if(r.ok){toast('已解绑');init();go('list')}else{toast('解绑失败','err')}});
461
+ }
462
+
463
+ // ============ Auto-refresh ============
464
+ var _detailLoading=false,_lastDetailRid=null;
465
+ var _render=render;
466
+ render=function(){
467
+ _render();
468
+ var _rid=S.projects[S.detailIdx]?S.projects[S.detailIdx].remoteId:null;
469
+ if(S.page==='detail'&&_rid&&!_detailLoading&&_lastDetailRid!==_rid){
470
+ _detailLoading=true;_lastDetailRid=_rid;
471
+ loadMcpStatus(_rid);loadMcpAll(_rid);loadPrompts(_rid);
472
+ setTimeout(function(){
473
+ _detailLoading=false;
474
+ loadTree(_rid);
475
+ },100);
476
+ }
477
+ };
478
+ setInterval(function(){
479
+ if(S.page==='list'){
480
+ api('/api/status').then(function(st){S.projects=st.projects||[];S.hostOk=st.hostConnected;_render()}).catch(function(){});
481
+ }
482
+ },10000);
483
+
484
+ init();
485
+ </script>
486
+ </body>
473
487
  </html>`;
474
488
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ppdocs/mcp",
3
- "version": "3.2.4",
3
+ "version": "3.2.6",
4
4
  "description": "ppdocs MCP Server - Knowledge Graph for Claude",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",