cc-viewer 1.9.8 → 1.9.10
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/README.md +1 -1
- package/cli.js +5 -3
- package/dist/assets/App-Ci9GYs24.js +2 -0
- package/dist/assets/App-D06hBGII.css +1 -0
- package/dist/assets/{MdxEditorPanel-BnbZpgDp.js → MdxEditorPanel-8fwlNHMz.js} +1 -1
- package/dist/assets/Mobile-DngSOqYr.js +1 -0
- package/dist/assets/ProxyStatsModal-CWSIELLj.js +1 -0
- package/dist/assets/{_baseUniq-CwQykUuc.js → _baseUniq-6ZLcdMmL.js} +1 -1
- package/dist/assets/{arc-EdUdBxtG.js → arc-Dirf6EZP.js} +1 -1
- package/dist/assets/{architectureDiagram-Q4EWVU46-DtBIdElY.js → architectureDiagram-Q4EWVU46-BfIhmu_I.js} +1 -1
- package/dist/assets/{blockDiagram-DXYQGD6D-guNZIKkZ.js → blockDiagram-DXYQGD6D-D0y4ALi1.js} +1 -1
- package/dist/assets/{c4Diagram-AHTNJAMY-xV5vB-Z9.js → c4Diagram-AHTNJAMY-Dlfp4SUK.js} +1 -1
- package/dist/assets/{channel-DYEktNap.js → channel-CaNcU8SI.js} +1 -1
- package/dist/assets/{chunk-4BX2VUAB-ZTkItpvj.js → chunk-4BX2VUAB-ki0vHNS-.js} +1 -1
- package/dist/assets/{chunk-4TB4RGXK-dYIewyyR.js → chunk-4TB4RGXK-D8yO8Iuv.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-CSsPNgPX.js → chunk-55IACEB6-BvIgaNPO.js} +1 -1
- package/dist/assets/{chunk-EDXVE4YY-CnMiuqzQ.js → chunk-EDXVE4YY-DF2Of_VO.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-CVpATPZ3.js → chunk-FMBD7UC4-BIw7SXs-.js} +1 -1
- package/dist/assets/{chunk-OYMX7WX6-Dpq4s8dW.js → chunk-OYMX7WX6-AB76s0A0.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-Bnojh6X6.js → chunk-QZHKN3VN-CuN2LCFc.js} +1 -1
- package/dist/assets/{chunk-YZCP3GAM-BYqK4Dsa.js → chunk-YZCP3GAM-D4DLBzBN.js} +1 -1
- package/dist/assets/classDiagram-6PBFFD2Q-DLJzWgYm.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-DLJzWgYm.js +1 -0
- package/dist/assets/clone-H_VGsHn9.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-OJ7mmWyV.js → cose-bilkent-S5V4N54A-Bn485PX9.js} +1 -1
- package/dist/assets/{dagre-KV5264BT-B1Kwd_Rk.js → dagre-KV5264BT-e4QVCf17.js} +1 -1
- package/dist/assets/{diagram-5BDNPKRD-D8qrw8sp.js → diagram-5BDNPKRD-_SBFGgnf.js} +1 -1
- package/dist/assets/{diagram-G4DWMVQ6-ip-N4ho8.js → diagram-G4DWMVQ6-Bz1ovxLs.js} +1 -1
- package/dist/assets/{diagram-MMDJMWI5-2PZaskUF.js → diagram-MMDJMWI5-B-oojW8K.js} +1 -1
- package/dist/assets/{diagram-TYMM5635-CeAOQUZP.js → diagram-TYMM5635-CTAyNaOj.js} +1 -1
- package/dist/assets/{erDiagram-SMLLAGMA-BCBaEYEb.js → erDiagram-SMLLAGMA-BfvKAqq2.js} +1 -1
- package/dist/assets/{flowDiagram-DWJPFMVM-CsGyIqcX.js → flowDiagram-DWJPFMVM-BpTEZEAe.js} +1 -1
- package/dist/assets/{ganttDiagram-T4ZO3ILL-COsj3FZ-.js → ganttDiagram-T4ZO3ILL-DUD_vkyE.js} +1 -1
- package/dist/assets/{gitGraphDiagram-UUTBAWPF-DvQVQGJG.js → gitGraphDiagram-UUTBAWPF-DnlXcTud.js} +1 -1
- package/dist/assets/{graph-BSB9fDSP.js → graph-D4xsUKSn.js} +1 -1
- package/dist/assets/{index-DKufsAzx.js → index-84-BjpGl.js} +1 -1
- package/dist/assets/index-BDKwOnE3.js +2 -0
- package/dist/assets/{index-Bo-12bug.js → index-BniBLRtZ.js} +1 -1
- package/dist/assets/{index-BRw4Iz2O.js → index-C2Zo3KOg.js} +1 -1
- package/dist/assets/{index-BuVComcY.js → index-CWZoKT7e.js} +1 -1
- package/dist/assets/{index-eM2ih5n7.js → index-D-4EsFub.js} +1 -1
- package/dist/assets/{index-CNNAYjkF.js → index-D3KzRmkP.js} +1 -1
- package/dist/assets/{index-DXBfwPYD.js → index-NGgdum2i.js} +1 -1
- package/dist/assets/{infoDiagram-42DDH7IO-eNucS4iW.js → infoDiagram-42DDH7IO-BbdvmCDt.js} +1 -1
- package/dist/assets/{ishikawaDiagram-UXIWVN3A-Dxxj-gUX.js → ishikawaDiagram-UXIWVN3A-oZBOPoTO.js} +1 -1
- package/dist/assets/{journeyDiagram-VCZTEJTY-yJJzVEBb.js → journeyDiagram-VCZTEJTY-DWoH0hs9.js} +1 -1
- package/dist/assets/{jszip.min-ByI0ds5u.js → jszip.min-I5y9RZhq.js} +1 -1
- package/dist/assets/{kanban-definition-6JOO6SKY-2rfTi2zw.js → kanban-definition-6JOO6SKY-BLHtjKsS.js} +1 -1
- package/dist/assets/{layout-DTmHj0Wm.js → layout-C116SNcQ.js} +1 -1
- package/dist/assets/{linear-BOA3a9FE.js → linear-Br08WCkF.js} +1 -1
- package/dist/assets/{mermaid.core-5H-NR79H.js → mermaid.core-D-4dTrvI.js} +2 -2
- package/dist/assets/{min-CpjFTEec.js → min-khs74HbH.js} +1 -1
- package/dist/assets/{mindmap-definition-QFDTVHPH-B8KCjFgB.js → mindmap-definition-QFDTVHPH-C3_yItFF.js} +1 -1
- package/dist/assets/{pieDiagram-DEJITSTG-BARy96g5.js → pieDiagram-DEJITSTG-ByCwM2_P.js} +1 -1
- package/dist/assets/{quadrantDiagram-34T5L4WZ-DYoTapuI.js → quadrantDiagram-34T5L4WZ-CjV65X1r.js} +1 -1
- package/dist/assets/{requirementDiagram-MS252O5E-EeANLLK2.js → requirementDiagram-MS252O5E-DFJ_dbtV.js} +1 -1
- package/dist/assets/{sankeyDiagram-XADWPNL6-DmQ-YVPj.js → sankeyDiagram-XADWPNL6-BBvXoBfj.js} +1 -1
- package/dist/assets/seqResourceLoaders-C34z9uOa.css +41 -0
- package/dist/assets/seqResourceLoaders-JJzgZZzU.js +2 -0
- package/dist/assets/{sequenceDiagram-FGHM5R23-SZtW0rYC.js → sequenceDiagram-FGHM5R23-BJM04Y3N.js} +1 -1
- package/dist/assets/{stateDiagram-FHFEXIEX-CEygsIld.js → stateDiagram-FHFEXIEX-DI2q_Kkn.js} +1 -1
- package/dist/assets/{stateDiagram-v2-QKLJ7IA2-BU1XPhBe.js → stateDiagram-v2-QKLJ7IA2-CrGC6kEY.js} +1 -1
- package/dist/assets/{timeline-definition-GMOUNBTQ-DhPBaQBt.js → timeline-definition-GMOUNBTQ-JcfRUsjD.js} +1 -1
- package/dist/assets/vendor-antd-DxEVqVhD.js +17 -0
- package/dist/assets/vendor-codemirror-CgNlkZva.js +1 -0
- package/dist/assets/{vendor-mdxeditor-Cz09a1_I.js → vendor-mdxeditor-DoreQf4A.js} +2 -2
- package/dist/assets/{vendor-qrcode-qbbntD6-.js → vendor-qrcode-q1CKtOyX.js} +1 -1
- package/dist/assets/{vendor-virtuoso-Sk8fQ4Hx.js → vendor-virtuoso-BXGe9q7E.js} +1 -1
- package/dist/assets/{vennDiagram-DHZGUBPP-NniTHxDw.js → vennDiagram-DHZGUBPP-mAHR1Ugj.js} +1 -1
- package/dist/assets/{wardley-RL74JXVD-D9SNOrDw.js → wardley-RL74JXVD-mlTq_P-4.js} +1 -1
- package/dist/assets/{wardleyDiagram-NUSXRM2D-DvHXKNza.js → wardleyDiagram-NUSXRM2D-BDGfajRb.js} +1 -1
- package/dist/assets/{xychartDiagram-5P7HB3ND-CaiwPTQB.js → xychartDiagram-5P7HB3ND-Cf2pR1mV.js} +1 -1
- package/dist/index.html +4 -4
- package/package.json +1 -1
- package/plugins/README.md +9 -1
- package/server/interceptor.js +1 -1
- package/server/lib/builtin-model-prompts.js +1 -1
- package/server/lib/chat-queue.js +1 -1
- package/server/lib/credential-access.js +1 -1
- package/server/lib/credential-migrate.js +1 -1
- package/server/lib/error-report.js +26 -0
- package/server/lib/json-store.js +1 -1
- package/server/lib/launch-config.js +1 -1
- package/server/lib/log-watcher.js +1 -1
- package/server/lib/plugin-host.mjs +198 -0
- package/server/lib/plugin-loader.js +346 -90
- package/server/lib/plugin-manager.js +46 -2
- package/server/lib/plugin-manifest.js +329 -0
- package/server/lib/plugin-panels.js +183 -0
- package/server/lib/plugin-proxy.js +408 -0
- package/server/lib/plugin-runtime.js +775 -0
- package/server/lib/plugin-service-manager.js +463 -0
- package/server/lib/proxy/proxy-retry.js +1 -1
- package/server/lib/proxy/proxy-stats.js +1 -1
- package/server/lib/resume-watchdog.js +1 -1
- package/server/lib/same-origin.js +13 -3
- package/server/lib/sdk-manager.js +117 -23
- package/server/lib/sdk-session-index.js +1 -1
- package/server/lib/stats-worker.js +1 -1
- package/server/lib/system-prompt-files.js +1 -1
- package/server/lib/system-prompt-live.js +1 -1
- package/server/lib/system-prompt-snapshots.js +1 -1
- package/server/lib/v2/adapter.js +1 -1
- package/server/lib/v2/convert.js +1 -1
- package/server/lib/v2/jsonl-read.js +1 -1
- package/server/lib/v2/live-feed.js +1 -1
- package/server/lib/v2/meta-rows.js +1 -1
- package/server/lib/v2/resume-list.js +1 -1
- package/server/lib/v2/session-list.js +1 -1
- package/server/lib/v2/session-owner.js +1 -1
- package/server/lib/v2/v2-writer.js +1 -1
- package/server/lib/wire-compress.js +1 -1
- package/server/lib/ws-mux.js +1 -1
- package/server/proxy.js +1 -1
- package/server/pty-manager.js +1 -1
- package/server/routes/ask-perm.js +1 -1
- package/server/routes/events.js +1 -1
- package/server/routes/expert.js +1 -1
- package/server/routes/files-content.js +1 -1
- package/server/routes/files-fs.js +4 -2
- package/server/routes/misc.js +11 -0
- package/server/routes/plugin-services.js +104 -0
- package/server/routes/plugins.js +145 -17
- package/server/routes/preferences.js +1 -1
- package/server/routes/resume.js +1 -1
- package/server/routes/sdk-context.js +1 -1
- package/server/routes/sdk-mcp-status.js +1 -1
- package/server/routes/workspaces.js +1 -1
- package/server/server.js +370 -173
- package/dist/assets/App-D2zrtS4P.js +0 -2
- package/dist/assets/App-DuDeDOuX.css +0 -1
- package/dist/assets/Mobile-CU-g7cTn.js +0 -1
- package/dist/assets/ProxyStatsModal-CavWQfBA.js +0 -1
- package/dist/assets/classDiagram-6PBFFD2Q-ClGF33bF.js +0 -1
- package/dist/assets/classDiagram-v2-HSJHXN6E-ClGF33bF.js +0 -1
- package/dist/assets/clone-CFj55h49.js +0 -1
- package/dist/assets/index-D2maXxkC.js +0 -2
- package/dist/assets/seqResourceLoaders-DZF6ofB7.css +0 -41
- package/dist/assets/seqResourceLoaders-jOSz1x7F.js +0 -2
- package/dist/assets/vendor-antd-CS_S2mBN.js +0 -17
- package/dist/assets/vendor-codemirror-B5l-VbBO.js +0 -1
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
import { readdirSync, existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { pathToFileURL } from 'node:url';
|
|
4
|
+
import { randomBytes } from 'node:crypto';
|
|
4
5
|
import { LOG_DIR } from '../../findcc.js';
|
|
5
6
|
import { PLUGINS_DIR } from '../_paths.js';
|
|
7
|
+
import {
|
|
8
|
+
parsePluginManifest,
|
|
9
|
+
computeMainFingerprint,
|
|
10
|
+
computePanelFingerprint,
|
|
11
|
+
listPluginDirs,
|
|
12
|
+
validatePluginName,
|
|
13
|
+
validateServicePrefix,
|
|
14
|
+
} from './plugin-manifest.js';
|
|
15
|
+
import { reportSwallowed } from './error-report.js';
|
|
6
16
|
|
|
7
17
|
// 动态获取(LOG_DIR 可能在运行时被 setLogDir 修改)
|
|
8
18
|
export function getPluginsDir() { return join(LOG_DIR, 'plugins'); }
|
|
@@ -24,81 +34,280 @@ const HOOK_TYPES = {
|
|
|
24
34
|
};
|
|
25
35
|
|
|
26
36
|
let _plugins = [];
|
|
37
|
+
// Full result of the last loadPlugins (directory service/panel plugins and failures included)
|
|
38
|
+
let _entries = [];
|
|
39
|
+
let _generation = 0;
|
|
40
|
+
// Directory plugins with an unchanged fingerprint reuse the old module instance and import URL to avoid repeated reloads
|
|
41
|
+
// accumulate module namespaces in the ESM registry that are never released
|
|
42
|
+
const _moduleCache = new Map(); // mainPath -> { fingerprint, url, module }
|
|
27
43
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
export
|
|
32
|
-
|
|
44
|
+
// Runtime event dispatcher (bridge for ctx.events.on): loadPlugins rebuilds _plugins every time,
|
|
45
|
+
// and the loader re-attaches this after each scan so it stays online across hot reloads.
|
|
46
|
+
let _eventDispatcherPlugin = null;
|
|
47
|
+
export function registerEventDispatcherPlugin(plugin) {
|
|
48
|
+
_eventDispatcherPlugin = plugin;
|
|
49
|
+
if (plugin && !_plugins.some(p => p.name === plugin.name)) {
|
|
50
|
+
_plugins.push(plugin);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
33
53
|
|
|
34
|
-
|
|
35
|
-
let disabledPlugins = [];
|
|
54
|
+
function readDisabledPlugins() {
|
|
36
55
|
try {
|
|
37
56
|
if (existsSync(getPrefsFilePath())) {
|
|
38
57
|
const prefs = JSON.parse(readFileSync(getPrefsFilePath(), 'utf-8'));
|
|
39
58
|
if (Array.isArray(prefs.disabledPlugins)) {
|
|
40
|
-
|
|
59
|
+
return prefs.disabledPlugins;
|
|
41
60
|
}
|
|
42
61
|
}
|
|
43
|
-
} catch {
|
|
62
|
+
} catch (err) {
|
|
63
|
+
reportSwallowed('plugin-loader.prefs', err);
|
|
64
|
+
}
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
44
67
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
68
|
+
async function importMainModule(mainPath, fingerprint, force) {
|
|
69
|
+
const cacheKey = mainPath;
|
|
70
|
+
const cached = _moduleCache.get(cacheKey);
|
|
71
|
+
if (!force && cached && cached.fingerprint === fingerprint) {
|
|
72
|
+
return { module: cached.module, url: cached.url, reused: true };
|
|
73
|
+
}
|
|
74
|
+
const tag = force ? randomBytes(8).toString('hex') : fingerprint.slice(0, 16);
|
|
75
|
+
// Fingerprint cache-bust: pathToFileURL wraps the absolute path (Windows-safe); a new query yields a new module instance
|
|
76
|
+
const mod = await import(pathToFileURL(mainPath).href + '?ccv=' + tag);
|
|
77
|
+
const url = `${pathToFileURL(mainPath).href}?ccv=${tag}`;
|
|
78
|
+
const module = mod.default || mod;
|
|
79
|
+
_moduleCache.set(cacheKey, { fingerprint, url, module });
|
|
80
|
+
return { module, url, reused: false };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Scan plugin directories (user LOG_DIR/plugins/ and the bundled plugins/) and load every plugin.
|
|
85
|
+
* - Top-level .js/.mjs: single-file hook plugins (legacy behavior unchanged)
|
|
86
|
+
* - Top-level directories with plugin.json: directory plugins (hooks optional; service/panel/lifecycle allowed)
|
|
87
|
+
* Never rejects: a single failure is recorded in that entry's loadError.
|
|
88
|
+
*/
|
|
89
|
+
export async function loadPlugins(opts = {}) {
|
|
90
|
+
const force = opts.force === true;
|
|
91
|
+
_plugins = [];
|
|
92
|
+
_entries = [];
|
|
93
|
+
if (force) _moduleCache.clear();
|
|
94
|
+
|
|
95
|
+
const disabledPlugins = readDisabledPlugins();
|
|
96
|
+
const seenNames = new Set();
|
|
97
|
+
const usedPrefixes = new Set();
|
|
98
|
+
|
|
99
|
+
const loadFilePlugin = async (dir, file, source) => {
|
|
100
|
+
const filePath = join(dir, file);
|
|
101
|
+
const entry = {
|
|
102
|
+
name: file,
|
|
103
|
+
source,
|
|
104
|
+
shape: 'file',
|
|
105
|
+
file,
|
|
106
|
+
dir: null,
|
|
107
|
+
pluginDir: null,
|
|
108
|
+
manifest: null,
|
|
109
|
+
plugin: null,
|
|
110
|
+
service: null,
|
|
111
|
+
panel: null,
|
|
112
|
+
fingerprints: null,
|
|
113
|
+
enabled: true,
|
|
114
|
+
loadError: null,
|
|
115
|
+
};
|
|
48
116
|
try {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
117
|
+
// On Windows `file://${C:\...}` yields `file://C:\...` (no third slash, unescaped backslashes);
|
|
118
|
+
// pathToFileURL guarantees the correct cross-platform form. On POSIX it matches the old string concatenation.
|
|
119
|
+
const mod = await import(pathToFileURL(filePath).href);
|
|
120
|
+
const plugin = mod.default || mod;
|
|
121
|
+
const name = plugin.name || (source === 'bundled' ? file.replace(/\.[cm]?js$/, '') : file);
|
|
122
|
+
entry.name = name;
|
|
123
|
+
entry.plugin = plugin;
|
|
55
124
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
125
|
+
if (plugin.service !== undefined) {
|
|
126
|
+
entry.loadError = 'single-file plugins cannot declare "service"; use a directory plugin with plugin.json';
|
|
127
|
+
if (SHOULD_LOG) console.error(`[CC Viewer] Plugin "${name}": ${entry.loadError}`);
|
|
128
|
+
_entries.push(entry);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (disabledPlugins.includes(name)) {
|
|
132
|
+
entry.enabled = false;
|
|
133
|
+
if (SHOULD_LOG) console.error(`[CC Viewer] Plugin "${name}" is disabled, skipping.`);
|
|
134
|
+
_entries.push(entry);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (source === 'user') seenNames.add(name);
|
|
138
|
+
if (plugin.hooks && typeof plugin.hooks === 'object') {
|
|
139
|
+
const alreadyLoaded = source === 'bundled' && _plugins.some(p => p.name === name);
|
|
140
|
+
if (!alreadyLoaded) {
|
|
141
|
+
_plugins.push({ name, hooks: plugin.hooks, file, ...(source === 'bundled' ? { source } : {}) });
|
|
142
|
+
if (SHOULD_LOG) console.error(`[CC Viewer] Plugin loaded: ${name} (${file})`);
|
|
68
143
|
}
|
|
144
|
+
}
|
|
145
|
+
_entries.push(entry);
|
|
146
|
+
} catch (err) {
|
|
147
|
+
entry.loadError = err.message;
|
|
148
|
+
if (SHOULD_LOG) console.error(`[CC Viewer] Failed to load plugin "${file}":`, err.message);
|
|
149
|
+
_entries.push(entry);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
69
152
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
153
|
+
const loadDirPlugin = async (parentDir, dirName, source) => {
|
|
154
|
+
const pluginDir = join(parentDir, dirName);
|
|
155
|
+
const entry = {
|
|
156
|
+
name: dirName,
|
|
157
|
+
source,
|
|
158
|
+
shape: 'dir',
|
|
159
|
+
file: null,
|
|
160
|
+
dir: dirName,
|
|
161
|
+
pluginDir,
|
|
162
|
+
manifest: null,
|
|
163
|
+
plugin: null,
|
|
164
|
+
service: null,
|
|
165
|
+
panel: null,
|
|
166
|
+
fingerprints: null,
|
|
167
|
+
enabled: true,
|
|
168
|
+
loadError: null,
|
|
169
|
+
};
|
|
170
|
+
const { manifest, error } = parsePluginManifest(pluginDir);
|
|
171
|
+
if (error) {
|
|
172
|
+
// Directories without plugin.json: ignored (may be a user's asset folder)
|
|
173
|
+
if (error === 'missing plugin.json') return;
|
|
174
|
+
entry.loadError = error;
|
|
175
|
+
_entries.push(entry);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
entry.manifest = manifest;
|
|
179
|
+
entry.name = manifest.name;
|
|
180
|
+
|
|
181
|
+
if (manifest.panel) {
|
|
182
|
+
entry.panel = {
|
|
183
|
+
entryRel: manifest.panel.entryRel,
|
|
184
|
+
entryPath: manifest.panel.entryPath,
|
|
185
|
+
rootDir: manifest.panel.rootDir,
|
|
186
|
+
cssRel: manifest.panel.cssRel,
|
|
187
|
+
cssPath: manifest.panel.cssPath,
|
|
188
|
+
title: manifest.panel.title,
|
|
189
|
+
titleI18n: manifest.panel.titleI18n,
|
|
190
|
+
icon: manifest.panel.icon,
|
|
191
|
+
width: manifest.panel.width,
|
|
192
|
+
desktopOnly: manifest.panel.desktopOnly,
|
|
193
|
+
fingerprint: computePanelFingerprint(manifest.panel),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
if (manifest.service) {
|
|
197
|
+
entry.service = {
|
|
198
|
+
entryRel: manifest.service.entryRel,
|
|
199
|
+
entryPath: manifest.service.entryPath,
|
|
200
|
+
prefix: manifest.service.prefix,
|
|
201
|
+
env: manifest.service.env,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (disabledPlugins.includes(manifest.name)) {
|
|
206
|
+
entry.enabled = false;
|
|
207
|
+
_entries.push(entry);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (!validatePluginName(manifest.name)) {
|
|
211
|
+
entry.loadError = `invalid plugin name: ${manifest.name}`;
|
|
212
|
+
_entries.push(entry);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (seenNames.has(manifest.name)) {
|
|
216
|
+
// User source overrides bundled source: a same-named bundled directory plugin silently yields (same as single-file plugins)
|
|
217
|
+
if (source === 'bundled') return;
|
|
218
|
+
entry.enabled = false;
|
|
219
|
+
entry.loadError = `duplicate plugin name: ${manifest.name}`;
|
|
220
|
+
_entries.push(entry);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (manifest.service) {
|
|
224
|
+
if (!validateServicePrefix(manifest.service.prefix) || usedPrefixes.has(manifest.service.prefix)) {
|
|
225
|
+
entry.loadError = `invalid or duplicate service.prefix: ${manifest.service.prefix}`;
|
|
226
|
+
_entries.push(entry);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// main module (optional): hooks + activate/deactivate/onError
|
|
232
|
+
let plugin = null;
|
|
233
|
+
if (manifest.mainPath) {
|
|
234
|
+
const fingerprint = computeMainFingerprint(manifest.raw, manifest.mainPath, pluginDir);
|
|
235
|
+
entry.fingerprints = { main: fingerprint };
|
|
236
|
+
try {
|
|
237
|
+
const { module } = await importMainModule(manifest.mainPath, fingerprint, force);
|
|
238
|
+
plugin = module;
|
|
239
|
+
entry.plugin = plugin;
|
|
240
|
+
if (plugin && plugin.name && plugin.name !== manifest.name && SHOULD_LOG) {
|
|
241
|
+
console.error(`[CC Viewer] Plugin "${manifest.name}": module name "${plugin.name}" differs from manifest name, manifest wins.`);
|
|
73
242
|
}
|
|
74
243
|
} catch (err) {
|
|
75
|
-
|
|
244
|
+
entry.loadError = `failed to import ${manifest.mainRel}: ${err.message}`;
|
|
245
|
+
if (SHOULD_LOG) console.error(`[CC Viewer] ${entry.loadError}`);
|
|
246
|
+
_entries.push(entry);
|
|
247
|
+
return;
|
|
76
248
|
}
|
|
77
249
|
}
|
|
250
|
+
|
|
251
|
+
seenNames.add(manifest.name);
|
|
252
|
+
if (manifest.service) usedPrefixes.add(manifest.service.prefix);
|
|
253
|
+
|
|
254
|
+
// hooks still use the same pipeline; service/panel/activate info lives in entries (filled above)
|
|
255
|
+
if (plugin && plugin.hooks && typeof plugin.hooks === 'object') {
|
|
256
|
+
_plugins.push({ name: manifest.name, hooks: plugin.hooks, file: dirName, ...(source === 'bundled' ? { source } : {}) });
|
|
257
|
+
}
|
|
258
|
+
if (SHOULD_LOG) console.error(`[CC Viewer] Directory plugin loaded: ${manifest.name} (${dirName})`);
|
|
259
|
+
_entries.push(entry);
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
// ---- User plugin directory ----
|
|
263
|
+
const userDir = getPluginsDir();
|
|
264
|
+
if (existsSync(userDir)) {
|
|
265
|
+
let dirents;
|
|
266
|
+
try {
|
|
267
|
+
dirents = readdirSync(userDir, { withFileTypes: true });
|
|
268
|
+
} catch {
|
|
269
|
+
dirents = [];
|
|
270
|
+
}
|
|
271
|
+
const files = dirents
|
|
272
|
+
.filter(d => d.isFile() && (d.name.endsWith('.js') || d.name.endsWith('.mjs')))
|
|
273
|
+
.map(d => d.name)
|
|
274
|
+
.sort();
|
|
275
|
+
for (const file of files) {
|
|
276
|
+
await loadFilePlugin(userDir, file, 'user');
|
|
277
|
+
}
|
|
278
|
+
for (const dirName of listPluginDirs(userDir)) {
|
|
279
|
+
await loadDirPlugin(userDir, dirName, 'user');
|
|
280
|
+
}
|
|
78
281
|
}
|
|
79
282
|
|
|
80
|
-
//
|
|
283
|
+
// ---- Bundled plugin directory (user source wins) ----
|
|
81
284
|
const bundledDir = PLUGINS_DIR;
|
|
82
285
|
if (existsSync(bundledDir)) {
|
|
83
|
-
let
|
|
286
|
+
let dirents;
|
|
84
287
|
try {
|
|
85
|
-
|
|
86
|
-
} catch {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if (SHOULD_LOG) console.error(`[CC Viewer] Failed to load bundled plugin "${file}":`, err.message);
|
|
99
|
-
}
|
|
288
|
+
dirents = readdirSync(bundledDir, { withFileTypes: true });
|
|
289
|
+
} catch {
|
|
290
|
+
dirents = [];
|
|
291
|
+
}
|
|
292
|
+
const files = dirents
|
|
293
|
+
.filter(d => d.isFile() && (d.name.endsWith('.js') || d.name.endsWith('.mjs')))
|
|
294
|
+
.map(d => d.name)
|
|
295
|
+
.sort();
|
|
296
|
+
for (const file of files) {
|
|
297
|
+
await loadFilePlugin(bundledDir, file, 'bundled');
|
|
298
|
+
}
|
|
299
|
+
for (const dirName of listPluginDirs(bundledDir)) {
|
|
300
|
+
await loadDirPlugin(bundledDir, dirName, 'bundled');
|
|
100
301
|
}
|
|
101
302
|
}
|
|
303
|
+
|
|
304
|
+
// Re-attach the runtime's event dispatcher (_plugins was cleared at the top)
|
|
305
|
+
if (_eventDispatcherPlugin && !_plugins.some(p => p.name === _eventDispatcherPlugin.name)) {
|
|
306
|
+
_plugins.push(_eventDispatcherPlugin);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
_generation += 1;
|
|
310
|
+
return { generation: _generation, entries: _entries };
|
|
102
311
|
}
|
|
103
312
|
|
|
104
313
|
/**
|
|
@@ -140,55 +349,102 @@ export async function runParallelHook(name, context = {}) {
|
|
|
140
349
|
await Promise.all(tasks);
|
|
141
350
|
}
|
|
142
351
|
|
|
352
|
+
// ---- Structured access for plugin-runtime ----
|
|
353
|
+
|
|
354
|
+
export function getPluginEntries() {
|
|
355
|
+
return _entries;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export function getServiceEntries() {
|
|
359
|
+
return _entries.filter(e => e.enabled && !e.loadError && e.service);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export function getPluginsGeneration() {
|
|
363
|
+
return _generation;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Test-only: reset the module cache (does not affect already imported modules; only later loadPlugins)
|
|
367
|
+
export function _resetModuleCacheForTest() {
|
|
368
|
+
_moduleCache.clear();
|
|
369
|
+
}
|
|
370
|
+
|
|
143
371
|
/**
|
|
144
372
|
* 返回所有插件文件信息(含已禁用的),供 /api/plugins 使用
|
|
145
373
|
*/
|
|
146
374
|
export function getPluginsInfo() {
|
|
147
|
-
|
|
375
|
+
const info = [];
|
|
376
|
+
const disabledPlugins = readDisabledPlugins();
|
|
148
377
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
378
|
+
// Single-file plugins: keep legacy behavior (regex-extract name from source when not loaded)
|
|
379
|
+
if (existsSync(getPluginsDir())) {
|
|
380
|
+
let files;
|
|
381
|
+
try {
|
|
382
|
+
files = readdirSync(getPluginsDir())
|
|
383
|
+
.filter(f => f.endsWith('.js') || f.endsWith('.mjs'))
|
|
384
|
+
.sort();
|
|
385
|
+
} catch {
|
|
386
|
+
files = [];
|
|
156
387
|
}
|
|
157
|
-
} catch { }
|
|
158
388
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
} catch {
|
|
165
|
-
return [];
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
return files.map(file => {
|
|
169
|
-
const loaded = _plugins.find(p => p.file === file);
|
|
170
|
-
let name = file;
|
|
389
|
+
for (const file of files) {
|
|
390
|
+
const loaded = _plugins.find(p => p.file === file);
|
|
391
|
+
const fileEntry = _entries.find(e => e.shape === 'file' && e.file === file && e.source === 'user');
|
|
392
|
+
let name = file;
|
|
393
|
+
let resolvedFromSource = false;
|
|
171
394
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
395
|
+
if (loaded) {
|
|
396
|
+
name = loaded.name;
|
|
397
|
+
} else {
|
|
398
|
+
try {
|
|
399
|
+
const filePath = join(getPluginsDir(), file);
|
|
400
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
401
|
+
// Simple match for name: 'xxx' or name: "xxx"
|
|
402
|
+
const match = content.match(/name\s*:\s*['"]([^'"]+)['"]/);
|
|
403
|
+
if (match) {
|
|
404
|
+
name = match[1];
|
|
405
|
+
resolvedFromSource = true;
|
|
406
|
+
}
|
|
407
|
+
} catch (err) {
|
|
408
|
+
reportSwallowed('plugin-loader.info-read', err);
|
|
409
|
+
}
|
|
410
|
+
if (!resolvedFromSource && fileEntry) {
|
|
411
|
+
name = fileEntry.name;
|
|
184
412
|
}
|
|
185
|
-
} catch {
|
|
186
|
-
// 读取失败,使用文件名
|
|
187
413
|
}
|
|
414
|
+
|
|
415
|
+
const hooks = loaded ? Object.keys(loaded.hooks) : [];
|
|
416
|
+
const enabled = !disabledPlugins.includes(name);
|
|
417
|
+
const item = { name, file, hooks, enabled, type: 'hook' };
|
|
418
|
+
if (fileEntry && fileEntry.loadError) item.loadError = fileEntry.loadError;
|
|
419
|
+
info.push(item);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Directory plugins: merge the last loadPlugins result (manifest reads execute no plugin code,
|
|
424
|
+
// but the list only reflects known state; reload refreshes it)
|
|
425
|
+
for (const entry of _entries) {
|
|
426
|
+
if (entry.shape !== 'dir') continue;
|
|
427
|
+
const item = {
|
|
428
|
+
name: entry.name,
|
|
429
|
+
dir: entry.dir,
|
|
430
|
+
hooks: entry.plugin && entry.plugin.hooks ? Object.keys(entry.plugin.hooks) : [],
|
|
431
|
+
enabled: entry.enabled && !disabledPlugins.includes(entry.name),
|
|
432
|
+
type: entry.service ? 'service' : (entry.panel ? 'panel' : (entry.manifest && entry.manifest.mainPath ? 'hook' : 'dir')),
|
|
433
|
+
source: entry.source,
|
|
434
|
+
};
|
|
435
|
+
if (entry.service) item.service = { prefix: entry.service.prefix };
|
|
436
|
+
if (entry.panel) {
|
|
437
|
+
item.panel = {
|
|
438
|
+
title: entry.panel.title,
|
|
439
|
+
titleI18n: entry.panel.titleI18n,
|
|
440
|
+
icon: entry.panel.icon,
|
|
441
|
+
width: entry.panel.width,
|
|
442
|
+
desktopOnly: entry.panel.desktopOnly,
|
|
443
|
+
};
|
|
188
444
|
}
|
|
445
|
+
if (entry.loadError) item.loadError = entry.loadError;
|
|
446
|
+
info.push(item);
|
|
447
|
+
}
|
|
189
448
|
|
|
190
|
-
|
|
191
|
-
const enabled = !disabledPlugins.includes(name);
|
|
192
|
-
return { name, file, hooks, enabled };
|
|
193
|
-
});
|
|
449
|
+
return info;
|
|
194
450
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, writeFileSync, unlinkSync } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync, unlinkSync, rmSync, realpathSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join, relative, isAbsolute } from 'node:path';
|
|
3
3
|
import { execFile } from 'node:child_process';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
|
+
import { PLUGIN_NAME_RE } from './plugin-manifest.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Upload plugin files from a file list.
|
|
@@ -116,3 +117,46 @@ export async function installPluginFromUrl(pluginsDir, fileUrl, extractNameScrip
|
|
|
116
117
|
writeFileSync(join(pluginsDir, filename), content, 'utf-8');
|
|
117
118
|
return { filename };
|
|
118
119
|
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Delete a directory plugin (service/panel plugin with plugin.json).
|
|
123
|
+
* Validates: slug shape, realpath containment inside pluginsDir (symlink-safe),
|
|
124
|
+
* presence of plugin.json, manifest name matching the directory name.
|
|
125
|
+
* Caller must stop/unload the plugin's service BEFORE calling this (Windows: the
|
|
126
|
+
* child's cwd is the plugin dir, deletion would EBUSY while it runs).
|
|
127
|
+
* @param {string} pluginsDir
|
|
128
|
+
* @param {string} dirName
|
|
129
|
+
* @throws {Error} with statusCode on validation failure
|
|
130
|
+
*/
|
|
131
|
+
export function deletePluginDir(pluginsDir, dirName) {
|
|
132
|
+
if (!dirName || !PLUGIN_NAME_RE.test(dirName)) {
|
|
133
|
+
throw Object.assign(new Error('Invalid directory name'), { statusCode: 400 });
|
|
134
|
+
}
|
|
135
|
+
const target = join(pluginsDir, dirName);
|
|
136
|
+
if (!existsSync(target)) {
|
|
137
|
+
throw Object.assign(new Error('Directory not found'), { statusCode: 404 });
|
|
138
|
+
}
|
|
139
|
+
const realRoot = realpathSync(pluginsDir);
|
|
140
|
+
const realTarget = realpathSync(target);
|
|
141
|
+
const rootCmp = process.platform === 'win32' ? realRoot.toLowerCase() : realRoot;
|
|
142
|
+
const targetCmp = process.platform === 'win32' ? realTarget.toLowerCase() : realTarget;
|
|
143
|
+
const rel = relative(rootCmp, targetCmp);
|
|
144
|
+
if (rel === '' || rel.startsWith('..') || isAbsolute(rel)) {
|
|
145
|
+
throw Object.assign(new Error('Directory escapes plugins root'), { statusCode: 400 });
|
|
146
|
+
}
|
|
147
|
+
const manifestPath = join(realTarget, 'plugin.json');
|
|
148
|
+
if (!existsSync(manifestPath)) {
|
|
149
|
+
throw Object.assign(new Error('Not a directory plugin (missing plugin.json)'), { statusCode: 400 });
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
|
153
|
+
if (manifest.name && manifest.name !== dirName) {
|
|
154
|
+
throw Object.assign(new Error(`Manifest name "${manifest.name}" does not match directory "${dirName}"`), { statusCode: 400 });
|
|
155
|
+
}
|
|
156
|
+
} catch (err) {
|
|
157
|
+
if (err.statusCode) throw err;
|
|
158
|
+
throw Object.assign(new Error('plugin.json is not valid JSON'), { statusCode: 400 });
|
|
159
|
+
}
|
|
160
|
+
rmSync(realTarget, { recursive: true, force: true });
|
|
161
|
+
return { deleted: dirName };
|
|
162
|
+
}
|