@leeoohoo/ui-apps-devkit 0.1.11 → 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 +275 -15
- 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');
|
|
@@ -1049,7 +1100,10 @@ const markMcpStreamStatus = (payload) => {
|
|
|
1049
1100
|
}
|
|
1050
1101
|
};
|
|
1051
1102
|
|
|
1052
|
-
const setPanelOpen = (open) => {
|
|
1103
|
+
const setPanelOpen = (open) => {
|
|
1104
|
+
panel.style.display = open ? 'flex' : 'none';
|
|
1105
|
+
if (open) refreshUiPromptsFromFile();
|
|
1106
|
+
};
|
|
1053
1107
|
fab.addEventListener('click', () => setPanelOpen(panel.style.display !== 'flex'));
|
|
1054
1108
|
panelClose.addEventListener('click', () => setPanelOpen(false));
|
|
1055
1109
|
window.addEventListener('chatos:uiPrompts:open', () => setPanelOpen(true));
|
|
@@ -1257,6 +1311,108 @@ const setMcpPanelOpen = (open) => {
|
|
|
1257
1311
|
}
|
|
1258
1312
|
};
|
|
1259
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
|
+
|
|
1260
1416
|
const runMcpTest = async () => {
|
|
1261
1417
|
const sendBtn = btnMcpSend;
|
|
1262
1418
|
try {
|
|
@@ -1419,6 +1575,10 @@ if (btnMcpClear)
|
|
|
1419
1575
|
if (mcpOutput) mcpOutput.textContent = '';
|
|
1420
1576
|
setMcpStatus('');
|
|
1421
1577
|
});
|
|
1578
|
+
if (btnMcpAsync)
|
|
1579
|
+
btnMcpAsync.addEventListener('click', () => {
|
|
1580
|
+
runMcpAsyncTest();
|
|
1581
|
+
});
|
|
1422
1582
|
if (btnMcpSend)
|
|
1423
1583
|
btnMcpSend.addEventListener('click', () => {
|
|
1424
1584
|
runMcpTest();
|
|
@@ -1432,18 +1592,31 @@ updateContextStatus();
|
|
|
1432
1592
|
|
|
1433
1593
|
const entries = [];
|
|
1434
1594
|
const listeners = new Set();
|
|
1435
|
-
const
|
|
1436
|
-
const payload = {
|
|
1437
|
-
|
|
1438
|
-
|
|
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();
|
|
1439
1611
|
};
|
|
1440
1612
|
|
|
1441
1613
|
const uuid = () => (globalThis.crypto?.randomUUID ? crypto.randomUUID() : String(Date.now()) + '-' + Math.random().toString(16).slice(2));
|
|
1442
1614
|
|
|
1443
|
-
function renderPrompts() {
|
|
1615
|
+
function renderPrompts(sourceEntries = entries) {
|
|
1444
1616
|
panelBody.textContent = '';
|
|
1445
1617
|
const pending = new Map();
|
|
1446
|
-
|
|
1618
|
+
const list = Array.isArray(sourceEntries) ? sourceEntries : [];
|
|
1619
|
+
for (const e of list) {
|
|
1447
1620
|
if (e?.type !== 'ui_prompt') continue;
|
|
1448
1621
|
const id = String(e?.requestId || '');
|
|
1449
1622
|
if (!id) continue;
|
|
@@ -1494,8 +1667,15 @@ function renderPrompts() {
|
|
|
1494
1667
|
};
|
|
1495
1668
|
|
|
1496
1669
|
const submit = async (response) => {
|
|
1497
|
-
|
|
1498
|
-
|
|
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();
|
|
1499
1679
|
};
|
|
1500
1680
|
|
|
1501
1681
|
if (kind === 'result') {
|
|
@@ -1632,6 +1812,46 @@ const callSandboxChat = async (payload, signal) => {
|
|
|
1632
1812
|
return j;
|
|
1633
1813
|
};
|
|
1634
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
|
+
|
|
1635
1855
|
const getTheme = () => currentTheme || resolveTheme();
|
|
1636
1856
|
|
|
1637
1857
|
const host = {
|
|
@@ -1675,22 +1895,41 @@ const host = {
|
|
|
1675
1895
|
},
|
|
1676
1896
|
},
|
|
1677
1897
|
uiPrompts: {
|
|
1678
|
-
read: async () =>
|
|
1898
|
+
read: async () => {
|
|
1899
|
+
const fileEntries = await readUiPromptsFromFile();
|
|
1900
|
+
return { path: sandboxPaths.uiPromptsFile || '(sandbox)', entries: fileEntries };
|
|
1901
|
+
},
|
|
1679
1902
|
onUpdate: (listener) => { listeners.add(listener); return () => listeners.delete(listener); },
|
|
1680
1903
|
request: async (payload) => {
|
|
1681
1904
|
const requestId = payload?.requestId ? String(payload.requestId) : uuid();
|
|
1682
1905
|
const prompt = payload?.prompt && typeof payload.prompt === 'object' ? { ...payload.prompt } : null;
|
|
1683
1906
|
if (prompt && !prompt.source) prompt.source = __SANDBOX__.pluginId + ':' + __SANDBOX__.appId;
|
|
1684
|
-
|
|
1685
|
-
|
|
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();
|
|
1686
1917
|
return { ok: true, requestId };
|
|
1687
1918
|
},
|
|
1688
1919
|
respond: async (payload) => {
|
|
1689
1920
|
const requestId = String(payload?.requestId || '');
|
|
1690
1921
|
if (!requestId) throw new Error('requestId is required');
|
|
1691
1922
|
const response = payload?.response && typeof payload.response === 'object' ? payload.response : null;
|
|
1692
|
-
|
|
1693
|
-
|
|
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();
|
|
1694
1933
|
return { ok: true };
|
|
1695
1934
|
},
|
|
1696
1935
|
open: () => (setPanelOpen(true), { ok: true }),
|
|
@@ -2269,6 +2508,8 @@ export async function startSandboxServer({ pluginDir, port = 4399, appId = '' })
|
|
|
2269
2508
|
ctxBase.dataDir = path.join(sandboxRoot, 'data', ctxBase.pluginId);
|
|
2270
2509
|
ensureDir(ctxBase.stateDir);
|
|
2271
2510
|
ensureDir(ctxBase.dataDir);
|
|
2511
|
+
const uiPromptsPath = path.join(ctxBase.stateDir, UI_PROMPTS_FILENAME);
|
|
2512
|
+
ensureUiPromptsFile(uiPromptsPath);
|
|
2272
2513
|
sandboxCallMeta = buildSandboxCallMeta({
|
|
2273
2514
|
rawCallMeta: app?.ai?.mcp?.callMeta,
|
|
2274
2515
|
context: {
|
|
@@ -2363,6 +2604,7 @@ export async function startSandboxServer({ pluginDir, port = 4399, appId = '' })
|
|
|
2363
2604
|
dataDir: ctxBase.dataDir || '',
|
|
2364
2605
|
pluginDir: ctxBase.pluginDir || '',
|
|
2365
2606
|
stateDir: ctxBase.stateDir || '',
|
|
2607
|
+
uiPromptsFile: uiPromptsPath || '',
|
|
2366
2608
|
sessionRoot: ctxBase.sessionRoot || '',
|
|
2367
2609
|
projectRoot: ctxBase.projectRoot || '',
|
|
2368
2610
|
};
|
|
@@ -2388,6 +2630,24 @@ export async function startSandboxServer({ pluginDir, port = 4399, appId = '' })
|
|
|
2388
2630
|
return sendJson(res, 200, { ok: true, manifest });
|
|
2389
2631
|
}
|
|
2390
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
|
+
|
|
2391
2651
|
if (pathname === '/api/sandbox/llm-config') {
|
|
2392
2652
|
if (req.method === 'GET') {
|
|
2393
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
|
|