cx-viewer 1.0.0 → 1.0.2
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/cli.js +2 -2
- package/dist/assets/App-CF-qZ3B6.js +41 -0
- package/dist/assets/{AppHeader-B3hrnZr-.css → AppHeader-BqUGBbBx.css} +2 -2
- package/dist/assets/{AppHeader.module-BMU2vyC4.js → AppHeader.module-BjoBBbCB.js} +194 -190
- package/dist/assets/Mobile-ByhBkyqb.js +10 -0
- package/dist/assets/{_baseUniq-CXWHShmB.js → _baseUniq-5XIuSb_1.js} +1 -1
- package/dist/assets/{arc-qbol0NJo.js → arc-V3mJhKFJ.js} +1 -1
- package/dist/assets/{architectureDiagram-Q4EWVU46-COZebb1N.js → architectureDiagram-Q4EWVU46-22bjxOE1.js} +1 -1
- package/dist/assets/{blockDiagram-DXYQGD6D-COCgzPKy.js → blockDiagram-DXYQGD6D-Brshek1N.js} +1 -1
- package/dist/assets/{c4Diagram-AHTNJAMY-Bs5XvLEo.js → c4Diagram-AHTNJAMY-DQHIuxkd.js} +1 -1
- package/dist/assets/channel-SmGaOM6g.js +1 -0
- package/dist/assets/{chunk-4BX2VUAB-CyGriz9d.js → chunk-4BX2VUAB-BQE7pifU.js} +1 -1
- package/dist/assets/{chunk-4TB4RGXK-DlT_q-fl.js → chunk-4TB4RGXK-DcZwv_PV.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-NHN99ZNC.js → chunk-55IACEB6-Gd_3-vV5.js} +1 -1
- package/dist/assets/{chunk-EDXVE4YY-fK4f9Na9.js → chunk-EDXVE4YY-BbnYdg9M.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-DL-dQIq1.js → chunk-FMBD7UC4-NSh7z90L.js} +1 -1
- package/dist/assets/{chunk-OYMX7WX6-6uruB4VY.js → chunk-OYMX7WX6-DdFwjHsc.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-CCWIqLO8.js → chunk-QZHKN3VN-C-1z3DA3.js} +1 -1
- package/dist/assets/{chunk-YZCP3GAM-Cu84DMJo.js → chunk-YZCP3GAM-w_NiY6dL.js} +1 -1
- package/dist/assets/classDiagram-6PBFFD2Q-Bu96zciS.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-Bu96zciS.js +1 -0
- package/dist/assets/clone-u2hQVO7u.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-Dqi4iU3-.js → cose-bilkent-S5V4N54A-bHlM-ydV.js} +1 -1
- package/dist/assets/{dagre-KV5264BT-Dl-EJ9rb.js → dagre-KV5264BT-2cCXOuNw.js} +1 -1
- package/dist/assets/{diagram-5BDNPKRD-BYtyzF0D.js → diagram-5BDNPKRD-C70P_Nd1.js} +1 -1
- package/dist/assets/{diagram-G4DWMVQ6-BuETHFNU.js → diagram-G4DWMVQ6-C2JvwT8U.js} +1 -1
- package/dist/assets/{diagram-MMDJMWI5-Z02oZp6I.js → diagram-MMDJMWI5-C9ygMSLV.js} +1 -1
- package/dist/assets/{diagram-TYMM5635-BpGlKv8l.js → diagram-TYMM5635-x-SMPmqb.js} +1 -1
- package/dist/assets/{erDiagram-SMLLAGMA-C4YngeQh.js → erDiagram-SMLLAGMA-BGIbL82W.js} +1 -1
- package/dist/assets/{flowDiagram-DWJPFMVM-BWddFffg.js → flowDiagram-DWJPFMVM-Dg0qwguE.js} +1 -1
- package/dist/assets/{ganttDiagram-T4ZO3ILL-Bg0jOD4m.js → ganttDiagram-T4ZO3ILL-XX5qCwKD.js} +1 -1
- package/dist/assets/{gitGraphDiagram-UUTBAWPF-Bz-3sTXZ.js → gitGraphDiagram-UUTBAWPF-DFil4bpA.js} +1 -1
- package/dist/assets/{graph-XiH8DkIu.js → graph-DJ6egHOd.js} +1 -1
- package/dist/assets/{index-BuUsWKGZ.js → index-DYIYWRaO.js} +2 -2
- package/dist/assets/{infoDiagram-42DDH7IO-DLo-lOCE.js → infoDiagram-42DDH7IO-yB7VRrpO.js} +1 -1
- package/dist/assets/{ishikawaDiagram-UXIWVN3A-BsIzT5eO.js → ishikawaDiagram-UXIWVN3A-DZ_lPco0.js} +1 -1
- package/dist/assets/{journeyDiagram-VCZTEJTY-C3bOkKqv.js → journeyDiagram-VCZTEJTY-B-FlPR2W.js} +1 -1
- package/dist/assets/{kanban-definition-6JOO6SKY-D-rJRNbp.js → kanban-definition-6JOO6SKY-mgJwVxKf.js} +1 -1
- package/dist/assets/{layout-Bqp8Hh1c.js → layout-BjzpJ8ZU.js} +1 -1
- package/dist/assets/{linear-BWRSipGb.js → linear-CqOq1rA7.js} +1 -1
- package/dist/assets/{mermaid.core-CJa7klhs.js → mermaid.core-CZyQhtml.js} +4 -4
- package/dist/assets/{min-CQ_VyzMd.js → min-B_tad8Qz.js} +1 -1
- package/dist/assets/{mindmap-definition-QFDTVHPH-Bnj7DDSx.js → mindmap-definition-QFDTVHPH-CgBeHk8K.js} +1 -1
- package/dist/assets/{pieDiagram-DEJITSTG-CC8a7Ylw.js → pieDiagram-DEJITSTG-CHENa0KJ.js} +1 -1
- package/dist/assets/{quadrantDiagram-34T5L4WZ-CrmL1WUV.js → quadrantDiagram-34T5L4WZ-BUwZwCpc.js} +1 -1
- package/dist/assets/{requirementDiagram-MS252O5E-TfkX4u_6.js → requirementDiagram-MS252O5E-H7OaNjNn.js} +1 -1
- package/dist/assets/{sankeyDiagram-XADWPNL6-DMwBbND7.js → sankeyDiagram-XADWPNL6-6UqaFrBf.js} +1 -1
- package/dist/assets/{sequenceDiagram-FGHM5R23-CFghMYsm.js → sequenceDiagram-FGHM5R23-DBgpLSsr.js} +1 -1
- package/dist/assets/{stateDiagram-FHFEXIEX-1H6JIM7l.js → stateDiagram-FHFEXIEX-D_6TWlLl.js} +1 -1
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-Cg06jbzN.js +1 -0
- package/dist/assets/{timeline-definition-GMOUNBTQ-DnLMBlVa.js → timeline-definition-GMOUNBTQ-9_saaHvl.js} +1 -1
- package/dist/assets/{vennDiagram-DHZGUBPP-DoIkY_KL.js → vennDiagram-DHZGUBPP-DMUS1fiT.js} +1 -1
- package/dist/assets/{wardley-RL74JXVD-3n5XmOD5.js → wardley-RL74JXVD-Y-_Nnpu2.js} +1 -1
- package/dist/assets/{wardleyDiagram-NUSXRM2D-UMOq3ofC.js → wardleyDiagram-NUSXRM2D-DIh62HY7.js} +1 -1
- package/dist/assets/{xychartDiagram-5P7HB3ND-CNDuQ5Cg.js → xychartDiagram-5P7HB3ND-B_CLrnpt.js} +1 -1
- package/dist/index.html +1 -1
- package/lib/appserver-bridge.js +108 -9
- package/lib/log-management.js +3 -1
- package/lib/plugin-loader.js +126 -6
- package/lib/sdk-manager.js +55 -0
- package/package.json +1 -1
- package/pty-manager.js +27 -3
- package/server.js +301 -34
- package/dist/assets/App-BzrN42Qt.js +0 -41
- package/dist/assets/Mobile-C9wxQTXV.js +0 -10
- package/dist/assets/channel-DvLUZ3Xf.js +0 -1
- package/dist/assets/classDiagram-6PBFFD2Q-ByKhp2Tk.js +0 -1
- package/dist/assets/classDiagram-v2-HSJHXN6E-ByKhp2Tk.js +0 -1
- package/dist/assets/clone-DZHpE494.js +0 -1
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-CtXMaKcb.js +0 -1
package/server.js
CHANGED
|
@@ -2,7 +2,7 @@ import { createServer } from 'node:http';
|
|
|
2
2
|
import { createServer as createHttpsServer } from 'node:https';
|
|
3
3
|
import { createConnection } from 'node:net';
|
|
4
4
|
import { randomBytes } from 'node:crypto';
|
|
5
|
-
import { readFileSync, writeFileSync, existsSync, watchFile, unwatchFile, statSync, readdirSync, renameSync, unlinkSync, rmSync, openSync, readSync, closeSync, realpathSync, mkdirSync, createReadStream, cpSync, copyFileSync } from 'node:fs';
|
|
5
|
+
import { readFileSync, writeFileSync, existsSync, watchFile, unwatchFile, statSync, readdirSync, renameSync, unlinkSync, rmSync, openSync, readSync, closeSync, realpathSync, mkdirSync, createReadStream, cpSync, copyFileSync, appendFileSync } from 'node:fs';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import { dirname, join, extname, resolve, basename } from 'node:path';
|
|
8
8
|
import { homedir, platform, networkInterfaces } from 'node:os';
|
|
@@ -53,6 +53,31 @@ import { countLogEntries, streamRawEntriesAsync, readPagedEntries } from './lib/
|
|
|
53
53
|
|
|
54
54
|
// 动态获取 getPrefsFile()(LOG_DIR 可能在运行时被 setLogDir 修改)
|
|
55
55
|
function getPrefsFile() { return join(LOG_DIR, 'preferences.json'); }
|
|
56
|
+
function readPreferences() {
|
|
57
|
+
let prefs = {};
|
|
58
|
+
try { if (existsSync(getPrefsFile())) prefs = JSON.parse(readFileSync(getPrefsFile(), 'utf-8')); } catch { }
|
|
59
|
+
return prefs;
|
|
60
|
+
}
|
|
61
|
+
function writePreferences(prefs) {
|
|
62
|
+
const prefsFile = getPrefsFile();
|
|
63
|
+
const prefsDir = dirname(prefsFile);
|
|
64
|
+
if (!existsSync(prefsDir)) mkdirSync(prefsDir, { recursive: true });
|
|
65
|
+
writeFileSync(prefsFile, JSON.stringify(prefs, null, 2));
|
|
66
|
+
}
|
|
67
|
+
function normalizePluginFilename(name) {
|
|
68
|
+
return String(name || '').replace(/.*[/\\]/, '');
|
|
69
|
+
}
|
|
70
|
+
function removeDisabledPluginNames(names) {
|
|
71
|
+
const targetNames = Array.from(new Set((names || []).filter(Boolean)));
|
|
72
|
+
if (targetNames.length === 0) return false;
|
|
73
|
+
const prefs = readPreferences();
|
|
74
|
+
const disabledPlugins = Array.isArray(prefs.disabledPlugins) ? prefs.disabledPlugins : [];
|
|
75
|
+
const next = disabledPlugins.filter(name => !targetNames.includes(name));
|
|
76
|
+
if (next.length === disabledPlugins.length) return false;
|
|
77
|
+
prefs.disabledPlugins = next;
|
|
78
|
+
writePreferences(prefs);
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
56
81
|
|
|
57
82
|
// 启动时一次性读取 ~/.codex/settings.json(不 watch)
|
|
58
83
|
let codexSettings = {};
|
|
@@ -153,11 +178,73 @@ const ACCESS_TOKEN = randomBytes(16).toString('hex');
|
|
|
153
178
|
|
|
154
179
|
let clients = [];
|
|
155
180
|
let server;
|
|
181
|
+
let secondaryServer = null;
|
|
156
182
|
let actualPort = 0;
|
|
183
|
+
let secondaryPort = 0;
|
|
157
184
|
let serverProtocol = 'http';
|
|
185
|
+
let publicAccessProtocol = 'http';
|
|
186
|
+
let publicAccessPort = 0;
|
|
187
|
+
let pluginRoutes = [];
|
|
158
188
|
// Stats Worker 实例
|
|
159
189
|
let statsWorker = null;
|
|
160
190
|
|
|
191
|
+
function registerPluginRoute(method, path, handler) {
|
|
192
|
+
const normalizedMethod = String(method || 'GET').toUpperCase();
|
|
193
|
+
const normalizedPath = String(path || '').trim();
|
|
194
|
+
if (!normalizedPath.startsWith('/')) {
|
|
195
|
+
throw new Error('Plugin route path must start with "/"');
|
|
196
|
+
}
|
|
197
|
+
if (typeof handler !== 'function') {
|
|
198
|
+
throw new Error('Plugin route handler must be a function');
|
|
199
|
+
}
|
|
200
|
+
const nextRoute = { method: normalizedMethod, path: normalizedPath, handler };
|
|
201
|
+
const idx = pluginRoutes.findIndex(route => route.method === normalizedMethod && route.path === normalizedPath);
|
|
202
|
+
if (idx >= 0) pluginRoutes[idx] = nextRoute;
|
|
203
|
+
else pluginRoutes.push(nextRoute);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function refreshPluginRuntime() {
|
|
207
|
+
pluginRoutes = [];
|
|
208
|
+
if (!server || !actualPort) return;
|
|
209
|
+
await runParallelHook('serverStarted', {
|
|
210
|
+
port: actualPort,
|
|
211
|
+
host: HOST,
|
|
212
|
+
url: `${serverProtocol}://127.0.0.1:${actualPort}`,
|
|
213
|
+
ip: getLocalIp(),
|
|
214
|
+
token: ACCESS_TOKEN,
|
|
215
|
+
protocol: serverProtocol,
|
|
216
|
+
httpServer: server,
|
|
217
|
+
registerRoute: registerPluginRoute,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function getPublicAccessUrl(ip = getLocalIp()) {
|
|
222
|
+
const protocol = publicAccessProtocol || serverProtocol;
|
|
223
|
+
const port = publicAccessPort || actualPort;
|
|
224
|
+
return `${protocol}://${ip}:${port}?token=${ACCESS_TOKEN}`;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function findAvailablePort(startPort) {
|
|
228
|
+
return new Promise((resolve) => {
|
|
229
|
+
function tryPort(port) {
|
|
230
|
+
if (port > MAX_PORT) {
|
|
231
|
+
resolve(null);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
const probe = createConnection({ host: '127.0.0.1', port });
|
|
235
|
+
probe.on('connect', () => {
|
|
236
|
+
probe.destroy();
|
|
237
|
+
tryPort(port + 1);
|
|
238
|
+
});
|
|
239
|
+
probe.on('error', () => {
|
|
240
|
+
probe.destroy();
|
|
241
|
+
resolve(port);
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
tryPort(startPort);
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
161
248
|
function startStatsWorker() {
|
|
162
249
|
try {
|
|
163
250
|
statsWorker = new Worker(new URL('./lib/stats-worker.js', import.meta.url));
|
|
@@ -270,6 +357,21 @@ async function handleRequest(req, res) {
|
|
|
270
357
|
}
|
|
271
358
|
}
|
|
272
359
|
|
|
360
|
+
const pluginRoute = pluginRoutes.find(route => route.method === method && route.path === url);
|
|
361
|
+
if (pluginRoute) {
|
|
362
|
+
try {
|
|
363
|
+
await pluginRoute.handler(req, res, parsedUrl);
|
|
364
|
+
} catch (err) {
|
|
365
|
+
if (!res.headersSent) {
|
|
366
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
367
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
368
|
+
} else {
|
|
369
|
+
try { res.end(); } catch {}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
|
|
273
375
|
// OTLP HTTP 接收端点 — 接收 Codex 原生 OTel trace 数据
|
|
274
376
|
if (url === '/v1/traces' && method === 'POST') {
|
|
275
377
|
const buffers = [];
|
|
@@ -466,8 +568,7 @@ async function handleRequest(req, res) {
|
|
|
466
568
|
}
|
|
467
569
|
|
|
468
570
|
if (url === '/api/preferences' && method === 'GET') {
|
|
469
|
-
let prefs =
|
|
470
|
-
try { if (existsSync(getPrefsFile())) prefs = JSON.parse(readFileSync(getPrefsFile(), 'utf-8')); } catch { }
|
|
571
|
+
let prefs = readPreferences();
|
|
471
572
|
prefs.logDir = LOG_DIR; // 始终返回当前运行时的日志目录
|
|
472
573
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
473
574
|
res.end(JSON.stringify(prefs));
|
|
@@ -484,14 +585,9 @@ async function handleRequest(req, res) {
|
|
|
484
585
|
if (incoming.logDir && typeof incoming.logDir === 'string') {
|
|
485
586
|
setLogDir(incoming.logDir);
|
|
486
587
|
}
|
|
487
|
-
let prefs =
|
|
488
|
-
try { if (existsSync(getPrefsFile())) prefs = JSON.parse(readFileSync(getPrefsFile(), 'utf-8')); } catch { }
|
|
588
|
+
let prefs = readPreferences();
|
|
489
589
|
Object.assign(prefs, incoming);
|
|
490
|
-
|
|
491
|
-
const prefsFile = getPrefsFile();
|
|
492
|
-
const prefsDir = dirname(prefsFile);
|
|
493
|
-
if (!existsSync(prefsDir)) mkdirSync(prefsDir, { recursive: true });
|
|
494
|
-
writeFileSync(prefsFile, JSON.stringify(prefs, null, 2));
|
|
590
|
+
writePreferences(prefs);
|
|
495
591
|
// 主题切换时同步到 Codex Code CLI:发 /theme,监听输出验证结果,不对就再发一次
|
|
496
592
|
if (incoming.themeColor && _writeToPty && _onPtyData) {
|
|
497
593
|
const target = incoming.themeColor === 'light' ? 'light' : 'dark';
|
|
@@ -973,6 +1069,17 @@ async function handleRequest(req, res) {
|
|
|
973
1069
|
return;
|
|
974
1070
|
}
|
|
975
1071
|
|
|
1072
|
+
if (url === '/api/terminal-history' && method === 'GET') {
|
|
1073
|
+
const { getPtyState, getOutputBuffer, getOutputHistoryId } = await import('./pty-manager.js');
|
|
1074
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1075
|
+
res.end(JSON.stringify({
|
|
1076
|
+
...getPtyState(),
|
|
1077
|
+
historyId: getOutputHistoryId(),
|
|
1078
|
+
data: getOutputBuffer() || '',
|
|
1079
|
+
}));
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
976
1083
|
// 当前版本号
|
|
977
1084
|
if (url === '/api/version-info' && method === 'GET') {
|
|
978
1085
|
try {
|
|
@@ -2263,6 +2370,32 @@ async function handleRequest(req, res) {
|
|
|
2263
2370
|
return;
|
|
2264
2371
|
}
|
|
2265
2372
|
|
|
2373
|
+
if (url === '/api/plugins/module' && method === 'GET') {
|
|
2374
|
+
try {
|
|
2375
|
+
const file = normalizePluginFilename(parsedUrl.searchParams.get('file'));
|
|
2376
|
+
if (!file || (extname(file) !== '.js' && extname(file) !== '.mjs')) {
|
|
2377
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
2378
|
+
res.end(JSON.stringify({ error: 'Invalid plugin filename' }));
|
|
2379
|
+
return;
|
|
2380
|
+
}
|
|
2381
|
+
const filePath = join(getPluginsDir(), file);
|
|
2382
|
+
if (!existsSync(filePath) || !statSync(filePath).isFile()) {
|
|
2383
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
2384
|
+
res.end(JSON.stringify({ error: 'Plugin file not found' }));
|
|
2385
|
+
return;
|
|
2386
|
+
}
|
|
2387
|
+
res.writeHead(200, {
|
|
2388
|
+
'Content-Type': 'application/javascript; charset=utf-8',
|
|
2389
|
+
'Cache-Control': 'no-store',
|
|
2390
|
+
});
|
|
2391
|
+
res.end(readFileSync(filePath, 'utf-8'));
|
|
2392
|
+
} catch (err) {
|
|
2393
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
2394
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
2395
|
+
}
|
|
2396
|
+
return;
|
|
2397
|
+
}
|
|
2398
|
+
|
|
2266
2399
|
if (url === '/api/plugins' && method === 'DELETE') {
|
|
2267
2400
|
const file = parsedUrl.searchParams.get('file');
|
|
2268
2401
|
if (!file || file.includes('..') || file.includes('/') || file.includes('\\')) {
|
|
@@ -2277,8 +2410,11 @@ async function handleRequest(req, res) {
|
|
|
2277
2410
|
res.end(JSON.stringify({ error: 'File not found' }));
|
|
2278
2411
|
return;
|
|
2279
2412
|
}
|
|
2413
|
+
const targetPlugin = getPluginsInfo().find(p => p.file === file);
|
|
2280
2414
|
unlinkSync(filePath);
|
|
2415
|
+
if (targetPlugin?.name) removeDisabledPluginNames([targetPlugin.name]);
|
|
2281
2416
|
await loadPlugins();
|
|
2417
|
+
await refreshPluginRuntime();
|
|
2282
2418
|
const plugins = getPluginsInfo();
|
|
2283
2419
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2284
2420
|
res.end(JSON.stringify({ ok: true, plugins, pluginsDir: getPluginsDir() }));
|
|
@@ -2292,6 +2428,7 @@ async function handleRequest(req, res) {
|
|
|
2292
2428
|
if (url === '/api/plugins/reload' && method === 'POST') {
|
|
2293
2429
|
try {
|
|
2294
2430
|
await loadPlugins();
|
|
2431
|
+
await refreshPluginRuntime();
|
|
2295
2432
|
const plugins = getPluginsInfo();
|
|
2296
2433
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2297
2434
|
res.end(JSON.stringify({ ok: true, plugins, pluginsDir: getPluginsDir() }));
|
|
@@ -2310,7 +2447,15 @@ async function handleRequest(req, res) {
|
|
|
2310
2447
|
const { files: fileList } = JSON.parse(body);
|
|
2311
2448
|
uploadPlugins(getPluginsDir(), fileList);
|
|
2312
2449
|
await loadPlugins();
|
|
2313
|
-
|
|
2450
|
+
await refreshPluginRuntime();
|
|
2451
|
+
let plugins = getPluginsInfo();
|
|
2452
|
+
const uploadedFiles = new Set((fileList || []).map(file => normalizePluginFilename(file?.name)));
|
|
2453
|
+
const uploadedNames = plugins.filter(plugin => uploadedFiles.has(plugin.file)).map(plugin => plugin.name);
|
|
2454
|
+
if (removeDisabledPluginNames(uploadedNames)) {
|
|
2455
|
+
await loadPlugins();
|
|
2456
|
+
await refreshPluginRuntime();
|
|
2457
|
+
plugins = getPluginsInfo();
|
|
2458
|
+
}
|
|
2314
2459
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2315
2460
|
res.end(JSON.stringify({ ok: true, plugins, pluginsDir: getPluginsDir() }));
|
|
2316
2461
|
} catch (err) {
|
|
@@ -2329,9 +2474,16 @@ async function handleRequest(req, res) {
|
|
|
2329
2474
|
try {
|
|
2330
2475
|
const { url: fileUrl } = JSON.parse(body);
|
|
2331
2476
|
const extractScript = join(__dirname, 'lib', 'extract-plugin-name.mjs');
|
|
2332
|
-
await installPluginFromUrl(getPluginsDir(), fileUrl, extractScript);
|
|
2477
|
+
const { filename } = await installPluginFromUrl(getPluginsDir(), fileUrl, extractScript);
|
|
2333
2478
|
await loadPlugins();
|
|
2334
|
-
|
|
2479
|
+
await refreshPluginRuntime();
|
|
2480
|
+
let plugins = getPluginsInfo();
|
|
2481
|
+
const installedNames = plugins.filter(plugin => plugin.file === filename).map(plugin => plugin.name);
|
|
2482
|
+
if (removeDisabledPluginNames(installedNames)) {
|
|
2483
|
+
await loadPlugins();
|
|
2484
|
+
await refreshPluginRuntime();
|
|
2485
|
+
plugins = getPluginsInfo();
|
|
2486
|
+
}
|
|
2335
2487
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2336
2488
|
res.end(JSON.stringify({ ok: true, plugins, pluginsDir: getPluginsDir() }));
|
|
2337
2489
|
} catch (err) {
|
|
@@ -2346,10 +2498,22 @@ async function handleRequest(req, res) {
|
|
|
2346
2498
|
// 返回局域网访问地址
|
|
2347
2499
|
if (url === '/api/local-url' && method === 'GET') {
|
|
2348
2500
|
const localIp = getLocalIp();
|
|
2349
|
-
const defaultUrl =
|
|
2350
|
-
const hookResult = await runWaterfallHook('localUrl', {
|
|
2501
|
+
const defaultUrl = getPublicAccessUrl(localIp);
|
|
2502
|
+
const hookResult = await runWaterfallHook('localUrl', {
|
|
2503
|
+
url: defaultUrl,
|
|
2504
|
+
ip: localIp,
|
|
2505
|
+
port: publicAccessPort || actualPort,
|
|
2506
|
+
token: ACCESS_TOKEN,
|
|
2507
|
+
protocol: publicAccessProtocol || serverProtocol,
|
|
2508
|
+
httpUrl: actualPort ? `http://${localIp}:${actualPort}?token=${ACCESS_TOKEN}` : null,
|
|
2509
|
+
httpsUrl: secondaryPort ? `https://${localIp}:${secondaryPort}?token=${ACCESS_TOKEN}` : null,
|
|
2510
|
+
});
|
|
2351
2511
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2352
|
-
res.end(JSON.stringify({
|
|
2512
|
+
res.end(JSON.stringify({
|
|
2513
|
+
url: hookResult.url,
|
|
2514
|
+
httpUrl: actualPort ? `http://${localIp}:${actualPort}?token=${ACCESS_TOKEN}` : null,
|
|
2515
|
+
httpsUrl: secondaryPort ? `https://${localIp}:${secondaryPort}?token=${ACCESS_TOKEN}` : null,
|
|
2516
|
+
}));
|
|
2353
2517
|
return;
|
|
2354
2518
|
}
|
|
2355
2519
|
|
|
@@ -2715,22 +2879,68 @@ async function handleRequest(req, res) {
|
|
|
2715
2879
|
}
|
|
2716
2880
|
|
|
2717
2881
|
export async function startViewer() {
|
|
2882
|
+
pluginRoutes = [];
|
|
2883
|
+
secondaryServer = null;
|
|
2884
|
+
secondaryPort = 0;
|
|
2885
|
+
publicAccessProtocol = 'http';
|
|
2886
|
+
publicAccessPort = 0;
|
|
2718
2887
|
// 加载插件(需要在创建服务器之前,以便通过 hook 获取 HTTPS 证书)
|
|
2719
2888
|
await loadPlugins();
|
|
2720
2889
|
|
|
2721
2890
|
// 通过插件 hook 获取 HTTPS 证书选项
|
|
2722
2891
|
let httpsOptions = null;
|
|
2892
|
+
let httpsFromPlugin = false;
|
|
2723
2893
|
try {
|
|
2724
2894
|
const httpsResult = await runWaterfallHook('httpsOptions', {});
|
|
2725
2895
|
httpsOptions = (httpsResult.pfx || httpsResult.cert) ? httpsResult : null;
|
|
2896
|
+
httpsFromPlugin = !!httpsOptions;
|
|
2726
2897
|
} catch (err) {
|
|
2727
2898
|
console.error('[CX Viewer] httpsOptions hook error:', err.message);
|
|
2728
2899
|
}
|
|
2729
2900
|
|
|
2730
|
-
|
|
2901
|
+
// 自动生成自签证书,供 HTTPS 访问使用。
|
|
2902
|
+
// CLI 模式下会保留 HTTP 主入口,同时额外起一个 HTTPS 入口用于二维码和局域网麦克风访问。
|
|
2903
|
+
if (!httpsOptions) {
|
|
2904
|
+
try {
|
|
2905
|
+
const { generateKeyPairSync, createSign, X509Certificate } = await import('node:crypto');
|
|
2906
|
+
const { privateKey, publicKey } = generateKeyPairSync('rsa', {
|
|
2907
|
+
modulusLength: 2048,
|
|
2908
|
+
publicKeyEncoding: { type: 'spki', format: 'pem' },
|
|
2909
|
+
privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
|
|
2910
|
+
});
|
|
2911
|
+
// 用 node:tls 的 createSecureContext 验证 key 可用性
|
|
2912
|
+
// 生成自签名证书(简化版:直接用 key + cert)
|
|
2913
|
+
const { execSync: _execSync } = await import('node:child_process');
|
|
2914
|
+
const certDir = join(homedir(), '.codex', 'cx-viewer');
|
|
2915
|
+
const keyPath = join(certDir, 'selfsigned.key');
|
|
2916
|
+
const certPath = join(certDir, 'selfsigned.crt');
|
|
2917
|
+
// 检查已有证书是否过期或不存在
|
|
2918
|
+
let needGen = !existsSync(keyPath) || !existsSync(certPath);
|
|
2919
|
+
if (!needGen) {
|
|
2920
|
+
try {
|
|
2921
|
+
const certPem = readFileSync(certPath, 'utf-8');
|
|
2922
|
+
const x509 = new X509Certificate(certPem);
|
|
2923
|
+
needGen = new Date(x509.validTo) < new Date();
|
|
2924
|
+
} catch { needGen = true; }
|
|
2925
|
+
}
|
|
2926
|
+
if (needGen) {
|
|
2927
|
+
mkdirSync(certDir, { recursive: true });
|
|
2928
|
+
_execSync(`openssl req -x509 -newkey rsa:2048 -keyout "${keyPath}" -out "${certPath}" -days 365 -nodes -subj "/CN=localhost"`, { stdio: 'ignore', timeout: 10000 });
|
|
2929
|
+
}
|
|
2930
|
+
if (existsSync(keyPath) && existsSync(certPath)) {
|
|
2931
|
+
httpsOptions = { key: readFileSync(keyPath), cert: readFileSync(certPath) };
|
|
2932
|
+
}
|
|
2933
|
+
} catch (err) {
|
|
2934
|
+
// openssl 不可用或生成失败,继续用 HTTP
|
|
2935
|
+
if (process.env.CXV_DEBUG) console.error('[CX Viewer] Self-signed cert generation failed:', err.message);
|
|
2936
|
+
}
|
|
2937
|
+
}
|
|
2938
|
+
|
|
2939
|
+
const useHttps = !!httpsOptions && !isCliMode;
|
|
2731
2940
|
const protocol = useHttps ? 'https' : 'http';
|
|
2732
2941
|
serverProtocol = protocol;
|
|
2733
|
-
|
|
2942
|
+
publicAccessProtocol = protocol;
|
|
2943
|
+
if (useHttps) console.error(httpsFromPlugin ? '[CX Viewer] HTTPS mode enabled via plugin hook' : '[CX Viewer] HTTPS mode enabled via self-signed certificate');
|
|
2734
2944
|
|
|
2735
2945
|
return new Promise((resolve, reject) => {
|
|
2736
2946
|
function tryListen(port) {
|
|
@@ -2765,7 +2975,38 @@ export async function startViewer() {
|
|
|
2765
2975
|
currentServer.listen(port, HOST, async () => {
|
|
2766
2976
|
server = currentServer;
|
|
2767
2977
|
actualPort = port;
|
|
2978
|
+
publicAccessPort = port;
|
|
2768
2979
|
const url = `${serverProtocol}://127.0.0.1:${port}`;
|
|
2980
|
+
if (isCliMode && httpsOptions) {
|
|
2981
|
+
const httpsPort = await findAvailablePort(port + 1);
|
|
2982
|
+
if (httpsPort) {
|
|
2983
|
+
try {
|
|
2984
|
+
secondaryServer = createHttpsServer(httpsOptions, handleRequest);
|
|
2985
|
+
await new Promise((resolveSecondary, rejectSecondary) => {
|
|
2986
|
+
secondaryServer.once('error', rejectSecondary);
|
|
2987
|
+
secondaryServer.listen(httpsPort, HOST, () => {
|
|
2988
|
+
secondaryServer.off('error', rejectSecondary);
|
|
2989
|
+
resolveSecondary();
|
|
2990
|
+
});
|
|
2991
|
+
});
|
|
2992
|
+
secondaryPort = httpsPort;
|
|
2993
|
+
publicAccessProtocol = 'https';
|
|
2994
|
+
publicAccessPort = httpsPort;
|
|
2995
|
+
console.error('[CX Viewer] Secondary HTTPS access enabled for QR/remote browser');
|
|
2996
|
+
console.error(` ➜ HTTPS: https://127.0.0.1:${httpsPort}`);
|
|
2997
|
+
const _ips = getAllLocalIps();
|
|
2998
|
+
for (const _ip of _ips) {
|
|
2999
|
+
console.error(` ➜ HTTPS: https://${_ip}:${httpsPort}?token=${ACCESS_TOKEN}`);
|
|
3000
|
+
}
|
|
3001
|
+
} catch (err) {
|
|
3002
|
+
secondaryServer = null;
|
|
3003
|
+
secondaryPort = 0;
|
|
3004
|
+
publicAccessProtocol = serverProtocol;
|
|
3005
|
+
publicAccessPort = port;
|
|
3006
|
+
console.error('[CX Viewer] Secondary HTTPS server creation failed:', err.message);
|
|
3007
|
+
}
|
|
3008
|
+
}
|
|
3009
|
+
}
|
|
2769
3010
|
if (!isCliMode) {
|
|
2770
3011
|
console.error(t('server.started'));
|
|
2771
3012
|
console.error(t('server.startedLocal', { protocol: serverProtocol, port }));
|
|
@@ -2773,17 +3014,23 @@ export async function startViewer() {
|
|
|
2773
3014
|
for (const _ip of _ips) {
|
|
2774
3015
|
console.error(t('server.startedNetwork', { protocol: serverProtocol, ip: _ip, port, token: ACCESS_TOKEN }));
|
|
2775
3016
|
}
|
|
3017
|
+
} else if (secondaryPort) {
|
|
3018
|
+
console.error(t('server.started'));
|
|
3019
|
+
console.error(t('server.startedLocal', { protocol: serverProtocol, port }));
|
|
3020
|
+
console.error(t('server.startedNetwork', { protocol: 'https', ip: getLocalIp(), port: secondaryPort, token: ACCESS_TOKEN }));
|
|
2776
3021
|
}
|
|
2777
3022
|
// v2.0.69 之前的版本会清空控制台,自动打开浏览器确保用户能看到界面
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
3023
|
+
if (!isCliMode) {
|
|
3024
|
+
try {
|
|
3025
|
+
const ccPkgPath = join(__dirname, '..', '@openai', 'codex', 'package.json');
|
|
3026
|
+
const ccVer = JSON.parse(readFileSync(ccPkgPath, 'utf-8')).version;
|
|
3027
|
+
const [maj, min, pat] = ccVer.split('.').map(Number);
|
|
3028
|
+
if (maj < 2 || (maj === 2 && min === 0 && pat < 69)) {
|
|
3029
|
+
const cmd = platform() === 'darwin' ? 'open' : platform() === 'win32' ? 'start' : 'xdg-open';
|
|
3030
|
+
execAsync(`${cmd} ${url}`, { timeout: 5000 }).catch(() => {});
|
|
3031
|
+
}
|
|
3032
|
+
} catch { }
|
|
3033
|
+
}
|
|
2787
3034
|
// 工作区模式下延迟到选择工作区后再启动监听
|
|
2788
3035
|
if (!isWorkspaceMode) {
|
|
2789
3036
|
readModelContextSize(); // Cache model→size mapping at startup
|
|
@@ -2794,9 +3041,12 @@ export async function startViewer() {
|
|
|
2794
3041
|
// CLI 模式下启动 WebSocket 服务 (必须 await,否则插件 hook 拿不到 upgrade listeners)
|
|
2795
3042
|
if (isCliMode) {
|
|
2796
3043
|
await setupTerminalWebSocket(currentServer);
|
|
3044
|
+
if (secondaryServer) {
|
|
3045
|
+
await setupTerminalWebSocket(secondaryServer);
|
|
3046
|
+
}
|
|
2797
3047
|
}
|
|
2798
3048
|
// 通知插件服务器已启动
|
|
2799
|
-
|
|
3049
|
+
refreshPluginRuntime()
|
|
2800
3050
|
.catch(err => console.error('[CX Viewer] Plugin serverStarted hook error:', err.message));
|
|
2801
3051
|
resolve(server);
|
|
2802
3052
|
});
|
|
@@ -2818,7 +3068,7 @@ export async function startViewer() {
|
|
|
2818
3068
|
async function setupTerminalWebSocket(httpServer) {
|
|
2819
3069
|
try {
|
|
2820
3070
|
const { WebSocketServer } = await import('ws');
|
|
2821
|
-
const { writeToPty, writeToPtySequential, resizePty, onPtyData, onPtyExit, getPtyState, getOutputBuffer, getCurrentWorkspace, spawnShell } = await import('./pty-manager.js');
|
|
3071
|
+
const { writeToPty, writeToPtySequential, resizePty, onPtyData, onPtyExit, getPtyState, getOutputBuffer, getOutputHistoryId, getCurrentWorkspace, spawnShell } = await import('./pty-manager.js');
|
|
2822
3072
|
_writeToPty = writeToPty;
|
|
2823
3073
|
_onPtyData = onPtyData;
|
|
2824
3074
|
const wss = new WebSocketServer({ noServer: true });
|
|
@@ -2854,15 +3104,25 @@ async function setupTerminalWebSocket(httpServer) {
|
|
|
2854
3104
|
}
|
|
2855
3105
|
});
|
|
2856
3106
|
|
|
2857
|
-
wss.on('connection', (ws) => {
|
|
3107
|
+
wss.on('connection', (ws, req) => {
|
|
3108
|
+
const sendReplayData = (data) => {
|
|
3109
|
+
const CHUNK_SIZE = 64 * 1024;
|
|
3110
|
+
for (let i = 0; i < data.length; i += CHUNK_SIZE) {
|
|
3111
|
+
if (ws.readyState !== 1) break;
|
|
3112
|
+
ws.send(JSON.stringify({ type: 'data', data: data.slice(i, i + CHUNK_SIZE) }));
|
|
3113
|
+
}
|
|
3114
|
+
};
|
|
3115
|
+
|
|
2858
3116
|
// 发送当前 PTY 状态
|
|
2859
3117
|
const state = getPtyState();
|
|
2860
|
-
|
|
3118
|
+
const historyId = getOutputHistoryId();
|
|
3119
|
+
ws.send(JSON.stringify({ type: 'state', ...state, historyId }));
|
|
2861
3120
|
|
|
2862
3121
|
// 发送历史输出缓冲
|
|
3122
|
+
const clientHistoryId = new URL(req.url, `${serverProtocol}://${req.headers.host}`).searchParams.get('historyId');
|
|
2863
3123
|
const buffer = getOutputBuffer();
|
|
2864
|
-
if (buffer) {
|
|
2865
|
-
|
|
3124
|
+
if (buffer && historyId !== clientHistoryId) {
|
|
3125
|
+
sendReplayData(buffer);
|
|
2866
3126
|
}
|
|
2867
3127
|
|
|
2868
3128
|
// PTY 输出 → WebSocket
|
|
@@ -3112,6 +3372,7 @@ export function stopViewer() {
|
|
|
3112
3372
|
}
|
|
3113
3373
|
async function _doStop() {
|
|
3114
3374
|
try { await Promise.race([runParallelHook('serverStopping'), new Promise(r => setTimeout(r, 3000))]); } catch { }
|
|
3375
|
+
pluginRoutes = [];
|
|
3115
3376
|
// 如果用户未做选择,将临时文件转为正式文件
|
|
3116
3377
|
if (_resumeState && _resumeState.tempFile) {
|
|
3117
3378
|
try {
|
|
@@ -3154,8 +3415,14 @@ async function _doStop() {
|
|
|
3154
3415
|
|
|
3155
3416
|
// ─── SDK Mode Exports ──────────────────────────────────────────
|
|
3156
3417
|
|
|
3157
|
-
/** Push a JSONL entry to all SSE clients (for SDK mode). */
|
|
3418
|
+
/** Push a JSONL entry to the active log and all SSE clients (for SDK mode). */
|
|
3158
3419
|
export function pushSdkEntry(entry) {
|
|
3420
|
+
if (entry && LOG_FILE) {
|
|
3421
|
+
try {
|
|
3422
|
+
appendFileSync(LOG_FILE, JSON.stringify(entry) + '\n---\n');
|
|
3423
|
+
notifyStatsWorker(LOG_FILE);
|
|
3424
|
+
} catch {}
|
|
3425
|
+
}
|
|
3159
3426
|
if (sendToClients) sendToClients(clients, entry);
|
|
3160
3427
|
}
|
|
3161
3428
|
|