@leeoohoo/ui-apps-devkit 0.1.6 → 0.1.8
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 +59 -17
- package/templates/basic/README.md +2 -0
- package/templates/basic/docs/CHATOS_UI_APPS_AI_CONTRIBUTIONS.md +2 -0
- package/templates/basic/docs/CHATOS_UI_PROMPTS_PROTOCOL.md +33 -3
- package/templates/notepad/README.md +6 -0
- package/templates/notepad/docs/CHATOS_UI_APPS_AI_CONTRIBUTIONS.md +2 -0
- package/templates/notepad/docs/CHATOS_UI_PROMPTS_PROTOCOL.md +33 -3
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
|
@@ -668,10 +668,12 @@ function htmlPage() {
|
|
|
668
668
|
border-radius:14px;
|
|
669
669
|
overflow:hidden;
|
|
670
670
|
box-shadow: 0 18px 60px rgba(0,0,0,0.18);
|
|
671
|
+
z-index: 1305;
|
|
672
|
+
pointer-events: auto;
|
|
671
673
|
}
|
|
672
674
|
#promptsPanelHeader { padding: 10px 12px; display:flex; align-items:center; justify-content:space-between; border-bottom: 1px solid var(--ds-panel-border); }
|
|
673
675
|
#promptsPanelBody { padding: 10px 12px; overflow:auto; display:flex; flex-direction:column; gap:10px; }
|
|
674
|
-
#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; }
|
|
675
677
|
.card { border: 1px solid var(--ds-panel-border); border-radius: 12px; padding: 10px; background: var(--ds-panel-bg); }
|
|
676
678
|
.row { display:flex; gap:10px; }
|
|
677
679
|
.toolbar-group { display:flex; gap:8px; align-items:center; flex-wrap:wrap; }
|
|
@@ -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>
|
|
@@ -1146,9 +1148,15 @@ const appendMcpOutput = (label, payload) => {
|
|
|
1146
1148
|
mcpOutput.scrollTop = mcpOutput.scrollHeight;
|
|
1147
1149
|
};
|
|
1148
1150
|
|
|
1151
|
+
const resolveMcpPaths = () => {
|
|
1152
|
+
const projectRootOverride = mcpWorkdir ? String(mcpWorkdir.value || '').trim() : '';
|
|
1153
|
+
if (!projectRootOverride) return sandboxPaths;
|
|
1154
|
+
return { ...sandboxPaths, projectRoot: projectRootOverride };
|
|
1155
|
+
};
|
|
1156
|
+
|
|
1149
1157
|
const refreshMcpPaths = () => {
|
|
1150
1158
|
if (!mcpPaths) return;
|
|
1151
|
-
mcpPaths.textContent = formatJson(
|
|
1159
|
+
mcpPaths.textContent = formatJson(resolveMcpPaths());
|
|
1152
1160
|
};
|
|
1153
1161
|
|
|
1154
1162
|
const refreshMcpConfigHint = async () => {
|
|
@@ -1193,9 +1201,11 @@ const runMcpTest = async () => {
|
|
|
1193
1201
|
systemPrompt: mcpSystem ? String(mcpSystem.value || '').trim() : '',
|
|
1194
1202
|
disableTools: Boolean(mcpDisableTools?.checked),
|
|
1195
1203
|
};
|
|
1196
|
-
const
|
|
1197
|
-
if (
|
|
1198
|
-
payload.callMeta = {
|
|
1204
|
+
const projectRootOverride = mcpWorkdir ? String(mcpWorkdir.value || '').trim() : '';
|
|
1205
|
+
if (projectRootOverride) {
|
|
1206
|
+
payload.callMeta = {
|
|
1207
|
+
chatos: { uiApp: { projectRoot: projectRootOverride } },
|
|
1208
|
+
};
|
|
1199
1209
|
}
|
|
1200
1210
|
setMcpStatus('Sending...');
|
|
1201
1211
|
appendMcpOutput('request', payload);
|
|
@@ -1327,6 +1337,7 @@ if (btnLlmClear) btnLlmClear.addEventListener('click', () => saveLlmConfig({ cle
|
|
|
1327
1337
|
if (btnMcpTest) btnMcpTest.addEventListener('click', () => setMcpPanelOpen(!isMcpPanelOpen()));
|
|
1328
1338
|
if (btnHalfApp) btnHalfApp.addEventListener('click', () => toggleCompactSurface());
|
|
1329
1339
|
if (btnMcpClose) btnMcpClose.addEventListener('click', () => setMcpPanelOpen(false));
|
|
1340
|
+
if (mcpWorkdir) mcpWorkdir.addEventListener('input', () => refreshMcpPaths());
|
|
1330
1341
|
if (btnMcpClear)
|
|
1331
1342
|
btnMcpClear.addEventListener('click', () => {
|
|
1332
1343
|
if (mcpOutput) mcpOutput.textContent = '';
|
|
@@ -1396,6 +1407,7 @@ function renderPrompts() {
|
|
|
1396
1407
|
form.style.gap = '10px';
|
|
1397
1408
|
|
|
1398
1409
|
const kind = String(req?.prompt?.kind || '');
|
|
1410
|
+
const allowCancel = req?.prompt?.allowCancel !== false;
|
|
1399
1411
|
|
|
1400
1412
|
const mkBtn = (label, danger) => {
|
|
1401
1413
|
const btn = document.createElement('button');
|
|
@@ -1410,7 +1422,31 @@ function renderPrompts() {
|
|
|
1410
1422
|
emitUpdate();
|
|
1411
1423
|
};
|
|
1412
1424
|
|
|
1413
|
-
if (kind === '
|
|
1425
|
+
if (kind === 'result') {
|
|
1426
|
+
const markdownText =
|
|
1427
|
+
typeof req?.prompt?.markdown === 'string'
|
|
1428
|
+
? req.prompt.markdown
|
|
1429
|
+
: typeof req?.prompt?.result === 'string'
|
|
1430
|
+
? req.prompt.result
|
|
1431
|
+
: typeof req?.prompt?.content === 'string'
|
|
1432
|
+
? req.prompt.content
|
|
1433
|
+
: '';
|
|
1434
|
+
const markdown = document.createElement('pre');
|
|
1435
|
+
markdown.className = 'mono';
|
|
1436
|
+
markdown.textContent = markdownText || '(无结果内容)';
|
|
1437
|
+
form.appendChild(markdown);
|
|
1438
|
+
const row = document.createElement('div');
|
|
1439
|
+
row.className = 'row';
|
|
1440
|
+
const ok = mkBtn('OK');
|
|
1441
|
+
ok.addEventListener('click', () => submit({ status: 'ok' }));
|
|
1442
|
+
row.appendChild(ok);
|
|
1443
|
+
if (allowCancel) {
|
|
1444
|
+
const cancel = mkBtn('Cancel', true);
|
|
1445
|
+
cancel.addEventListener('click', () => submit({ status: 'cancel' }));
|
|
1446
|
+
row.appendChild(cancel);
|
|
1447
|
+
}
|
|
1448
|
+
form.appendChild(row);
|
|
1449
|
+
} else if (kind === 'kv') {
|
|
1414
1450
|
const fields = Array.isArray(req?.prompt?.fields) ? req.prompt.fields : [];
|
|
1415
1451
|
const values = {};
|
|
1416
1452
|
for (const f of fields) {
|
|
@@ -1432,10 +1468,12 @@ function renderPrompts() {
|
|
|
1432
1468
|
row.className = 'row';
|
|
1433
1469
|
const ok = mkBtn('Submit');
|
|
1434
1470
|
ok.addEventListener('click', () => submit({ status: 'ok', values }));
|
|
1435
|
-
const cancel = mkBtn('Cancel', true);
|
|
1436
|
-
cancel.addEventListener('click', () => submit({ status: 'cancel' }));
|
|
1437
1471
|
row.appendChild(ok);
|
|
1438
|
-
|
|
1472
|
+
if (allowCancel) {
|
|
1473
|
+
const cancel = mkBtn('Cancel', true);
|
|
1474
|
+
cancel.addEventListener('click', () => submit({ status: 'cancel' }));
|
|
1475
|
+
row.appendChild(cancel);
|
|
1476
|
+
}
|
|
1439
1477
|
form.appendChild(row);
|
|
1440
1478
|
} else if (kind === 'choice') {
|
|
1441
1479
|
const options = Array.isArray(req?.prompt?.options) ? req.prompt.options : [];
|
|
@@ -1464,20 +1502,24 @@ function renderPrompts() {
|
|
|
1464
1502
|
row.className = 'row';
|
|
1465
1503
|
const ok = mkBtn('Submit');
|
|
1466
1504
|
ok.addEventListener('click', () => submit({ status: 'ok', value: multiple ? Array.from(selected) : Array.from(selected)[0] || '' }));
|
|
1467
|
-
const cancel = mkBtn('Cancel', true);
|
|
1468
|
-
cancel.addEventListener('click', () => submit({ status: 'cancel' }));
|
|
1469
1505
|
row.appendChild(ok);
|
|
1470
|
-
|
|
1506
|
+
if (allowCancel) {
|
|
1507
|
+
const cancel = mkBtn('Cancel', true);
|
|
1508
|
+
cancel.addEventListener('click', () => submit({ status: 'cancel' }));
|
|
1509
|
+
row.appendChild(cancel);
|
|
1510
|
+
}
|
|
1471
1511
|
form.appendChild(row);
|
|
1472
1512
|
} else {
|
|
1473
1513
|
const row = document.createElement('div');
|
|
1474
1514
|
row.className = 'row';
|
|
1475
1515
|
const ok = mkBtn('OK');
|
|
1476
1516
|
ok.addEventListener('click', () => submit({ status: 'ok' }));
|
|
1477
|
-
const cancel = mkBtn('Cancel', true);
|
|
1478
|
-
cancel.addEventListener('click', () => submit({ status: 'cancel' }));
|
|
1479
1517
|
row.appendChild(ok);
|
|
1480
|
-
|
|
1518
|
+
if (allowCancel) {
|
|
1519
|
+
const cancel = mkBtn('Cancel', true);
|
|
1520
|
+
cancel.addEventListener('click', () => submit({ status: 'cancel' }));
|
|
1521
|
+
row.appendChild(cancel);
|
|
1522
|
+
}
|
|
1481
1523
|
form.appendChild(row);
|
|
1482
1524
|
}
|
|
1483
1525
|
|
|
@@ -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
|
|
|
@@ -6,7 +6,7 @@ UI Prompts 是 ChatOS 的全局交互队列:任意组件(AI / MCP / UI Apps
|
|
|
6
6
|
|
|
7
7
|
- 存储格式:`ui-prompts.jsonl`(JSON Lines 追加日志)
|
|
8
8
|
- 交互生命周期:`request` → `response`
|
|
9
|
-
- UI 渲染支持的 `prompt.kind` 与字段(`kv` / `choice` / `task_confirm` / `file_change_confirm`)
|
|
9
|
+
- UI 渲染支持的 `prompt.kind` 与字段(`kv` / `choice` / `task_confirm` / `file_change_confirm` / `result`)
|
|
10
10
|
- UI Apps 的 Host API 调用方式(`host.uiPrompts.*`)
|
|
11
11
|
|
|
12
12
|
实现对照(以代码为准):
|
|
@@ -146,7 +146,7 @@ UI Apps 的 `module` 应用通过 Host API 与 UI Prompts 交互:
|
|
|
146
146
|
|
|
147
147
|
| 字段 | 类型 | 必填 | 说明 |
|
|
148
148
|
|---|---:|---:|---|
|
|
149
|
-
| `kind` | `string` | 是 | 取值:`kv` / `choice` / `task_confirm` / `file_change_confirm` |
|
|
149
|
+
| `kind` | `string` | 是 | 取值:`kv` / `choice` / `task_confirm` / `file_change_confirm` / `result` |
|
|
150
150
|
| `title` | `string` | 否 | UI 标题 |
|
|
151
151
|
| `message` | `string` | 否 | UI 描述/说明 |
|
|
152
152
|
| `source` | `string` | 否 | 来源标识(UI 显示 Tag) |
|
|
@@ -376,7 +376,36 @@ UI 渲染规则:
|
|
|
376
376
|
|
|
377
377
|
---
|
|
378
378
|
|
|
379
|
-
## 9.
|
|
379
|
+
## 9. `kind="result"`:执行结果(Markdown 展示)
|
|
380
|
+
|
|
381
|
+
该类型用于“执行完成后的结果通知”。UI 会以 Markdown 形式展示结果,并在用户确认后写入 `response`。
|
|
382
|
+
|
|
383
|
+
### 9.1 请求结构
|
|
384
|
+
|
|
385
|
+
```json
|
|
386
|
+
{
|
|
387
|
+
"kind": "result",
|
|
388
|
+
"title": "执行结果",
|
|
389
|
+
"message": "已完成全部步骤。",
|
|
390
|
+
"source": "com.example.plugin:my-app",
|
|
391
|
+
"allowCancel": true,
|
|
392
|
+
"markdown": "## Done\n- step 1\n- step 2"
|
|
393
|
+
}
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
字段:
|
|
397
|
+
|
|
398
|
+
- `markdown`:可选字符串;作为 Markdown 内容展示
|
|
399
|
+
|
|
400
|
+
### 9.2 响应结构
|
|
401
|
+
|
|
402
|
+
```json
|
|
403
|
+
{ "status": "ok" }
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
---
|
|
407
|
+
|
|
408
|
+
## 10. 复杂交互的构建方式
|
|
380
409
|
|
|
381
410
|
UI Prompts 的基本单位是一条 `request` 记录。复杂交互由多条 `request/response` 串联构成:
|
|
382
411
|
|
|
@@ -390,3 +419,4 @@ UI Prompts 的基本单位是一条 `request` 记录。复杂交互由多条 `re
|
|
|
390
419
|
- “多选/单选”使用 `kind="choice"` 的 `multiple/options` 承载
|
|
391
420
|
- “任务列表确认”使用 `kind="task_confirm"` 承载
|
|
392
421
|
- “diff/命令确认”使用 `kind="file_change_confirm"` 承载
|
|
422
|
+
- “执行结果通知”使用 `kind="result"` 承载
|
|
@@ -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`,非宿主环境要可降级运行。
|
|
@@ -6,7 +6,7 @@ UI Prompts 是 ChatOS 的全局交互队列:任意组件(AI / MCP / UI Apps
|
|
|
6
6
|
|
|
7
7
|
- 存储格式:`ui-prompts.jsonl`(JSON Lines 追加日志)
|
|
8
8
|
- 交互生命周期:`request` → `response`
|
|
9
|
-
- UI 渲染支持的 `prompt.kind` 与字段(`kv` / `choice` / `task_confirm` / `file_change_confirm`)
|
|
9
|
+
- UI 渲染支持的 `prompt.kind` 与字段(`kv` / `choice` / `task_confirm` / `file_change_confirm` / `result`)
|
|
10
10
|
- UI Apps 的 Host API 调用方式(`host.uiPrompts.*`)
|
|
11
11
|
|
|
12
12
|
实现对照(以代码为准):
|
|
@@ -146,7 +146,7 @@ UI Apps 的 `module` 应用通过 Host API 与 UI Prompts 交互:
|
|
|
146
146
|
|
|
147
147
|
| 字段 | 类型 | 必填 | 说明 |
|
|
148
148
|
|---|---:|---:|---|
|
|
149
|
-
| `kind` | `string` | 是 | 取值:`kv` / `choice` / `task_confirm` / `file_change_confirm` |
|
|
149
|
+
| `kind` | `string` | 是 | 取值:`kv` / `choice` / `task_confirm` / `file_change_confirm` / `result` |
|
|
150
150
|
| `title` | `string` | 否 | UI 标题 |
|
|
151
151
|
| `message` | `string` | 否 | UI 描述/说明 |
|
|
152
152
|
| `source` | `string` | 否 | 来源标识(UI 显示 Tag) |
|
|
@@ -376,7 +376,36 @@ UI 渲染规则:
|
|
|
376
376
|
|
|
377
377
|
---
|
|
378
378
|
|
|
379
|
-
## 9.
|
|
379
|
+
## 9. `kind="result"`:执行结果(Markdown 展示)
|
|
380
|
+
|
|
381
|
+
该类型用于“执行完成后的结果通知”。UI 会以 Markdown 形式展示结果,并在用户确认后写入 `response`。
|
|
382
|
+
|
|
383
|
+
### 9.1 请求结构
|
|
384
|
+
|
|
385
|
+
```json
|
|
386
|
+
{
|
|
387
|
+
"kind": "result",
|
|
388
|
+
"title": "执行结果",
|
|
389
|
+
"message": "已完成全部步骤。",
|
|
390
|
+
"source": "com.example.plugin:my-app",
|
|
391
|
+
"allowCancel": true,
|
|
392
|
+
"markdown": "## Done\n- step 1\n- step 2"
|
|
393
|
+
}
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
字段:
|
|
397
|
+
|
|
398
|
+
- `markdown`:可选字符串;作为 Markdown 内容展示
|
|
399
|
+
|
|
400
|
+
### 9.2 响应结构
|
|
401
|
+
|
|
402
|
+
```json
|
|
403
|
+
{ "status": "ok" }
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
---
|
|
407
|
+
|
|
408
|
+
## 10. 复杂交互的构建方式
|
|
380
409
|
|
|
381
410
|
UI Prompts 的基本单位是一条 `request` 记录。复杂交互由多条 `request/response` 串联构成:
|
|
382
411
|
|
|
@@ -390,3 +419,4 @@ UI Prompts 的基本单位是一条 `request` 记录。复杂交互由多条 `re
|
|
|
390
419
|
- “多选/单选”使用 `kind="choice"` 的 `multiple/options` 承载
|
|
391
420
|
- “任务列表确认”使用 `kind="task_confirm"` 承载
|
|
392
421
|
- “diff/命令确认”使用 `kind="file_change_confirm"` 承载
|
|
422
|
+
- “执行结果通知”使用 `kind="result"` 承载
|