@ppdocs/mcp 3.2.4 → 3.2.5
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 +3 -10
- package/dist/web/server.js +14 -15
- package/dist/web/ui.js +481 -467
- package/package.json +1 -1
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,
|
|
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
|
-
|
|
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 || {}),
|
package/dist/web/server.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
-
|
|
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 (!
|
|
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
|
-
//
|
|
256
|
-
|
|
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);
|
|
@@ -578,17 +579,15 @@ function hasMcpInJson(filePath, serverName) {
|
|
|
578
579
|
return false;
|
|
579
580
|
}
|
|
580
581
|
}
|
|
581
|
-
/** 构建 MCP server 配置对象 */
|
|
582
|
-
function buildMcpServerConfig(
|
|
582
|
+
/** 构建 MCP server 配置对象 (不传 PPDOCS_API_URL,依赖 cwd/.ppdocs 文件自动识别项目) */
|
|
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
587
|
} : {
|
|
588
588
|
command: 'npx',
|
|
589
589
|
args: ['-y', '@ppdocs/mcp@latest'],
|
|
590
590
|
env: {
|
|
591
|
-
PPDOCS_API_URL: apiUrl,
|
|
592
591
|
PATH: `${process.env.PATH || '/usr/bin:/bin'}:/usr/local/bin:/opt/homebrew/bin`,
|
|
593
592
|
},
|
|
594
593
|
};
|
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
|
-
|
|
78
|
-
<
|
|
79
|
-
<
|
|
80
|
-
|
|
81
|
-
<
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
// ============
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
// ============
|
|
89
|
-
function
|
|
90
|
-
|
|
91
|
-
// ============
|
|
92
|
-
function
|
|
93
|
-
|
|
94
|
-
// ============
|
|
95
|
-
function
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
case '
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
'<div class="
|
|
136
|
-
'<div class="
|
|
137
|
-
'<
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
'<
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
'<div class="
|
|
178
|
-
'<
|
|
179
|
-
|
|
180
|
-
'<div class="
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
'<
|
|
194
|
-
'<
|
|
195
|
-
'<
|
|
196
|
-
|
|
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(
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
},
|
|
237
|
-
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
var
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
'<
|
|
247
|
-
'<
|
|
248
|
-
'<p style="color:#
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
'<div
|
|
254
|
-
'<
|
|
255
|
-
'<
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
'<div
|
|
261
|
-
'<
|
|
262
|
-
'<
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
'<div
|
|
267
|
-
'<div style="
|
|
268
|
-
'<
|
|
269
|
-
'<p style="font-size:
|
|
270
|
-
'<p style="font-size:12px;color:#
|
|
271
|
-
'<
|
|
272
|
-
'<div style="
|
|
273
|
-
'<
|
|
274
|
-
'
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
var
|
|
283
|
-
var tabs
|
|
284
|
-
var
|
|
285
|
-
|
|
286
|
-
if(tab==='
|
|
287
|
-
else if(tab==='
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
'<div class="
|
|
291
|
-
'<div class="
|
|
292
|
-
'<
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
var
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
'<div class="stat"><span
|
|
301
|
-
'<div class="stat"><span
|
|
302
|
-
'<div class="stat"><span
|
|
303
|
-
'<div class="stat"><span
|
|
304
|
-
'
|
|
305
|
-
'
|
|
306
|
-
'<div class="
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
var
|
|
312
|
-
|
|
313
|
-
{key:'
|
|
314
|
-
{key:'
|
|
315
|
-
{key:'
|
|
316
|
-
{key:'
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
var
|
|
321
|
-
var
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
var
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
'<div class="
|
|
345
|
-
'
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
var files
|
|
352
|
-
|
|
353
|
-
var
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
}
|
|
403
|
-
});
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
'
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
function
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
'
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
}
|
|
439
|
-
function
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
if(
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
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
|
}
|