@leeoohoo/ui-apps-devkit 0.1.7 → 0.1.9
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
CHANGED
package/src/sandbox/server.js
CHANGED
|
@@ -8,6 +8,8 @@ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
|
|
|
8
8
|
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
|
9
9
|
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
10
10
|
import { WebSocketClientTransport } from '@modelcontextprotocol/sdk/client/websocket.js';
|
|
11
|
+
import { LoggingMessageNotificationSchema, NotificationSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
12
|
+
import * as z from 'zod/v4';
|
|
11
13
|
|
|
12
14
|
import { copyDir, ensureDir, isDirectory, isFile } from '../lib/fs.js';
|
|
13
15
|
import { loadPluginManifest, pickAppFromManifest } from '../lib/plugin.js';
|
|
@@ -273,6 +275,34 @@ function formatMcpToolResult(serverName, toolName, result) {
|
|
|
273
275
|
return `${header}\n${segments.join('\n\n')}`;
|
|
274
276
|
}
|
|
275
277
|
|
|
278
|
+
const MCP_STREAM_NOTIFICATION_METHODS = [
|
|
279
|
+
'codex_app.window_run.stream',
|
|
280
|
+
'codex_app.window_run.done',
|
|
281
|
+
'codex_app.window_run.completed',
|
|
282
|
+
];
|
|
283
|
+
|
|
284
|
+
const buildLooseNotificationSchema = (method) =>
|
|
285
|
+
NotificationSchema.extend({
|
|
286
|
+
method: z.literal(method),
|
|
287
|
+
params: z.unknown().optional(),
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
function registerMcpNotificationHandlers(client, { serverName, onNotification } = {}) {
|
|
291
|
+
if (!client || typeof client.setNotificationHandler !== 'function') return;
|
|
292
|
+
if (typeof onNotification !== 'function') return;
|
|
293
|
+
const emit = (notification) => {
|
|
294
|
+
try {
|
|
295
|
+
onNotification({ serverName, ...notification });
|
|
296
|
+
} catch {
|
|
297
|
+
// ignore notification relay errors
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
client.setNotificationHandler(LoggingMessageNotificationSchema, (notification) => emit(notification));
|
|
301
|
+
MCP_STREAM_NOTIFICATION_METHODS.forEach((method) => {
|
|
302
|
+
client.setNotificationHandler(buildLooseNotificationSchema(method), (notification) => emit(notification));
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
276
306
|
async function listAllMcpTools(client) {
|
|
277
307
|
const collected = [];
|
|
278
308
|
let cursor = null;
|
|
@@ -290,15 +320,17 @@ async function listAllMcpTools(client) {
|
|
|
290
320
|
return collected;
|
|
291
321
|
}
|
|
292
322
|
|
|
293
|
-
async function connectMcpServer(entry) {
|
|
323
|
+
async function connectMcpServer(entry, options = {}) {
|
|
294
324
|
if (!entry || typeof entry !== 'object') return null;
|
|
295
325
|
const serverName = normalizeText(entry.name) || 'mcp_server';
|
|
326
|
+
const onNotification = typeof options?.onNotification === 'function' ? options.onNotification : null;
|
|
296
327
|
const env = { ...process.env };
|
|
297
328
|
if (!env.MODEL_CLI_SESSION_ROOT) env.MODEL_CLI_SESSION_ROOT = process.cwd();
|
|
298
329
|
if (!env.MODEL_CLI_WORKSPACE_ROOT) env.MODEL_CLI_WORKSPACE_ROOT = process.cwd();
|
|
299
330
|
|
|
300
331
|
if (entry.command) {
|
|
301
332
|
const client = new Client({ name: 'sandbox', version: '0.1.0' });
|
|
333
|
+
registerMcpNotificationHandlers(client, { serverName, onNotification });
|
|
302
334
|
const transport = new StdioClientTransport({
|
|
303
335
|
command: entry.command,
|
|
304
336
|
args: Array.isArray(entry.args) ? entry.args : [],
|
|
@@ -318,6 +350,7 @@ async function connectMcpServer(entry) {
|
|
|
318
350
|
if (parsed.protocol === 'ws:' || parsed.protocol === 'wss:') {
|
|
319
351
|
const client = new Client({ name: 'sandbox', version: '0.1.0' });
|
|
320
352
|
const transport = new WebSocketClientTransport(parsed);
|
|
353
|
+
registerMcpNotificationHandlers(client, { serverName, onNotification });
|
|
321
354
|
await client.connect(transport);
|
|
322
355
|
const tools = await listAllMcpTools(client);
|
|
323
356
|
return { serverName, client, transport, tools };
|
|
@@ -327,6 +360,7 @@ async function connectMcpServer(entry) {
|
|
|
327
360
|
try {
|
|
328
361
|
const client = new Client({ name: 'sandbox', version: '0.1.0' });
|
|
329
362
|
const transport = new StreamableHTTPClientTransport(parsed);
|
|
363
|
+
registerMcpNotificationHandlers(client, { serverName, onNotification });
|
|
330
364
|
await client.connect(transport);
|
|
331
365
|
const tools = await listAllMcpTools(client);
|
|
332
366
|
return { serverName, client, transport, tools };
|
|
@@ -336,6 +370,7 @@ async function connectMcpServer(entry) {
|
|
|
336
370
|
try {
|
|
337
371
|
const client = new Client({ name: 'sandbox', version: '0.1.0' });
|
|
338
372
|
const transport = new SSEClientTransport(parsed);
|
|
373
|
+
registerMcpNotificationHandlers(client, { serverName, onNotification });
|
|
339
374
|
await client.connect(transport);
|
|
340
375
|
const tools = await listAllMcpTools(client);
|
|
341
376
|
return { serverName, client, transport, tools };
|
|
@@ -1407,6 +1442,7 @@ function renderPrompts() {
|
|
|
1407
1442
|
form.style.gap = '10px';
|
|
1408
1443
|
|
|
1409
1444
|
const kind = String(req?.prompt?.kind || '');
|
|
1445
|
+
const allowCancel = req?.prompt?.allowCancel !== false;
|
|
1410
1446
|
|
|
1411
1447
|
const mkBtn = (label, danger) => {
|
|
1412
1448
|
const btn = document.createElement('button');
|
|
@@ -1421,7 +1457,31 @@ function renderPrompts() {
|
|
|
1421
1457
|
emitUpdate();
|
|
1422
1458
|
};
|
|
1423
1459
|
|
|
1424
|
-
if (kind === '
|
|
1460
|
+
if (kind === 'result') {
|
|
1461
|
+
const markdownText =
|
|
1462
|
+
typeof req?.prompt?.markdown === 'string'
|
|
1463
|
+
? req.prompt.markdown
|
|
1464
|
+
: typeof req?.prompt?.result === 'string'
|
|
1465
|
+
? req.prompt.result
|
|
1466
|
+
: typeof req?.prompt?.content === 'string'
|
|
1467
|
+
? req.prompt.content
|
|
1468
|
+
: '';
|
|
1469
|
+
const markdown = document.createElement('pre');
|
|
1470
|
+
markdown.className = 'mono';
|
|
1471
|
+
markdown.textContent = markdownText || '(无结果内容)';
|
|
1472
|
+
form.appendChild(markdown);
|
|
1473
|
+
const row = document.createElement('div');
|
|
1474
|
+
row.className = 'row';
|
|
1475
|
+
const ok = mkBtn('OK');
|
|
1476
|
+
ok.addEventListener('click', () => submit({ status: 'ok' }));
|
|
1477
|
+
row.appendChild(ok);
|
|
1478
|
+
if (allowCancel) {
|
|
1479
|
+
const cancel = mkBtn('Cancel', true);
|
|
1480
|
+
cancel.addEventListener('click', () => submit({ status: 'cancel' }));
|
|
1481
|
+
row.appendChild(cancel);
|
|
1482
|
+
}
|
|
1483
|
+
form.appendChild(row);
|
|
1484
|
+
} else if (kind === 'kv') {
|
|
1425
1485
|
const fields = Array.isArray(req?.prompt?.fields) ? req.prompt.fields : [];
|
|
1426
1486
|
const values = {};
|
|
1427
1487
|
for (const f of fields) {
|
|
@@ -1443,10 +1503,12 @@ function renderPrompts() {
|
|
|
1443
1503
|
row.className = 'row';
|
|
1444
1504
|
const ok = mkBtn('Submit');
|
|
1445
1505
|
ok.addEventListener('click', () => submit({ status: 'ok', values }));
|
|
1446
|
-
const cancel = mkBtn('Cancel', true);
|
|
1447
|
-
cancel.addEventListener('click', () => submit({ status: 'cancel' }));
|
|
1448
1506
|
row.appendChild(ok);
|
|
1449
|
-
|
|
1507
|
+
if (allowCancel) {
|
|
1508
|
+
const cancel = mkBtn('Cancel', true);
|
|
1509
|
+
cancel.addEventListener('click', () => submit({ status: 'cancel' }));
|
|
1510
|
+
row.appendChild(cancel);
|
|
1511
|
+
}
|
|
1450
1512
|
form.appendChild(row);
|
|
1451
1513
|
} else if (kind === 'choice') {
|
|
1452
1514
|
const options = Array.isArray(req?.prompt?.options) ? req.prompt.options : [];
|
|
@@ -1475,20 +1537,24 @@ function renderPrompts() {
|
|
|
1475
1537
|
row.className = 'row';
|
|
1476
1538
|
const ok = mkBtn('Submit');
|
|
1477
1539
|
ok.addEventListener('click', () => submit({ status: 'ok', value: multiple ? Array.from(selected) : Array.from(selected)[0] || '' }));
|
|
1478
|
-
const cancel = mkBtn('Cancel', true);
|
|
1479
|
-
cancel.addEventListener('click', () => submit({ status: 'cancel' }));
|
|
1480
1540
|
row.appendChild(ok);
|
|
1481
|
-
|
|
1541
|
+
if (allowCancel) {
|
|
1542
|
+
const cancel = mkBtn('Cancel', true);
|
|
1543
|
+
cancel.addEventListener('click', () => submit({ status: 'cancel' }));
|
|
1544
|
+
row.appendChild(cancel);
|
|
1545
|
+
}
|
|
1482
1546
|
form.appendChild(row);
|
|
1483
1547
|
} else {
|
|
1484
1548
|
const row = document.createElement('div');
|
|
1485
1549
|
row.className = 'row';
|
|
1486
1550
|
const ok = mkBtn('OK');
|
|
1487
1551
|
ok.addEventListener('click', () => submit({ status: 'ok' }));
|
|
1488
|
-
const cancel = mkBtn('Cancel', true);
|
|
1489
|
-
cancel.addEventListener('click', () => submit({ status: 'cancel' }));
|
|
1490
1552
|
row.appendChild(ok);
|
|
1491
|
-
|
|
1553
|
+
if (allowCancel) {
|
|
1554
|
+
const cancel = mkBtn('Cancel', true);
|
|
1555
|
+
cancel.addEventListener('click', () => submit({ status: 'cancel' }));
|
|
1556
|
+
row.appendChild(cancel);
|
|
1557
|
+
}
|
|
1492
1558
|
form.appendChild(row);
|
|
1493
1559
|
}
|
|
1494
1560
|
|
|
@@ -1887,6 +1953,19 @@ const scheduleReload = (() => {
|
|
|
1887
1953
|
try {
|
|
1888
1954
|
const es = new EventSource('/events');
|
|
1889
1955
|
es.addEventListener('reload', () => scheduleReload());
|
|
1956
|
+
es.addEventListener('mcp-notification', (event) => {
|
|
1957
|
+
if (!event?.data) return;
|
|
1958
|
+
let payload = null;
|
|
1959
|
+
try {
|
|
1960
|
+
payload = JSON.parse(event.data);
|
|
1961
|
+
} catch {
|
|
1962
|
+
payload = { raw: event.data };
|
|
1963
|
+
}
|
|
1964
|
+
if (!payload) return;
|
|
1965
|
+
const server = payload?.serverName ? String(payload.serverName) : 'mcp';
|
|
1966
|
+
const method = payload?.method ? String(payload.method) : 'notification';
|
|
1967
|
+
appendMcpOutput(`${server} ${method}`, payload?.params?.text || payload);
|
|
1968
|
+
});
|
|
1890
1969
|
} catch {
|
|
1891
1970
|
// ignore
|
|
1892
1971
|
}
|
|
@@ -1939,6 +2018,7 @@ export async function startSandboxServer({ pluginDir, port = 4399, appId = '' })
|
|
|
1939
2018
|
let mcpRuntime = null;
|
|
1940
2019
|
let mcpRuntimePromise = null;
|
|
1941
2020
|
let sandboxCallMeta = null;
|
|
2021
|
+
let relayMcpNotification = null;
|
|
1942
2022
|
|
|
1943
2023
|
const resetMcpRuntime = async () => {
|
|
1944
2024
|
const runtime = mcpRuntime;
|
|
@@ -1965,7 +2045,9 @@ export async function startSandboxServer({ pluginDir, port = 4399, appId = '' })
|
|
|
1965
2045
|
if (mcpRuntime) return mcpRuntime;
|
|
1966
2046
|
if (!mcpRuntimePromise) {
|
|
1967
2047
|
mcpRuntimePromise = (async () => {
|
|
1968
|
-
const handle = await connectMcpServer(appMcpEntry
|
|
2048
|
+
const handle = await connectMcpServer(appMcpEntry, {
|
|
2049
|
+
onNotification: relayMcpNotification,
|
|
2050
|
+
});
|
|
1969
2051
|
if (!handle) return null;
|
|
1970
2052
|
const toolEntries = Array.isArray(handle.tools)
|
|
1971
2053
|
? handle.tools.map((tool) => {
|
|
@@ -2172,6 +2254,13 @@ export async function startSandboxServer({ pluginDir, port = 4399, appId = '' })
|
|
|
2172
2254
|
sseWrite(res, event, data);
|
|
2173
2255
|
}
|
|
2174
2256
|
};
|
|
2257
|
+
relayMcpNotification = (notification) => {
|
|
2258
|
+
if (!notification) return;
|
|
2259
|
+
sseBroadcast('mcp-notification', {
|
|
2260
|
+
...notification,
|
|
2261
|
+
receivedAt: new Date().toISOString(),
|
|
2262
|
+
});
|
|
2263
|
+
};
|
|
2175
2264
|
|
|
2176
2265
|
let changeSeq = 0;
|
|
2177
2266
|
const stopWatch = startRecursiveWatcher(pluginDir, ({ eventType, filePath }) => {
|
|
@@ -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"` 承载
|
|
@@ -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"` 承载
|