@leeoohoo/ui-apps-devkit 0.1.5 → 0.1.7
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/package.json +1 -1
- package/src/commands/install.js +66 -18
- package/src/commands/validate.js +48 -0
- package/src/sandbox/server.js +88 -41
- package/templates/basic/README.md +2 -0
- package/templates/basic/docs/CHATOS_UI_APPS_AI_CONTRIBUTIONS.md +2 -0
- package/templates/notepad/README.md +6 -0
- package/templates/notepad/docs/CHATOS_UI_APPS_AI_CONTRIBUTIONS.md +2 -0
package/package.json
CHANGED
package/src/commands/install.js
CHANGED
|
@@ -1,11 +1,29 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
1
2
|
import os from 'os';
|
|
2
3
|
import path from 'path';
|
|
4
|
+
import crypto from 'crypto';
|
|
3
5
|
|
|
4
6
|
import { copyDir, ensureDir, isDirectory, rmForce, sanitizeDirComponent } from '../lib/fs.js';
|
|
5
7
|
import { loadDevkitConfig } from '../lib/config.js';
|
|
6
8
|
import { findPluginDir, loadPluginManifest } from '../lib/plugin.js';
|
|
7
9
|
import { STATE_ROOT_DIRNAME } from '../lib/state-constants.js';
|
|
8
10
|
|
|
11
|
+
function createActionId(prefix) {
|
|
12
|
+
const base = typeof prefix === 'string' && prefix.trim() ? prefix.trim() : 'action';
|
|
13
|
+
const short = crypto.randomUUID().split('-')[0];
|
|
14
|
+
return `${base}-${Date.now().toString(36)}-${short}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function logWith(filePath, entry) {
|
|
18
|
+
if (!filePath) return;
|
|
19
|
+
try {
|
|
20
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
21
|
+
fs.appendFileSync(filePath, `${JSON.stringify(entry)}\n`, 'utf8');
|
|
22
|
+
} catch {
|
|
23
|
+
// ignore logging failures
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
9
27
|
function defaultStateDir(hostApp) {
|
|
10
28
|
const app = typeof hostApp === 'string' && hostApp.trim() ? hostApp.trim() : 'chatos';
|
|
11
29
|
return path.join(os.homedir(), STATE_ROOT_DIRNAME, app);
|
|
@@ -28,29 +46,59 @@ function copyPluginDir(srcDir, destDir) {
|
|
|
28
46
|
|
|
29
47
|
export async function cmdInstall({ flags }) {
|
|
30
48
|
const { config } = loadDevkitConfig(process.cwd());
|
|
31
|
-
const
|
|
32
|
-
const { pluginId, name, version } = loadPluginManifest(pluginDir);
|
|
33
|
-
|
|
49
|
+
const actionId = createActionId('devkit_install');
|
|
34
50
|
const hostApp = String(flags['host-app'] || flags.hostApp || 'chatos').trim() || 'chatos';
|
|
35
51
|
const stateDir = String(flags['state-dir'] || flags.stateDir || defaultStateDir(hostApp)).trim();
|
|
36
|
-
|
|
52
|
+
const logFile = String(flags['log-file'] || flags.logFile || path.join(stateDir, 'devkit-install-log.jsonl')).trim();
|
|
53
|
+
const log = (level, message, meta, err) => {
|
|
54
|
+
const entry = {
|
|
55
|
+
ts: new Date().toISOString(),
|
|
56
|
+
level,
|
|
57
|
+
message,
|
|
58
|
+
actionId,
|
|
59
|
+
pid: process.pid,
|
|
60
|
+
meta: meta && typeof meta === 'object' ? meta : meta === undefined ? undefined : { value: meta },
|
|
61
|
+
error: err?.message || (err ? String(err) : undefined),
|
|
62
|
+
};
|
|
63
|
+
logWith(logFile, entry);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
if (!stateDir) throw new Error('stateDir is required');
|
|
68
|
+
log('info', 'devkit.install.start', { hostApp, stateDir, logFile });
|
|
69
|
+
const pluginDir = findPluginDir(process.cwd(), flags['plugin-dir'] || flags.pluginDir || config?.pluginDir);
|
|
70
|
+
const { pluginId, name, version } = loadPluginManifest(pluginDir);
|
|
71
|
+
|
|
72
|
+
const pluginsRoot = path.join(stateDir, 'ui_apps', 'plugins');
|
|
73
|
+
ensureDir(pluginsRoot);
|
|
37
74
|
|
|
38
|
-
|
|
39
|
-
|
|
75
|
+
const dirName = sanitizeDirComponent(pluginId);
|
|
76
|
+
if (!dirName) throw new Error(`Invalid plugin id: ${pluginId}`);
|
|
40
77
|
|
|
41
|
-
|
|
42
|
-
|
|
78
|
+
const destDir = path.join(pluginsRoot, dirName);
|
|
79
|
+
const replaced = isDirectory(destDir);
|
|
80
|
+
copyPluginDir(pluginDir, destDir);
|
|
43
81
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
82
|
+
log('info', 'devkit.install.complete', {
|
|
83
|
+
pluginId,
|
|
84
|
+
name,
|
|
85
|
+
version,
|
|
86
|
+
pluginDir,
|
|
87
|
+
destDir,
|
|
88
|
+
replaced,
|
|
89
|
+
});
|
|
47
90
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
91
|
+
// eslint-disable-next-line no-console
|
|
92
|
+
console.log(
|
|
93
|
+
`Installed: ${pluginId} (${name}@${version})\n` +
|
|
94
|
+
` -> ${destDir}\n` +
|
|
95
|
+
` replaced: ${replaced}\n` +
|
|
96
|
+
` log: ${logFile}\n\n` +
|
|
97
|
+
`Open ChatOS -> 应用 -> 刷新(同 id 覆盖生效)。`
|
|
98
|
+
);
|
|
99
|
+
} catch (err) {
|
|
100
|
+
log('error', 'devkit.install.failed', {}, err);
|
|
101
|
+
throw err;
|
|
102
|
+
}
|
|
55
103
|
}
|
|
56
104
|
|
package/src/commands/validate.js
CHANGED
|
@@ -18,11 +18,52 @@ function statSizeSafe(filePath) {
|
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
function collectSymlinks(rootDir) {
|
|
22
|
+
const root = typeof rootDir === 'string' ? rootDir.trim() : '';
|
|
23
|
+
if (!root) return [];
|
|
24
|
+
const out = [];
|
|
25
|
+
const walk = (dir) => {
|
|
26
|
+
let entries = [];
|
|
27
|
+
try {
|
|
28
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
29
|
+
} catch {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
entries.forEach((entry) => {
|
|
33
|
+
if (!entry || !entry.name) return;
|
|
34
|
+
const fullPath = path.join(dir, entry.name);
|
|
35
|
+
let stat;
|
|
36
|
+
try {
|
|
37
|
+
stat = fs.lstatSync(fullPath);
|
|
38
|
+
} catch {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (stat.isSymbolicLink()) {
|
|
42
|
+
out.push(fullPath);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (stat.isDirectory()) {
|
|
46
|
+
walk(fullPath);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
walk(root);
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
|
|
21
54
|
export async function cmdValidate({ flags }) {
|
|
22
55
|
const { config } = loadDevkitConfig(process.cwd());
|
|
23
56
|
const pluginDir = findPluginDir(process.cwd(), flags['plugin-dir'] || flags.pluginDir || config?.pluginDir);
|
|
24
57
|
|
|
25
58
|
const { manifestPath, manifest } = loadPluginManifest(pluginDir);
|
|
59
|
+
const warnings = [];
|
|
60
|
+
const warn = (message) => warnings.push(message);
|
|
61
|
+
|
|
62
|
+
const symlinks = collectSymlinks(pluginDir);
|
|
63
|
+
if (symlinks.length > 0) {
|
|
64
|
+
const rel = symlinks.map((entry) => path.relative(pluginDir, entry) || entry);
|
|
65
|
+
warn(`Symlink detected inside plugin dir (may bypass path boundaries): ${rel.join(', ')}`);
|
|
66
|
+
}
|
|
26
67
|
|
|
27
68
|
const manifestSize = statSizeSafe(manifestPath);
|
|
28
69
|
assert(manifestSize <= 256 * 1024, `plugin.json too large (>256KiB): ${manifestSize} bytes`);
|
|
@@ -108,6 +149,13 @@ export async function cmdValidate({ flags }) {
|
|
|
108
149
|
}
|
|
109
150
|
}
|
|
110
151
|
|
|
152
|
+
if (warnings.length > 0) {
|
|
153
|
+
warnings.forEach((message) => {
|
|
154
|
+
// eslint-disable-next-line no-console
|
|
155
|
+
console.warn(`WARN: ${message}`);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
111
159
|
// eslint-disable-next-line no-console
|
|
112
160
|
console.log(`OK: ${path.relative(process.cwd(), pluginDir)} (apps=${apps.length})`);
|
|
113
161
|
}
|
package/src/sandbox/server.js
CHANGED
|
@@ -628,6 +628,16 @@ function htmlPage() {
|
|
|
628
628
|
}
|
|
629
629
|
#container { flex: 1 1 auto; min-height:0; overflow:hidden; }
|
|
630
630
|
#containerInner { height:100%; overflow:auto; }
|
|
631
|
+
body[data-surface='compact'] #headerSlot {
|
|
632
|
+
width: 50vw;
|
|
633
|
+
align-self: flex-end;
|
|
634
|
+
border-left: 1px solid var(--ds-panel-border);
|
|
635
|
+
}
|
|
636
|
+
body[data-surface='compact'] #container {
|
|
637
|
+
width: 50vw;
|
|
638
|
+
align-self: flex-end;
|
|
639
|
+
border-left: 1px solid var(--ds-panel-border);
|
|
640
|
+
}
|
|
631
641
|
.muted { opacity: 0.7; font-size: 12px; }
|
|
632
642
|
.bar { display:flex; gap:10px; align-items:center; justify-content:space-between; }
|
|
633
643
|
.btn {
|
|
@@ -658,10 +668,12 @@ function htmlPage() {
|
|
|
658
668
|
border-radius:14px;
|
|
659
669
|
overflow:hidden;
|
|
660
670
|
box-shadow: 0 18px 60px rgba(0,0,0,0.18);
|
|
671
|
+
z-index: 1305;
|
|
672
|
+
pointer-events: auto;
|
|
661
673
|
}
|
|
662
674
|
#promptsPanelHeader { padding: 10px 12px; display:flex; align-items:center; justify-content:space-between; border-bottom: 1px solid var(--ds-panel-border); }
|
|
663
675
|
#promptsPanelBody { padding: 10px 12px; overflow:auto; display:flex; flex-direction:column; gap:10px; }
|
|
664
|
-
#promptsFab { position: fixed; right: 16px; bottom: 16px; width: 44px; height: 44px; border-radius: 999px; display:flex; align-items:center; justify-content:center; }
|
|
676
|
+
#promptsFab { position: fixed; right: 16px; bottom: 16px; width: 44px; height: 44px; border-radius: 999px; display:flex; align-items:center; justify-content:center; z-index: 1305; pointer-events: auto; }
|
|
665
677
|
.card { border: 1px solid var(--ds-panel-border); border-radius: 12px; padding: 10px; background: var(--ds-panel-bg); }
|
|
666
678
|
.row { display:flex; gap:10px; }
|
|
667
679
|
.toolbar-group { display:flex; gap:8px; align-items:center; flex-wrap:wrap; }
|
|
@@ -739,16 +751,6 @@ function htmlPage() {
|
|
|
739
751
|
box-shadow: 0 14px 40px rgba(0,0,0,0.16);
|
|
740
752
|
z-index: 12;
|
|
741
753
|
}
|
|
742
|
-
#mcpPanel[data-mode='half'] {
|
|
743
|
-
right: 0;
|
|
744
|
-
top: 72px;
|
|
745
|
-
width: 50vw;
|
|
746
|
-
height: calc(100vh - 84px);
|
|
747
|
-
max-height: none;
|
|
748
|
-
border-right: none;
|
|
749
|
-
border-top-right-radius: 0;
|
|
750
|
-
border-bottom-right-radius: 0;
|
|
751
|
-
}
|
|
752
754
|
#mcpPanelHeader {
|
|
753
755
|
padding: 10px 12px;
|
|
754
756
|
display:flex;
|
|
@@ -805,7 +807,7 @@ function htmlPage() {
|
|
|
805
807
|
<div id="sandboxContext" class="muted"></div>
|
|
806
808
|
<button id="btnLlmConfig" class="btn" type="button">AI Config</button>
|
|
807
809
|
<button id="btnMcpTest" class="btn" type="button">MCP Test</button>
|
|
808
|
-
<button id="
|
|
810
|
+
<button id="btnHalfApp" class="btn" type="button">半屏</button>
|
|
809
811
|
<button id="btnInspectorToggle" class="btn" type="button">Inspect</button>
|
|
810
812
|
<button id="btnReload" class="btn" type="button">Reload</button>
|
|
811
813
|
</div>
|
|
@@ -867,8 +869,8 @@ function htmlPage() {
|
|
|
867
869
|
<div class="card">
|
|
868
870
|
<div class="section-title">Paths</div>
|
|
869
871
|
<pre id="mcpPaths" class="mono"></pre>
|
|
870
|
-
<label for="mcpWorkdir">
|
|
871
|
-
<input id="mcpWorkdir" type="text" placeholder="留空则使用默认
|
|
872
|
+
<label for="mcpWorkdir">ProjectRoot Override (optional)</label>
|
|
873
|
+
<input id="mcpWorkdir" type="text" placeholder="留空则使用默认 projectRoot" />
|
|
872
874
|
</div>
|
|
873
875
|
<div class="card">
|
|
874
876
|
<div class="section-title">Model</div>
|
|
@@ -961,7 +963,7 @@ const llmModelId = $('#llmModelId');
|
|
|
961
963
|
const llmStatus = $('#llmStatus');
|
|
962
964
|
const llmKeyStatus = $('#llmKeyStatus');
|
|
963
965
|
const btnMcpTest = $('#btnMcpTest');
|
|
964
|
-
const
|
|
966
|
+
const btnHalfApp = $('#btnHalfApp');
|
|
965
967
|
const mcpPanel = $('#mcpPanel');
|
|
966
968
|
const btnMcpClose = $('#btnMcpClose');
|
|
967
969
|
const btnMcpClear = $('#btnMcpClear');
|
|
@@ -1033,6 +1035,34 @@ const updateContextStatus = () => {
|
|
|
1033
1035
|
sandboxContext.textContent = __SANDBOX__.pluginId + ':' + __SANDBOX__.appId;
|
|
1034
1036
|
};
|
|
1035
1037
|
|
|
1038
|
+
const entryCompactUrl = __SANDBOX__.entryCompactUrl || '';
|
|
1039
|
+
const hasCompactEntry = Boolean(entryCompactUrl);
|
|
1040
|
+
let currentSurface = 'full';
|
|
1041
|
+
|
|
1042
|
+
const updateSurfaceState = (surface) => {
|
|
1043
|
+
currentSurface = surface === 'compact' ? 'compact' : 'full';
|
|
1044
|
+
document.body.dataset.surface = currentSurface;
|
|
1045
|
+
if (btnHalfApp) {
|
|
1046
|
+
if (!hasCompactEntry) {
|
|
1047
|
+
btnHalfApp.disabled = true;
|
|
1048
|
+
btnHalfApp.title = 'plugin.json 未配置 entry.compact.path';
|
|
1049
|
+
delete btnHalfApp.dataset.active;
|
|
1050
|
+
} else {
|
|
1051
|
+
btnHalfApp.disabled = false;
|
|
1052
|
+
btnHalfApp.title = '';
|
|
1053
|
+
btnHalfApp.dataset.active = currentSurface === 'compact' ? '1' : '0';
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
if (host?.ui) host.ui.surface = currentSurface;
|
|
1057
|
+
};
|
|
1058
|
+
|
|
1059
|
+
const toggleCompactSurface = () => {
|
|
1060
|
+
if (!hasCompactEntry) return;
|
|
1061
|
+
updateSurfaceState(currentSurface === 'compact' ? 'full' : 'compact');
|
|
1062
|
+
loadAndMount().catch(renderError);
|
|
1063
|
+
updateInspectorIfOpen();
|
|
1064
|
+
};
|
|
1065
|
+
|
|
1036
1066
|
const isInspectorOpen = () => sandboxInspector && sandboxInspector.style.display === 'flex';
|
|
1037
1067
|
const isLlmPanelOpen = () => llmPanel && llmPanel.style.display === 'flex';
|
|
1038
1068
|
const isMcpPanelOpen = () => mcpPanel && mcpPanel.style.display === 'flex';
|
|
@@ -1118,9 +1148,15 @@ const appendMcpOutput = (label, payload) => {
|
|
|
1118
1148
|
mcpOutput.scrollTop = mcpOutput.scrollHeight;
|
|
1119
1149
|
};
|
|
1120
1150
|
|
|
1151
|
+
const resolveMcpPaths = () => {
|
|
1152
|
+
const projectRootOverride = mcpWorkdir ? String(mcpWorkdir.value || '').trim() : '';
|
|
1153
|
+
if (!projectRootOverride) return sandboxPaths;
|
|
1154
|
+
return { ...sandboxPaths, projectRoot: projectRootOverride };
|
|
1155
|
+
};
|
|
1156
|
+
|
|
1121
1157
|
const refreshMcpPaths = () => {
|
|
1122
1158
|
if (!mcpPaths) return;
|
|
1123
|
-
mcpPaths.textContent = formatJson(
|
|
1159
|
+
mcpPaths.textContent = formatJson(resolveMcpPaths());
|
|
1124
1160
|
};
|
|
1125
1161
|
|
|
1126
1162
|
const refreshMcpConfigHint = async () => {
|
|
@@ -1140,13 +1176,8 @@ const refreshMcpConfigHint = async () => {
|
|
|
1140
1176
|
}
|
|
1141
1177
|
};
|
|
1142
1178
|
|
|
1143
|
-
const setMcpPanelOpen = (open
|
|
1179
|
+
const setMcpPanelOpen = (open) => {
|
|
1144
1180
|
if (!mcpPanel) return;
|
|
1145
|
-
if (mode) {
|
|
1146
|
-
mcpPanel.dataset.mode = mode;
|
|
1147
|
-
} else {
|
|
1148
|
-
delete mcpPanel.dataset.mode;
|
|
1149
|
-
}
|
|
1150
1181
|
mcpPanel.style.display = open ? 'flex' : 'none';
|
|
1151
1182
|
mcpPanel.setAttribute('aria-hidden', open ? 'false' : 'true');
|
|
1152
1183
|
if (open) {
|
|
@@ -1155,16 +1186,6 @@ const setMcpPanelOpen = (open, mode) => {
|
|
|
1155
1186
|
}
|
|
1156
1187
|
};
|
|
1157
1188
|
|
|
1158
|
-
const toggleMcpPanel = (mode) => {
|
|
1159
|
-
if (!mcpPanel) return;
|
|
1160
|
-
const currentMode = mcpPanel.dataset.mode || 'panel';
|
|
1161
|
-
if (isMcpPanelOpen() && currentMode === mode) {
|
|
1162
|
-
setMcpPanelOpen(false);
|
|
1163
|
-
return;
|
|
1164
|
-
}
|
|
1165
|
-
setMcpPanelOpen(true, mode);
|
|
1166
|
-
};
|
|
1167
|
-
|
|
1168
1189
|
const runMcpTest = async () => {
|
|
1169
1190
|
const sendBtn = btnMcpSend;
|
|
1170
1191
|
try {
|
|
@@ -1180,9 +1201,11 @@ const runMcpTest = async () => {
|
|
|
1180
1201
|
systemPrompt: mcpSystem ? String(mcpSystem.value || '').trim() : '',
|
|
1181
1202
|
disableTools: Boolean(mcpDisableTools?.checked),
|
|
1182
1203
|
};
|
|
1183
|
-
const
|
|
1184
|
-
if (
|
|
1185
|
-
payload.callMeta = {
|
|
1204
|
+
const projectRootOverride = mcpWorkdir ? String(mcpWorkdir.value || '').trim() : '';
|
|
1205
|
+
if (projectRootOverride) {
|
|
1206
|
+
payload.callMeta = {
|
|
1207
|
+
chatos: { uiApp: { projectRoot: projectRootOverride } },
|
|
1208
|
+
};
|
|
1186
1209
|
}
|
|
1187
1210
|
setMcpStatus('Sending...');
|
|
1188
1211
|
appendMcpOutput('request', payload);
|
|
@@ -1226,7 +1249,13 @@ const collectTokens = () => {
|
|
|
1226
1249
|
const readHostContext = () => {
|
|
1227
1250
|
if (!inspectorEnabled) return null;
|
|
1228
1251
|
if (typeof host?.context?.get === 'function') return host.context.get();
|
|
1229
|
-
return {
|
|
1252
|
+
return {
|
|
1253
|
+
pluginId: __SANDBOX__.pluginId,
|
|
1254
|
+
appId: __SANDBOX__.appId,
|
|
1255
|
+
theme: currentTheme,
|
|
1256
|
+
surface: currentSurface,
|
|
1257
|
+
bridge: { enabled: true },
|
|
1258
|
+
};
|
|
1230
1259
|
};
|
|
1231
1260
|
|
|
1232
1261
|
const readThemeInfo = () => ({
|
|
@@ -1305,9 +1334,10 @@ if (btnLlmClose) btnLlmClose.addEventListener('click', () => setLlmPanelOpen(fal
|
|
|
1305
1334
|
if (btnLlmRefresh) btnLlmRefresh.addEventListener('click', () => refreshLlmConfig());
|
|
1306
1335
|
if (btnLlmSave) btnLlmSave.addEventListener('click', () => saveLlmConfig());
|
|
1307
1336
|
if (btnLlmClear) btnLlmClear.addEventListener('click', () => saveLlmConfig({ clearKey: true }));
|
|
1308
|
-
if (btnMcpTest) btnMcpTest.addEventListener('click', () =>
|
|
1309
|
-
if (
|
|
1337
|
+
if (btnMcpTest) btnMcpTest.addEventListener('click', () => setMcpPanelOpen(!isMcpPanelOpen()));
|
|
1338
|
+
if (btnHalfApp) btnHalfApp.addEventListener('click', () => toggleCompactSurface());
|
|
1310
1339
|
if (btnMcpClose) btnMcpClose.addEventListener('click', () => setMcpPanelOpen(false));
|
|
1340
|
+
if (mcpWorkdir) mcpWorkdir.addEventListener('input', () => refreshMcpPaths());
|
|
1311
1341
|
if (btnMcpClear)
|
|
1312
1342
|
btnMcpClear.addEventListener('click', () => {
|
|
1313
1343
|
if (mcpOutput) mcpOutput.textContent = '';
|
|
@@ -1499,7 +1529,15 @@ const getTheme = () => currentTheme || resolveTheme();
|
|
|
1499
1529
|
|
|
1500
1530
|
const host = {
|
|
1501
1531
|
bridge: { enabled: true },
|
|
1502
|
-
context: {
|
|
1532
|
+
context: {
|
|
1533
|
+
get: () => ({
|
|
1534
|
+
pluginId: __SANDBOX__.pluginId,
|
|
1535
|
+
appId: __SANDBOX__.appId,
|
|
1536
|
+
theme: getTheme(),
|
|
1537
|
+
surface: currentSurface,
|
|
1538
|
+
bridge: { enabled: true },
|
|
1539
|
+
}),
|
|
1540
|
+
},
|
|
1503
1541
|
theme: {
|
|
1504
1542
|
get: getTheme,
|
|
1505
1543
|
onChange: (listener) => {
|
|
@@ -1552,7 +1590,7 @@ const host = {
|
|
|
1552
1590
|
close: () => (setPanelOpen(false), { ok: true }),
|
|
1553
1591
|
toggle: () => (setPanelOpen(panel.style.display !== 'flex'), { ok: true }),
|
|
1554
1592
|
},
|
|
1555
|
-
ui: { navigate: (menu) => ({ ok: true, menu }) },
|
|
1593
|
+
ui: { navigate: (menu) => ({ ok: true, menu }), surface: 'full' },
|
|
1556
1594
|
chat: (() => {
|
|
1557
1595
|
const clone = (v) => JSON.parse(JSON.stringify(v));
|
|
1558
1596
|
|
|
@@ -1807,6 +1845,8 @@ const host = {
|
|
|
1807
1845
|
})(),
|
|
1808
1846
|
};
|
|
1809
1847
|
|
|
1848
|
+
updateSurfaceState('full');
|
|
1849
|
+
|
|
1810
1850
|
inspectorEnabled = true;
|
|
1811
1851
|
updateInspector();
|
|
1812
1852
|
|
|
@@ -1816,7 +1856,7 @@ async function loadAndMount() {
|
|
|
1816
1856
|
if (typeof dispose === 'function') { try { await dispose(); } catch {} dispose = null; }
|
|
1817
1857
|
container.textContent = '';
|
|
1818
1858
|
|
|
1819
|
-
const entryUrl = __SANDBOX__.entryUrl;
|
|
1859
|
+
const entryUrl = currentSurface === 'compact' && entryCompactUrl ? entryCompactUrl : __SANDBOX__.entryUrl;
|
|
1820
1860
|
const mod = await import(entryUrl + (entryUrl.includes('?') ? '&' : '?') + 't=' + Date.now());
|
|
1821
1861
|
const mount = mod?.mount || mod?.default?.mount || (typeof mod?.default === 'function' ? mod.default : null);
|
|
1822
1862
|
if (typeof mount !== 'function') throw new Error('module entry must export mount()');
|
|
@@ -1879,7 +1919,13 @@ export async function startSandboxServer({ pluginDir, port = 4399, appId = '' })
|
|
|
1879
1919
|
const entryAbs = resolveInsideDir(pluginDir, entryRel);
|
|
1880
1920
|
if (!isFile(entryAbs)) throw new Error(`module entry not found: ${entryRel}`);
|
|
1881
1921
|
|
|
1922
|
+
const entryCompactRel = String(app?.entry?.compact?.path || '').trim();
|
|
1882
1923
|
const entryUrl = `/plugin/${encodeURIComponent(entryRel).replaceAll('%2F', '/')}`;
|
|
1924
|
+
const entryCompactAbs = entryCompactRel ? resolveInsideDir(pluginDir, entryCompactRel) : '';
|
|
1925
|
+
const entryCompactUrl =
|
|
1926
|
+
entryCompactAbs && isFile(entryCompactAbs)
|
|
1927
|
+
? `/plugin/${encodeURIComponent(entryCompactRel).replaceAll('%2F', '/')}`
|
|
1928
|
+
: '';
|
|
1883
1929
|
|
|
1884
1930
|
let backendInstance = null;
|
|
1885
1931
|
let backendFactory = null;
|
|
@@ -2193,6 +2239,7 @@ export async function startSandboxServer({ pluginDir, port = 4399, appId = '' })
|
|
|
2193
2239
|
.replaceAll('__SANDBOX__.pluginId', JSON.stringify(ctxBase.pluginId))
|
|
2194
2240
|
.replaceAll('__SANDBOX__.appId', JSON.stringify(effectiveAppId))
|
|
2195
2241
|
.replaceAll('__SANDBOX__.entryUrl', JSON.stringify(entryUrl))
|
|
2242
|
+
.replaceAll('__SANDBOX__.entryCompactUrl', JSON.stringify(entryCompactUrl))
|
|
2196
2243
|
.replaceAll('__SANDBOX__.registryApp', JSON.stringify({ plugin: { id: ctxBase.pluginId }, id: effectiveAppId, entry: { type: 'module', url: entryUrl } }))
|
|
2197
2244
|
.replaceAll('__SANDBOX__.tokenNames', JSON.stringify(tokenNames))
|
|
2198
2245
|
.replaceAll('__SANDBOX__.paths', JSON.stringify(sandboxPaths));
|
|
@@ -34,6 +34,7 @@ npm run dev
|
|
|
34
34
|
|
|
35
35
|
- `plugin/plugin.json`:`apps[i].entry.type` 必须是 `module`,且 `path` 在插件目录内。
|
|
36
36
|
- `plugin/plugin.json`:可选 `apps[i].entry.compact.path`,用于 compact UI。
|
|
37
|
+
- 安全:插件目录内尽量避免 symlink(`npm run validate` 会提示),以免路径边界与打包行为不一致。
|
|
37
38
|
- `mount()`:返回卸载函数并清理事件/订阅;滚动放在应用内部,固定内容用 `slots.header`。
|
|
38
39
|
- 主题:用 `host.theme.*` 与 `--ds-*` tokens;避免硬编码颜色。
|
|
39
40
|
- 宿主能力:先判断 `host.bridge.enabled`,非宿主环境要可降级运行。
|
|
@@ -49,6 +50,7 @@ npm run dev
|
|
|
49
50
|
2) **后端调用 LLM**:在 `plugin/backend/index.mjs` 里通过 `ctx.llm.complete()` 调用模型;前端用 `host.backend.invoke('llmComplete', { input })` 触发。
|
|
50
51
|
|
|
51
52
|
说明:本地沙箱默认是 mock;可通过右上角 `AI Config` 配置 `API Key / Base URL / Model ID` 后启用真实模型调用,并用于测试应用 MCP(需配置 `ai.mcp`)。
|
|
53
|
+
补充:沙箱默认把 `_meta.workdir` 设为 `dataDir`;如需模拟其它工作目录,可在 `AI Config` 里设置 `Workdir` 覆盖(支持 `$dataDir/$pluginDir/$projectRoot`)。
|
|
52
54
|
|
|
53
55
|
## 安装到本机 ChatOS
|
|
54
56
|
|
|
@@ -9,6 +9,11 @@ npm install
|
|
|
9
9
|
npm run dev
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
+
## 本地沙箱 AI Config(可选)
|
|
13
|
+
|
|
14
|
+
- 右上角 `AI Config` 可配置 `API Key / Base URL / Model ID`,用于测试真实模型调用(以及 MCP 调用)。
|
|
15
|
+
- 沙箱默认把 `_meta.workdir` 设为 `dataDir`;如需模拟其它工作目录,可在 `AI Config` 里设置 `Workdir` 覆盖(支持 `$dataDir/$pluginDir/$projectRoot`)。
|
|
16
|
+
|
|
12
17
|
## 目录说明
|
|
13
18
|
|
|
14
19
|
- `plugin/plugin.json`:插件清单(应用列表、入口、后端、AI 贡献)
|
|
@@ -29,6 +34,7 @@ npm run dev
|
|
|
29
34
|
|
|
30
35
|
- `plugin/plugin.json`:`apps[i].entry.type` 必须是 `module`,且 `path` 在插件目录内。
|
|
31
36
|
- `plugin/plugin.json`:可选 `apps[i].entry.compact.path`,用于 compact UI。
|
|
37
|
+
- 安全:插件目录内尽量避免 symlink(`npm run validate` 会提示),以免路径边界与打包行为不一致。
|
|
32
38
|
- `mount()`:返回卸载函数并清理事件/订阅;滚动放在应用内部,固定内容用 `slots.header`。
|
|
33
39
|
- 主题:用 `host.theme.*` 与 `--ds-*` tokens;避免硬编码颜色。
|
|
34
40
|
- 宿主能力:先判断 `host.bridge.enabled`,非宿主环境要可降级运行。
|