@leeoohoo/ui-apps-devkit 0.1.10 → 0.1.12
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/sandbox/server.js +318 -16
- package/templates/basic/docs/CHATOS_UI_APPS_ASYNC_TASK_PROTOCOL.md +100 -0
- package/templates/basic/docs/CHATOS_UI_APPS_OVERVIEW.md +1 -0
- package/templates/notepad/docs/CHATOS_UI_APPS_ASYNC_TASK_PROTOCOL.md +100 -0
- package/templates/notepad/docs/CHATOS_UI_APPS_OVERVIEW.md +1 -0
package/package.json
CHANGED
package/src/sandbox/server.js
CHANGED
|
@@ -65,11 +65,58 @@ function resolveSandboxConfigPath({ primaryRoot, legacyRoot }) {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
const DEFAULT_LLM_BASE_URL = 'https://api.openai.com/v1';
|
|
68
|
+
const UI_PROMPTS_FILENAME = 'ui-prompts.jsonl';
|
|
68
69
|
|
|
69
70
|
function normalizeText(value) {
|
|
70
71
|
return typeof value === 'string' ? value.trim() : '';
|
|
71
72
|
}
|
|
72
73
|
|
|
74
|
+
function ensureUiPromptsFile(filePath) {
|
|
75
|
+
const normalized = typeof filePath === 'string' ? filePath.trim() : '';
|
|
76
|
+
if (!normalized) return '';
|
|
77
|
+
try {
|
|
78
|
+
ensureDir(path.dirname(normalized));
|
|
79
|
+
if (!fs.existsSync(normalized)) {
|
|
80
|
+
fs.writeFileSync(normalized, '', 'utf8');
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
// ignore
|
|
84
|
+
}
|
|
85
|
+
return normalized;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function appendUiPromptsEntry(filePath, entry) {
|
|
89
|
+
const target = ensureUiPromptsFile(filePath);
|
|
90
|
+
if (!target) return;
|
|
91
|
+
try {
|
|
92
|
+
const payload = entry && typeof entry === 'object' ? entry : { value: entry };
|
|
93
|
+
fs.appendFileSync(target, `${JSON.stringify(payload)}\n`, 'utf8');
|
|
94
|
+
} catch {
|
|
95
|
+
// ignore
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function readUiPromptsEntries(filePath) {
|
|
100
|
+
const target = ensureUiPromptsFile(filePath);
|
|
101
|
+
if (!target) return [];
|
|
102
|
+
try {
|
|
103
|
+
const raw = fs.readFileSync(target, 'utf8');
|
|
104
|
+
if (!raw) return [];
|
|
105
|
+
const lines = raw.split(/\r?\n/).filter((line) => line.trim());
|
|
106
|
+
const entries = [];
|
|
107
|
+
for (const line of lines) {
|
|
108
|
+
try {
|
|
109
|
+
entries.push(JSON.parse(line));
|
|
110
|
+
} catch {
|
|
111
|
+
// ignore parse errors
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return entries;
|
|
115
|
+
} catch {
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
73
120
|
function isPlainObject(value) {
|
|
74
121
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
75
122
|
}
|
|
@@ -925,7 +972,10 @@ function htmlPage() {
|
|
|
925
972
|
<input id="mcpDisableTools" type="checkbox" style="width:auto;" />
|
|
926
973
|
Disable tools
|
|
927
974
|
</label>
|
|
928
|
-
<
|
|
975
|
+
<div class="row">
|
|
976
|
+
<button id="btnMcpAsync" class="btn" type="button">AsyncTask Test</button>
|
|
977
|
+
<button id="btnMcpSend" class="btn" type="button">Send</button>
|
|
978
|
+
</div>
|
|
929
979
|
</div>
|
|
930
980
|
<div id="mcpStatus" class="muted"></div>
|
|
931
981
|
</div>
|
|
@@ -1003,6 +1053,7 @@ const mcpPanel = $('#mcpPanel');
|
|
|
1003
1053
|
const btnMcpClose = $('#btnMcpClose');
|
|
1004
1054
|
const btnMcpClear = $('#btnMcpClear');
|
|
1005
1055
|
const btnMcpSend = $('#btnMcpSend');
|
|
1056
|
+
const btnMcpAsync = $('#btnMcpAsync');
|
|
1006
1057
|
const mcpPaths = $('#mcpPaths');
|
|
1007
1058
|
const mcpWorkdir = $('#mcpWorkdir');
|
|
1008
1059
|
const mcpModelId = $('#mcpModelId');
|
|
@@ -1013,7 +1064,46 @@ const mcpStatus = $('#mcpStatus');
|
|
|
1013
1064
|
const mcpOutput = $('#mcpOutput');
|
|
1014
1065
|
const mcpConfigHint = $('#mcpConfigHint');
|
|
1015
1066
|
|
|
1016
|
-
const
|
|
1067
|
+
const mcpStreamState = {
|
|
1068
|
+
active: false,
|
|
1069
|
+
seen: false,
|
|
1070
|
+
runId: '',
|
|
1071
|
+
};
|
|
1072
|
+
|
|
1073
|
+
const resetMcpStreamState = () => {
|
|
1074
|
+
mcpStreamState.active = false;
|
|
1075
|
+
mcpStreamState.seen = false;
|
|
1076
|
+
mcpStreamState.runId = '';
|
|
1077
|
+
};
|
|
1078
|
+
|
|
1079
|
+
const markMcpStreamStatus = (payload) => {
|
|
1080
|
+
const method = payload && typeof payload.method === 'string' ? payload.method : '';
|
|
1081
|
+
if (!method.startsWith('codex_app.window_run.')) return;
|
|
1082
|
+
const params = payload && typeof payload.params === 'object' ? payload.params : null;
|
|
1083
|
+
const runId = params && typeof params.runId === 'string' ? params.runId : '';
|
|
1084
|
+
if (runId && (!mcpStreamState.runId || mcpStreamState.runId === runId)) {
|
|
1085
|
+
mcpStreamState.runId = runId;
|
|
1086
|
+
}
|
|
1087
|
+
mcpStreamState.seen = true;
|
|
1088
|
+
const status = params && typeof params.status === 'string' ? params.status.toLowerCase() : '';
|
|
1089
|
+
const done =
|
|
1090
|
+
method === 'codex_app.window_run.done' ||
|
|
1091
|
+
method === 'codex_app.window_run.completed' ||
|
|
1092
|
+
params?.done === true ||
|
|
1093
|
+
['completed', 'failed', 'aborted', 'cancelled'].includes(status);
|
|
1094
|
+
if (done) {
|
|
1095
|
+
mcpStreamState.active = false;
|
|
1096
|
+
setMcpStatus('Done');
|
|
1097
|
+
} else {
|
|
1098
|
+
mcpStreamState.active = true;
|
|
1099
|
+
setMcpStatus('Streaming...');
|
|
1100
|
+
}
|
|
1101
|
+
};
|
|
1102
|
+
|
|
1103
|
+
const setPanelOpen = (open) => {
|
|
1104
|
+
panel.style.display = open ? 'flex' : 'none';
|
|
1105
|
+
if (open) refreshUiPromptsFromFile();
|
|
1106
|
+
};
|
|
1017
1107
|
fab.addEventListener('click', () => setPanelOpen(panel.style.display !== 'flex'));
|
|
1018
1108
|
panelClose.addEventListener('click', () => setPanelOpen(false));
|
|
1019
1109
|
window.addEventListener('chatos:uiPrompts:open', () => setPanelOpen(true));
|
|
@@ -1221,6 +1311,108 @@ const setMcpPanelOpen = (open) => {
|
|
|
1221
1311
|
}
|
|
1222
1312
|
};
|
|
1223
1313
|
|
|
1314
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
1315
|
+
|
|
1316
|
+
const normalizeRequestId = (value) => (typeof value === 'string' ? value.trim() : '');
|
|
1317
|
+
|
|
1318
|
+
const buildAsyncRequestIds = (taskId) => {
|
|
1319
|
+
const id = normalizeRequestId(taskId);
|
|
1320
|
+
if (!id) return [];
|
|
1321
|
+
return [id, `mcp-task:${id}`];
|
|
1322
|
+
};
|
|
1323
|
+
|
|
1324
|
+
const extractAsyncResult = (list, requestIds) => {
|
|
1325
|
+
const ids = new Set((Array.isArray(requestIds) ? requestIds : []).filter(Boolean));
|
|
1326
|
+
if (ids.size === 0) return null;
|
|
1327
|
+
const entriesList = Array.isArray(list) ? list : [];
|
|
1328
|
+
for (let i = entriesList.length - 1; i >= 0; i -= 1) {
|
|
1329
|
+
const entry = entriesList[i];
|
|
1330
|
+
if (!entry || typeof entry !== 'object') continue;
|
|
1331
|
+
if (entry.type !== 'ui_prompt') continue;
|
|
1332
|
+
if (entry.action !== 'request') continue;
|
|
1333
|
+
const requestId = normalizeRequestId(entry.requestId);
|
|
1334
|
+
if (!requestId || !ids.has(requestId)) continue;
|
|
1335
|
+
const prompt = entry.prompt && typeof entry.prompt === 'object' ? entry.prompt : null;
|
|
1336
|
+
if (!prompt || typeof prompt.kind !== 'string' || prompt.kind.trim() !== 'result') continue;
|
|
1337
|
+
if (typeof prompt.markdown === 'string') return prompt.markdown;
|
|
1338
|
+
if (typeof prompt.result === 'string') return prompt.result;
|
|
1339
|
+
if (typeof prompt.content === 'string') return prompt.content;
|
|
1340
|
+
return '';
|
|
1341
|
+
}
|
|
1342
|
+
return null;
|
|
1343
|
+
};
|
|
1344
|
+
|
|
1345
|
+
const pollAsyncResult = async ({ taskId, timeoutMs = 6000, intervalMs = 500 } = {}) => {
|
|
1346
|
+
const requestIds = buildAsyncRequestIds(taskId);
|
|
1347
|
+
const deadline = Date.now() + Math.max(500, timeoutMs);
|
|
1348
|
+
while (Date.now() < deadline) {
|
|
1349
|
+
const fileEntries = await readUiPromptsFromFile();
|
|
1350
|
+
const text = extractAsyncResult(fileEntries, requestIds);
|
|
1351
|
+
if (text !== null) return { found: true, text };
|
|
1352
|
+
await sleep(Math.max(200, intervalMs));
|
|
1353
|
+
}
|
|
1354
|
+
return { found: false, text: '' };
|
|
1355
|
+
};
|
|
1356
|
+
|
|
1357
|
+
const buildAsyncTaskCallMeta = (taskId) => ({
|
|
1358
|
+
asyncTask: {
|
|
1359
|
+
tools: ['codex_app_window_run'],
|
|
1360
|
+
taskIdKey: 'taskId',
|
|
1361
|
+
resultSource: 'ui_prompts',
|
|
1362
|
+
uiPromptFile: 'ui-prompts.jsonl',
|
|
1363
|
+
},
|
|
1364
|
+
taskId,
|
|
1365
|
+
});
|
|
1366
|
+
|
|
1367
|
+
const runMcpAsyncTest = async () => {
|
|
1368
|
+
const sendBtn = btnMcpAsync;
|
|
1369
|
+
try {
|
|
1370
|
+
const message = mcpMessage ? String(mcpMessage.value || '').trim() : '';
|
|
1371
|
+
if (!message) {
|
|
1372
|
+
setMcpStatus('Message is required.', true);
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
resetMcpStreamState();
|
|
1376
|
+
if (sendBtn) sendBtn.disabled = true;
|
|
1377
|
+
|
|
1378
|
+
const taskId = 'task_' + uuid();
|
|
1379
|
+
const callMeta = buildAsyncTaskCallMeta(taskId);
|
|
1380
|
+
const ack = { status: 'accepted', taskId };
|
|
1381
|
+
const resultText = `AsyncTask result for ${taskId}`;
|
|
1382
|
+
|
|
1383
|
+
appendMcpOutput('asyncTask.request', { message, callMeta });
|
|
1384
|
+
appendMcpOutput('asyncTask.ack', ack);
|
|
1385
|
+
setMcpStatus(`ACK: ${taskId} (waiting for uiPrompts result)...`);
|
|
1386
|
+
|
|
1387
|
+
const pollPromise = pollAsyncResult({ taskId, timeoutMs: 8000, intervalMs: 400 });
|
|
1388
|
+
|
|
1389
|
+
setTimeout(async () => {
|
|
1390
|
+
const prompt = { kind: 'result', markdown: resultText, source: 'sandbox-async-test' };
|
|
1391
|
+
try {
|
|
1392
|
+
await host.uiPrompts.request({ requestId: taskId, prompt });
|
|
1393
|
+
appendMcpOutput('asyncTask.result', { requestId: taskId, prompt });
|
|
1394
|
+
setMcpStatus(`Result stored in uiPrompts (taskId=${taskId})`);
|
|
1395
|
+
} catch (err) {
|
|
1396
|
+
appendMcpOutput('asyncTask.error', err?.message || String(err));
|
|
1397
|
+
setMcpStatus(err?.message || String(err), true);
|
|
1398
|
+
}
|
|
1399
|
+
}, 800);
|
|
1400
|
+
|
|
1401
|
+
const pollResult = await pollPromise;
|
|
1402
|
+
if (pollResult.found) {
|
|
1403
|
+
appendMcpOutput('asyncTask.polled', pollResult.text);
|
|
1404
|
+
setMcpStatus(`Polled result (taskId=${taskId})`);
|
|
1405
|
+
} else {
|
|
1406
|
+
appendMcpOutput('asyncTask.timeout', { taskId });
|
|
1407
|
+
setMcpStatus(`Polling timeout (taskId=${taskId})`, true);
|
|
1408
|
+
}
|
|
1409
|
+
} catch (err) {
|
|
1410
|
+
setMcpStatus(err?.message || String(err), true);
|
|
1411
|
+
} finally {
|
|
1412
|
+
if (sendBtn) sendBtn.disabled = false;
|
|
1413
|
+
}
|
|
1414
|
+
};
|
|
1415
|
+
|
|
1224
1416
|
const runMcpTest = async () => {
|
|
1225
1417
|
const sendBtn = btnMcpSend;
|
|
1226
1418
|
try {
|
|
@@ -1229,6 +1421,7 @@ const runMcpTest = async () => {
|
|
|
1229
1421
|
setMcpStatus('Message is required.', true);
|
|
1230
1422
|
return;
|
|
1231
1423
|
}
|
|
1424
|
+
resetMcpStreamState();
|
|
1232
1425
|
if (sendBtn) sendBtn.disabled = true;
|
|
1233
1426
|
const payload = {
|
|
1234
1427
|
messages: [{ role: 'user', text: message }],
|
|
@@ -1255,7 +1448,11 @@ const runMcpTest = async () => {
|
|
|
1255
1448
|
if (Array.isArray(j?.toolTrace) && j.toolTrace.length > 0) {
|
|
1256
1449
|
appendMcpOutput('toolTrace', j.toolTrace);
|
|
1257
1450
|
}
|
|
1258
|
-
|
|
1451
|
+
if (mcpStreamState.seen || mcpStreamState.active) {
|
|
1452
|
+
setMcpStatus('Waiting for MCP stream...');
|
|
1453
|
+
} else {
|
|
1454
|
+
setMcpStatus('Done');
|
|
1455
|
+
}
|
|
1259
1456
|
} catch (err) {
|
|
1260
1457
|
setMcpStatus(err?.message || String(err), true);
|
|
1261
1458
|
} finally {
|
|
@@ -1378,6 +1575,10 @@ if (btnMcpClear)
|
|
|
1378
1575
|
if (mcpOutput) mcpOutput.textContent = '';
|
|
1379
1576
|
setMcpStatus('');
|
|
1380
1577
|
});
|
|
1578
|
+
if (btnMcpAsync)
|
|
1579
|
+
btnMcpAsync.addEventListener('click', () => {
|
|
1580
|
+
runMcpAsyncTest();
|
|
1581
|
+
});
|
|
1381
1582
|
if (btnMcpSend)
|
|
1382
1583
|
btnMcpSend.addEventListener('click', () => {
|
|
1383
1584
|
runMcpTest();
|
|
@@ -1391,18 +1592,31 @@ updateContextStatus();
|
|
|
1391
1592
|
|
|
1392
1593
|
const entries = [];
|
|
1393
1594
|
const listeners = new Set();
|
|
1394
|
-
const
|
|
1395
|
-
const payload = {
|
|
1396
|
-
|
|
1397
|
-
|
|
1595
|
+
const notifyUiPrompts = (list) => {
|
|
1596
|
+
const payload = {
|
|
1597
|
+
path: sandboxPaths.uiPromptsFile || '(sandbox)',
|
|
1598
|
+
entries: Array.isArray(list) ? [...list] : [],
|
|
1599
|
+
};
|
|
1600
|
+
for (const fn of listeners) {
|
|
1601
|
+
try {
|
|
1602
|
+
fn(payload);
|
|
1603
|
+
} catch {
|
|
1604
|
+
// ignore
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
};
|
|
1608
|
+
|
|
1609
|
+
const emitUpdate = async () => {
|
|
1610
|
+
await refreshUiPromptsFromFile();
|
|
1398
1611
|
};
|
|
1399
1612
|
|
|
1400
1613
|
const uuid = () => (globalThis.crypto?.randomUUID ? crypto.randomUUID() : String(Date.now()) + '-' + Math.random().toString(16).slice(2));
|
|
1401
1614
|
|
|
1402
|
-
function renderPrompts() {
|
|
1615
|
+
function renderPrompts(sourceEntries = entries) {
|
|
1403
1616
|
panelBody.textContent = '';
|
|
1404
1617
|
const pending = new Map();
|
|
1405
|
-
|
|
1618
|
+
const list = Array.isArray(sourceEntries) ? sourceEntries : [];
|
|
1619
|
+
for (const e of list) {
|
|
1406
1620
|
if (e?.type !== 'ui_prompt') continue;
|
|
1407
1621
|
const id = String(e?.requestId || '');
|
|
1408
1622
|
if (!id) continue;
|
|
@@ -1453,8 +1667,15 @@ function renderPrompts() {
|
|
|
1453
1667
|
};
|
|
1454
1668
|
|
|
1455
1669
|
const submit = async (response) => {
|
|
1456
|
-
|
|
1457
|
-
|
|
1670
|
+
const entry = {
|
|
1671
|
+
ts: new Date().toISOString(),
|
|
1672
|
+
type: 'ui_prompt',
|
|
1673
|
+
action: 'response',
|
|
1674
|
+
requestId,
|
|
1675
|
+
response,
|
|
1676
|
+
};
|
|
1677
|
+
await appendUiPromptsToFile(entry);
|
|
1678
|
+
await emitUpdate();
|
|
1458
1679
|
};
|
|
1459
1680
|
|
|
1460
1681
|
if (kind === 'result') {
|
|
@@ -1591,6 +1812,46 @@ const callSandboxChat = async (payload, signal) => {
|
|
|
1591
1812
|
return j;
|
|
1592
1813
|
};
|
|
1593
1814
|
|
|
1815
|
+
const appendUiPromptsToFile = async (entry) => {
|
|
1816
|
+
try {
|
|
1817
|
+
const r = await fetch('/api/ui-prompts/append', {
|
|
1818
|
+
method: 'POST',
|
|
1819
|
+
headers: { 'content-type': 'application/json' },
|
|
1820
|
+
body: JSON.stringify({ entry }),
|
|
1821
|
+
});
|
|
1822
|
+
const j = await r.json();
|
|
1823
|
+
return Boolean(j?.ok);
|
|
1824
|
+
} catch {
|
|
1825
|
+
return false;
|
|
1826
|
+
}
|
|
1827
|
+
};
|
|
1828
|
+
|
|
1829
|
+
const readUiPromptsFromFile = async () => {
|
|
1830
|
+
try {
|
|
1831
|
+
const r = await fetch('/api/ui-prompts/read');
|
|
1832
|
+
const j = await r.json();
|
|
1833
|
+
if (j?.ok && Array.isArray(j.entries)) return j.entries;
|
|
1834
|
+
} catch {
|
|
1835
|
+
// ignore
|
|
1836
|
+
}
|
|
1837
|
+
return [];
|
|
1838
|
+
};
|
|
1839
|
+
|
|
1840
|
+
const replaceEntries = (list) => {
|
|
1841
|
+
entries.length = 0;
|
|
1842
|
+
if (Array.isArray(list) && list.length > 0) {
|
|
1843
|
+
entries.push(...list);
|
|
1844
|
+
}
|
|
1845
|
+
};
|
|
1846
|
+
|
|
1847
|
+
const refreshUiPromptsFromFile = async () => {
|
|
1848
|
+
const fileEntries = await readUiPromptsFromFile();
|
|
1849
|
+
replaceEntries(fileEntries);
|
|
1850
|
+
notifyUiPrompts(entries);
|
|
1851
|
+
renderPrompts(entries);
|
|
1852
|
+
return entries;
|
|
1853
|
+
};
|
|
1854
|
+
|
|
1594
1855
|
const getTheme = () => currentTheme || resolveTheme();
|
|
1595
1856
|
|
|
1596
1857
|
const host = {
|
|
@@ -1634,22 +1895,41 @@ const host = {
|
|
|
1634
1895
|
},
|
|
1635
1896
|
},
|
|
1636
1897
|
uiPrompts: {
|
|
1637
|
-
read: async () =>
|
|
1898
|
+
read: async () => {
|
|
1899
|
+
const fileEntries = await readUiPromptsFromFile();
|
|
1900
|
+
return { path: sandboxPaths.uiPromptsFile || '(sandbox)', entries: fileEntries };
|
|
1901
|
+
},
|
|
1638
1902
|
onUpdate: (listener) => { listeners.add(listener); return () => listeners.delete(listener); },
|
|
1639
1903
|
request: async (payload) => {
|
|
1640
1904
|
const requestId = payload?.requestId ? String(payload.requestId) : uuid();
|
|
1641
1905
|
const prompt = payload?.prompt && typeof payload.prompt === 'object' ? { ...payload.prompt } : null;
|
|
1642
1906
|
if (prompt && !prompt.source) prompt.source = __SANDBOX__.pluginId + ':' + __SANDBOX__.appId;
|
|
1643
|
-
|
|
1644
|
-
|
|
1907
|
+
const entry = {
|
|
1908
|
+
ts: new Date().toISOString(),
|
|
1909
|
+
type: 'ui_prompt',
|
|
1910
|
+
action: 'request',
|
|
1911
|
+
requestId,
|
|
1912
|
+
runId: payload?.runId,
|
|
1913
|
+
prompt,
|
|
1914
|
+
};
|
|
1915
|
+
await appendUiPromptsToFile(entry);
|
|
1916
|
+
await emitUpdate();
|
|
1645
1917
|
return { ok: true, requestId };
|
|
1646
1918
|
},
|
|
1647
1919
|
respond: async (payload) => {
|
|
1648
1920
|
const requestId = String(payload?.requestId || '');
|
|
1649
1921
|
if (!requestId) throw new Error('requestId is required');
|
|
1650
1922
|
const response = payload?.response && typeof payload.response === 'object' ? payload.response : null;
|
|
1651
|
-
|
|
1652
|
-
|
|
1923
|
+
const entry = {
|
|
1924
|
+
ts: new Date().toISOString(),
|
|
1925
|
+
type: 'ui_prompt',
|
|
1926
|
+
action: 'response',
|
|
1927
|
+
requestId,
|
|
1928
|
+
runId: payload?.runId,
|
|
1929
|
+
response,
|
|
1930
|
+
};
|
|
1931
|
+
await appendUiPromptsToFile(entry);
|
|
1932
|
+
await emitUpdate();
|
|
1653
1933
|
return { ok: true };
|
|
1654
1934
|
},
|
|
1655
1935
|
open: () => (setPanelOpen(true), { ok: true }),
|
|
@@ -1964,6 +2244,7 @@ try {
|
|
|
1964
2244
|
if (!payload) return;
|
|
1965
2245
|
const server = payload?.serverName ? String(payload.serverName) : 'mcp';
|
|
1966
2246
|
const method = payload?.method ? String(payload.method) : 'notification';
|
|
2247
|
+
markMcpStreamStatus(payload);
|
|
1967
2248
|
appendMcpOutput(server + ' ' + method, payload?.params?.text || payload);
|
|
1968
2249
|
});
|
|
1969
2250
|
} catch {
|
|
@@ -2227,6 +2508,8 @@ export async function startSandboxServer({ pluginDir, port = 4399, appId = '' })
|
|
|
2227
2508
|
ctxBase.dataDir = path.join(sandboxRoot, 'data', ctxBase.pluginId);
|
|
2228
2509
|
ensureDir(ctxBase.stateDir);
|
|
2229
2510
|
ensureDir(ctxBase.dataDir);
|
|
2511
|
+
const uiPromptsPath = path.join(ctxBase.stateDir, UI_PROMPTS_FILENAME);
|
|
2512
|
+
ensureUiPromptsFile(uiPromptsPath);
|
|
2230
2513
|
sandboxCallMeta = buildSandboxCallMeta({
|
|
2231
2514
|
rawCallMeta: app?.ai?.mcp?.callMeta,
|
|
2232
2515
|
context: {
|
|
@@ -2321,6 +2604,7 @@ export async function startSandboxServer({ pluginDir, port = 4399, appId = '' })
|
|
|
2321
2604
|
dataDir: ctxBase.dataDir || '',
|
|
2322
2605
|
pluginDir: ctxBase.pluginDir || '',
|
|
2323
2606
|
stateDir: ctxBase.stateDir || '',
|
|
2607
|
+
uiPromptsFile: uiPromptsPath || '',
|
|
2324
2608
|
sessionRoot: ctxBase.sessionRoot || '',
|
|
2325
2609
|
projectRoot: ctxBase.projectRoot || '',
|
|
2326
2610
|
};
|
|
@@ -2346,6 +2630,24 @@ export async function startSandboxServer({ pluginDir, port = 4399, appId = '' })
|
|
|
2346
2630
|
return sendJson(res, 200, { ok: true, manifest });
|
|
2347
2631
|
}
|
|
2348
2632
|
|
|
2633
|
+
if (pathname === '/api/ui-prompts/read') {
|
|
2634
|
+
if (req.method !== 'GET') return sendJson(res, 405, { ok: false, message: 'Method not allowed' });
|
|
2635
|
+
const entries = readUiPromptsEntries(uiPromptsPath);
|
|
2636
|
+
return sendJson(res, 200, { ok: true, path: uiPromptsPath || '', entries });
|
|
2637
|
+
}
|
|
2638
|
+
|
|
2639
|
+
if (pathname === '/api/ui-prompts/append') {
|
|
2640
|
+
if (req.method !== 'POST') return sendJson(res, 405, { ok: false, message: 'Method not allowed' });
|
|
2641
|
+
try {
|
|
2642
|
+
const payload = await readJsonBody(req);
|
|
2643
|
+
const entry = payload?.entry && typeof payload.entry === 'object' ? payload.entry : payload;
|
|
2644
|
+
appendUiPromptsEntry(uiPromptsPath, entry);
|
|
2645
|
+
return sendJson(res, 200, { ok: true });
|
|
2646
|
+
} catch (err) {
|
|
2647
|
+
return sendJson(res, 200, { ok: false, message: err?.message || String(err) });
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2349
2651
|
if (pathname === '/api/sandbox/llm-config') {
|
|
2350
2652
|
if (req.method === 'GET') {
|
|
2351
2653
|
const cfg = getSandboxLlmConfig();
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# ChatOS UI Apps:AsyncTask 轮询协议(MCP 调用)
|
|
2
|
+
|
|
3
|
+
本协议用于描述 **ACK + 交互待办轮询** 的异步 MCP 调用模式。当 `callMeta.asyncTask` 配置命中某个工具时,ChatOS 不走流式通知,而是:
|
|
4
|
+
|
|
5
|
+
1. 立即接收 MCP ACK(包含 taskId)
|
|
6
|
+
2. 后台执行任务
|
|
7
|
+
3. 结果写入「交互待办」(`ui-prompts.jsonl`)
|
|
8
|
+
4. ChatOS 轮询匹配结果并返回给工具调用
|
|
9
|
+
|
|
10
|
+
> 关键点:**没有流式通知**,只能通过轮询 `ui-prompts.jsonl` 获取结果。
|
|
11
|
+
|
|
12
|
+
## 1) 触发条件(callMeta.asyncTask)
|
|
13
|
+
|
|
14
|
+
当 MCP server 的 `callMeta` 包含 `asyncTask`,且 `tools` 覆盖当前工具名时,ChatOS 会启用轮询模式。
|
|
15
|
+
|
|
16
|
+
示例:
|
|
17
|
+
```json
|
|
18
|
+
{
|
|
19
|
+
"asyncTask": {
|
|
20
|
+
"tools": ["codex_app_window_run"],
|
|
21
|
+
"taskIdKey": "taskId",
|
|
22
|
+
"resultSource": "ui_prompts",
|
|
23
|
+
"uiPromptFile": "ui-prompts.jsonl",
|
|
24
|
+
"pollIntervalMs": 1000
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
约定字段:
|
|
30
|
+
- `tools`: 触发异步模式的 tool 名列表(小写匹配)
|
|
31
|
+
- `taskIdKey`: taskId 字段名(默认 `taskId`)
|
|
32
|
+
- `resultSource`: 固定 `ui_prompts`
|
|
33
|
+
- `uiPromptFile`: 固定 `ui-prompts.jsonl`
|
|
34
|
+
- `pollIntervalMs`: 轮询间隔(200-5000ms)
|
|
35
|
+
|
|
36
|
+
## 2) ACK 规范(MCP 返回)
|
|
37
|
+
|
|
38
|
+
工具调用应立即返回 ACK,并带上 `taskId`:
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{
|
|
42
|
+
"status": "accepted",
|
|
43
|
+
"taskId": "task_123"
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
> ChatOS 不会依赖 ACK 的结构完成轮询,但该 ACK 对其它客户端/调试很重要。
|
|
48
|
+
|
|
49
|
+
## 3) 结果写入(交互待办)
|
|
50
|
+
|
|
51
|
+
异步结果必须写入 `ui-prompts.jsonl`,格式必须满足以下条件:
|
|
52
|
+
|
|
53
|
+
- `type = "ui_prompt"`
|
|
54
|
+
- `action = "request"`
|
|
55
|
+
- `requestId = taskId` 或 `mcp-task:<taskId>`
|
|
56
|
+
- `prompt.kind = "result"`
|
|
57
|
+
|
|
58
|
+
示例:
|
|
59
|
+
```json
|
|
60
|
+
{
|
|
61
|
+
"ts": "2025-01-01T00:00:00.000Z",
|
|
62
|
+
"type": "ui_prompt",
|
|
63
|
+
"action": "request",
|
|
64
|
+
"requestId": "task_123",
|
|
65
|
+
"prompt": {
|
|
66
|
+
"kind": "result",
|
|
67
|
+
"markdown": "final output"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`prompt` 文本字段优先级(ChatOS 解析顺序):
|
|
73
|
+
1. `prompt.markdown`
|
|
74
|
+
2. `prompt.result`
|
|
75
|
+
3. `prompt.content`
|
|
76
|
+
|
|
77
|
+
## 4) 轮询规则(ChatOS)
|
|
78
|
+
|
|
79
|
+
ChatOS 会在 `ui-prompts.jsonl` 中按以下规则匹配结果:
|
|
80
|
+
|
|
81
|
+
- `type === "ui_prompt"`
|
|
82
|
+
- `action === "request"`
|
|
83
|
+
- `prompt.kind === "result"`
|
|
84
|
+
- `requestId in [taskId, "mcp-task:" + taskId]`
|
|
85
|
+
|
|
86
|
+
匹配成功即返回结果文本给工具调用。
|
|
87
|
+
|
|
88
|
+
## 5) 沙箱调试(Devkit)
|
|
89
|
+
|
|
90
|
+
Devkit 沙箱已提供 `uiPrompts` 内存存储与 UI 面板,可用于调试本协议:
|
|
91
|
+
|
|
92
|
+
- 通过 **MCP Test 面板的 AsyncTask Test** 按钮生成 taskId + 写入结果,并执行轮询匹配
|
|
93
|
+
- 在 MCP Output 中可看到轮询命中结果,UI Prompts 面板可看到 result 类型条目
|
|
94
|
+
- 沙箱会同时写入 `stateDir/ui-prompts.jsonl`(可在 MCP Test 的 Paths 中查看路径)
|
|
95
|
+
|
|
96
|
+
## 6) 常见问题
|
|
97
|
+
|
|
98
|
+
- **轮询超时**:确认 `ui-prompts.jsonl` 中是否有正确格式的 `prompt.kind=result`。
|
|
99
|
+
- **找不到结果**:确认 `requestId` 为 `taskId` 或 `mcp-task:<taskId>`。
|
|
100
|
+
- **结果为空**:确认 `prompt.markdown/result/content` 至少存在一个文本字段。
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
- [`CHATOS_UI_APPS_STYLE_GUIDE.md`](./CHATOS_UI_APPS_STYLE_GUIDE.md):主题与样式约定(CSS Tokens / 主题切换)。
|
|
10
10
|
- [`CHATOS_UI_APPS_TROUBLESHOOTING.md`](./CHATOS_UI_APPS_TROUBLESHOOTING.md):常见问题与排查清单。
|
|
11
11
|
- [`CHATOS_UI_PROMPTS_PROTOCOL.md`](./CHATOS_UI_PROMPTS_PROTOCOL.md):右下角笑脸「交互待办(UI Prompts)」协议(表单/单选/多选/复杂确认)。
|
|
12
|
+
- [`CHATOS_UI_APPS_ASYNC_TASK_PROTOCOL.md`](./CHATOS_UI_APPS_ASYNC_TASK_PROTOCOL.md):AsyncTask 轮询协议(ACK + 交互待办)。
|
|
12
13
|
- [`CHATOS_UI_APPS_BACKEND_PROTOCOL.md`](./CHATOS_UI_APPS_BACKEND_PROTOCOL.md):插件后端(Electron main 进程)协议与 `ctx` 运行时上下文。
|
|
13
14
|
- [`CHATOS_UI_APPS_AI_CONTRIBUTIONS.md`](./CHATOS_UI_APPS_AI_CONTRIBUTIONS.md):应用如何对 Chat Agent 暴露 MCP/Prompt(含命名规则、合并规则、内置清单机制)。
|
|
14
15
|
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# ChatOS UI Apps:AsyncTask 轮询协议(MCP 调用)
|
|
2
|
+
|
|
3
|
+
本协议用于描述 **ACK + 交互待办轮询** 的异步 MCP 调用模式。当 `callMeta.asyncTask` 配置命中某个工具时,ChatOS 不走流式通知,而是:
|
|
4
|
+
|
|
5
|
+
1. 立即接收 MCP ACK(包含 taskId)
|
|
6
|
+
2. 后台执行任务
|
|
7
|
+
3. 结果写入「交互待办」(`ui-prompts.jsonl`)
|
|
8
|
+
4. ChatOS 轮询匹配结果并返回给工具调用
|
|
9
|
+
|
|
10
|
+
> 关键点:**没有流式通知**,只能通过轮询 `ui-prompts.jsonl` 获取结果。
|
|
11
|
+
|
|
12
|
+
## 1) 触发条件(callMeta.asyncTask)
|
|
13
|
+
|
|
14
|
+
当 MCP server 的 `callMeta` 包含 `asyncTask`,且 `tools` 覆盖当前工具名时,ChatOS 会启用轮询模式。
|
|
15
|
+
|
|
16
|
+
示例:
|
|
17
|
+
```json
|
|
18
|
+
{
|
|
19
|
+
"asyncTask": {
|
|
20
|
+
"tools": ["codex_app_window_run"],
|
|
21
|
+
"taskIdKey": "taskId",
|
|
22
|
+
"resultSource": "ui_prompts",
|
|
23
|
+
"uiPromptFile": "ui-prompts.jsonl",
|
|
24
|
+
"pollIntervalMs": 1000
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
约定字段:
|
|
30
|
+
- `tools`: 触发异步模式的 tool 名列表(小写匹配)
|
|
31
|
+
- `taskIdKey`: taskId 字段名(默认 `taskId`)
|
|
32
|
+
- `resultSource`: 固定 `ui_prompts`
|
|
33
|
+
- `uiPromptFile`: 固定 `ui-prompts.jsonl`
|
|
34
|
+
- `pollIntervalMs`: 轮询间隔(200-5000ms)
|
|
35
|
+
|
|
36
|
+
## 2) ACK 规范(MCP 返回)
|
|
37
|
+
|
|
38
|
+
工具调用应立即返回 ACK,并带上 `taskId`:
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{
|
|
42
|
+
"status": "accepted",
|
|
43
|
+
"taskId": "task_123"
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
> ChatOS 不会依赖 ACK 的结构完成轮询,但该 ACK 对其它客户端/调试很重要。
|
|
48
|
+
|
|
49
|
+
## 3) 结果写入(交互待办)
|
|
50
|
+
|
|
51
|
+
异步结果必须写入 `ui-prompts.jsonl`,格式必须满足以下条件:
|
|
52
|
+
|
|
53
|
+
- `type = "ui_prompt"`
|
|
54
|
+
- `action = "request"`
|
|
55
|
+
- `requestId = taskId` 或 `mcp-task:<taskId>`
|
|
56
|
+
- `prompt.kind = "result"`
|
|
57
|
+
|
|
58
|
+
示例:
|
|
59
|
+
```json
|
|
60
|
+
{
|
|
61
|
+
"ts": "2025-01-01T00:00:00.000Z",
|
|
62
|
+
"type": "ui_prompt",
|
|
63
|
+
"action": "request",
|
|
64
|
+
"requestId": "task_123",
|
|
65
|
+
"prompt": {
|
|
66
|
+
"kind": "result",
|
|
67
|
+
"markdown": "final output"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`prompt` 文本字段优先级(ChatOS 解析顺序):
|
|
73
|
+
1. `prompt.markdown`
|
|
74
|
+
2. `prompt.result`
|
|
75
|
+
3. `prompt.content`
|
|
76
|
+
|
|
77
|
+
## 4) 轮询规则(ChatOS)
|
|
78
|
+
|
|
79
|
+
ChatOS 会在 `ui-prompts.jsonl` 中按以下规则匹配结果:
|
|
80
|
+
|
|
81
|
+
- `type === "ui_prompt"`
|
|
82
|
+
- `action === "request"`
|
|
83
|
+
- `prompt.kind === "result"`
|
|
84
|
+
- `requestId in [taskId, "mcp-task:" + taskId]`
|
|
85
|
+
|
|
86
|
+
匹配成功即返回结果文本给工具调用。
|
|
87
|
+
|
|
88
|
+
## 5) 沙箱调试(Devkit)
|
|
89
|
+
|
|
90
|
+
Devkit 沙箱已提供 `uiPrompts` 内存存储与 UI 面板,可用于调试本协议:
|
|
91
|
+
|
|
92
|
+
- 通过 **MCP Test 面板的 AsyncTask Test** 按钮生成 taskId + 写入结果,并执行轮询匹配
|
|
93
|
+
- 在 MCP Output 中可看到轮询命中结果,UI Prompts 面板可看到 result 类型条目
|
|
94
|
+
- 沙箱会同时写入 `stateDir/ui-prompts.jsonl`(可在 MCP Test 的 Paths 中查看路径)
|
|
95
|
+
|
|
96
|
+
## 6) 常见问题
|
|
97
|
+
|
|
98
|
+
- **轮询超时**:确认 `ui-prompts.jsonl` 中是否有正确格式的 `prompt.kind=result`。
|
|
99
|
+
- **找不到结果**:确认 `requestId` 为 `taskId` 或 `mcp-task:<taskId>`。
|
|
100
|
+
- **结果为空**:确认 `prompt.markdown/result/content` 至少存在一个文本字段。
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
- [`CHATOS_UI_APPS_STYLE_GUIDE.md`](./CHATOS_UI_APPS_STYLE_GUIDE.md):主题与样式约定(CSS Tokens / 主题切换)。
|
|
10
10
|
- [`CHATOS_UI_APPS_TROUBLESHOOTING.md`](./CHATOS_UI_APPS_TROUBLESHOOTING.md):常见问题与排查清单。
|
|
11
11
|
- [`CHATOS_UI_PROMPTS_PROTOCOL.md`](./CHATOS_UI_PROMPTS_PROTOCOL.md):右下角笑脸「交互待办(UI Prompts)」协议(表单/单选/多选/复杂确认)。
|
|
12
|
+
- [`CHATOS_UI_APPS_ASYNC_TASK_PROTOCOL.md`](./CHATOS_UI_APPS_ASYNC_TASK_PROTOCOL.md):AsyncTask 轮询协议(ACK + 交互待办)。
|
|
12
13
|
- [`CHATOS_UI_APPS_BACKEND_PROTOCOL.md`](./CHATOS_UI_APPS_BACKEND_PROTOCOL.md):插件后端(Electron main 进程)协议与 `ctx` 运行时上下文。
|
|
13
14
|
- [`CHATOS_UI_APPS_AI_CONTRIBUTIONS.md`](./CHATOS_UI_APPS_AI_CONTRIBUTIONS.md):应用如何对 Chat Agent 暴露 MCP/Prompt(含命名规则、合并规则、内置清单机制)。
|
|
14
15
|
|