@leeoohoo/ui-apps-devkit 0.1.12 → 0.1.14
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 +175 -8
package/package.json
CHANGED
package/src/sandbox/server.js
CHANGED
|
@@ -66,11 +66,124 @@ function resolveSandboxConfigPath({ primaryRoot, legacyRoot }) {
|
|
|
66
66
|
|
|
67
67
|
const DEFAULT_LLM_BASE_URL = 'https://api.openai.com/v1';
|
|
68
68
|
const UI_PROMPTS_FILENAME = 'ui-prompts.jsonl';
|
|
69
|
+
const DEFAULT_ASYNC_POLL_MS = 1000;
|
|
70
|
+
const DEFAULT_ASYNC_TIMEOUT_MS = 2 * 60 * 60 * 1000;
|
|
71
|
+
|
|
72
|
+
const sleepMs = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
69
73
|
|
|
70
74
|
function normalizeText(value) {
|
|
71
75
|
return typeof value === 'string' ? value.trim() : '';
|
|
72
76
|
}
|
|
73
77
|
|
|
78
|
+
function normalizeAsyncToolName(value) {
|
|
79
|
+
const normalized = normalizeText(value);
|
|
80
|
+
return normalized ? normalized.toLowerCase() : '';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function normalizeAsyncTaskConfig(raw, toolName) {
|
|
84
|
+
if (!raw || typeof raw !== 'object') return null;
|
|
85
|
+
const tools = Array.isArray(raw.tools) ? raw.tools.map(normalizeAsyncToolName).filter(Boolean) : [];
|
|
86
|
+
const toolKey = normalizeAsyncToolName(toolName);
|
|
87
|
+
if (tools.length > 0 && toolKey && !tools.includes(toolKey)) return null;
|
|
88
|
+
const taskIdKey = typeof raw.taskIdKey === 'string' ? raw.taskIdKey.trim() : 'taskId';
|
|
89
|
+
if (!taskIdKey) return null;
|
|
90
|
+
const resultSource = typeof raw.resultSource === 'string' ? raw.resultSource.trim().toLowerCase() : 'ui_prompts';
|
|
91
|
+
const uiPromptFile = typeof raw.uiPromptFile === 'string' ? raw.uiPromptFile.trim() : UI_PROMPTS_FILENAME;
|
|
92
|
+
const pollIntervalMs = Number.isFinite(Number(raw.pollIntervalMs))
|
|
93
|
+
? Math.max(200, Math.min(5000, Number(raw.pollIntervalMs)))
|
|
94
|
+
: DEFAULT_ASYNC_POLL_MS;
|
|
95
|
+
return {
|
|
96
|
+
taskIdKey,
|
|
97
|
+
resultSource,
|
|
98
|
+
uiPromptFile,
|
|
99
|
+
pollIntervalMs,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function generateTaskId({ sessionId, serverName, toolName } = {}) {
|
|
104
|
+
const parts = [];
|
|
105
|
+
const sid = normalizeText(sessionId);
|
|
106
|
+
if (sid) parts.push(sid);
|
|
107
|
+
const server = normalizeText(serverName);
|
|
108
|
+
const tool = normalizeText(toolName);
|
|
109
|
+
if (server || tool) parts.push([server, tool].filter(Boolean).join('.'));
|
|
110
|
+
let token = '';
|
|
111
|
+
if (typeof globalThis.crypto?.randomUUID === 'function') {
|
|
112
|
+
token = globalThis.crypto.randomUUID();
|
|
113
|
+
} else {
|
|
114
|
+
token = Date.now().toString(36) + '_' + Math.random().toString(16).slice(2, 10);
|
|
115
|
+
}
|
|
116
|
+
parts.push(token);
|
|
117
|
+
return parts.filter(Boolean).join('_');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function resolveAsyncTimeoutMs(options) {
|
|
121
|
+
const timeout = Number(options?.timeoutMs || options?.maxTotalTimeout || options?.timeout || 0);
|
|
122
|
+
if (Number.isFinite(timeout) && timeout > 0) return timeout;
|
|
123
|
+
return DEFAULT_ASYNC_TIMEOUT_MS;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function extractUiPromptResult(entries, requestIds) {
|
|
127
|
+
const ids = new Set(
|
|
128
|
+
(Array.isArray(requestIds) ? requestIds : [])
|
|
129
|
+
.map((id) => (typeof id === 'string' ? id.trim() : ''))
|
|
130
|
+
.filter(Boolean)
|
|
131
|
+
);
|
|
132
|
+
if (ids.size === 0) return null;
|
|
133
|
+
const list = Array.isArray(entries) ? entries : [];
|
|
134
|
+
for (let i = list.length - 1; i >= 0; i -= 1) {
|
|
135
|
+
const entry = list[i];
|
|
136
|
+
if (!entry || typeof entry !== 'object') continue;
|
|
137
|
+
if (entry.type !== 'ui_prompt') continue;
|
|
138
|
+
if (entry.action !== 'request') continue;
|
|
139
|
+
const requestId = typeof entry.requestId === 'string' ? entry.requestId.trim() : '';
|
|
140
|
+
if (!requestId || !ids.has(requestId)) continue;
|
|
141
|
+
const prompt = entry.prompt && typeof entry.prompt === 'object' ? entry.prompt : null;
|
|
142
|
+
if (!prompt || typeof prompt.kind !== 'string' || prompt.kind.trim() !== 'result') continue;
|
|
143
|
+
const markdown =
|
|
144
|
+
typeof prompt.markdown === 'string'
|
|
145
|
+
? prompt.markdown
|
|
146
|
+
: typeof prompt.result === 'string'
|
|
147
|
+
? prompt.result
|
|
148
|
+
: typeof prompt.content === 'string'
|
|
149
|
+
? prompt.content
|
|
150
|
+
: '';
|
|
151
|
+
return markdown;
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function waitForUiPromptResult({ taskId, config, callMeta, options } = {}) {
|
|
157
|
+
const id = typeof taskId === 'string' ? taskId.trim() : '';
|
|
158
|
+
if (!id) return { found: false, text: '' };
|
|
159
|
+
|
|
160
|
+
const stateDir = callMeta?.chatos?.uiApp?.stateDir
|
|
161
|
+
? String(callMeta.chatos.uiApp.stateDir)
|
|
162
|
+
: '';
|
|
163
|
+
const uiPromptFile =
|
|
164
|
+
typeof config?.uiPromptFile === 'string' && config.uiPromptFile.trim()
|
|
165
|
+
? config.uiPromptFile.trim()
|
|
166
|
+
: UI_PROMPTS_FILENAME;
|
|
167
|
+
const filePath = path.isAbsolute(uiPromptFile) ? uiPromptFile : stateDir ? path.join(stateDir, uiPromptFile) : '';
|
|
168
|
+
if (!filePath) return { found: false, text: '' };
|
|
169
|
+
|
|
170
|
+
const requestIds = [id, 'mcp-task:' + id];
|
|
171
|
+
const pollIntervalMs = config?.pollIntervalMs || DEFAULT_ASYNC_POLL_MS;
|
|
172
|
+
const timeoutMs = resolveAsyncTimeoutMs(options);
|
|
173
|
+
const deadline = Date.now() + timeoutMs;
|
|
174
|
+
|
|
175
|
+
while (Date.now() < deadline) {
|
|
176
|
+
const entries = readUiPromptsEntries(filePath);
|
|
177
|
+
const text = extractUiPromptResult(entries, requestIds);
|
|
178
|
+
if (text !== null) {
|
|
179
|
+
return { found: true, text };
|
|
180
|
+
}
|
|
181
|
+
await sleepMs(pollIntervalMs);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return { found: false, text: '' };
|
|
185
|
+
}
|
|
186
|
+
|
|
74
187
|
function ensureUiPromptsFile(filePath) {
|
|
75
188
|
const normalized = typeof filePath === 'string' ? filePath.trim() : '';
|
|
76
189
|
if (!normalized) return '';
|
|
@@ -1318,7 +1431,7 @@ const normalizeRequestId = (value) => (typeof value === 'string' ? value.trim()
|
|
|
1318
1431
|
const buildAsyncRequestIds = (taskId) => {
|
|
1319
1432
|
const id = normalizeRequestId(taskId);
|
|
1320
1433
|
if (!id) return [];
|
|
1321
|
-
return [id,
|
|
1434
|
+
return [id, 'mcp-task:' + id];
|
|
1322
1435
|
};
|
|
1323
1436
|
|
|
1324
1437
|
const extractAsyncResult = (list, requestIds) => {
|
|
@@ -1378,11 +1491,11 @@ const runMcpAsyncTest = async () => {
|
|
|
1378
1491
|
const taskId = 'task_' + uuid();
|
|
1379
1492
|
const callMeta = buildAsyncTaskCallMeta(taskId);
|
|
1380
1493
|
const ack = { status: 'accepted', taskId };
|
|
1381
|
-
const resultText =
|
|
1494
|
+
const resultText = 'AsyncTask result for ' + taskId;
|
|
1382
1495
|
|
|
1383
1496
|
appendMcpOutput('asyncTask.request', { message, callMeta });
|
|
1384
1497
|
appendMcpOutput('asyncTask.ack', ack);
|
|
1385
|
-
setMcpStatus(
|
|
1498
|
+
setMcpStatus('ACK: ' + taskId + ' (waiting for uiPrompts result)...');
|
|
1386
1499
|
|
|
1387
1500
|
const pollPromise = pollAsyncResult({ taskId, timeoutMs: 8000, intervalMs: 400 });
|
|
1388
1501
|
|
|
@@ -1391,7 +1504,7 @@ const runMcpAsyncTest = async () => {
|
|
|
1391
1504
|
try {
|
|
1392
1505
|
await host.uiPrompts.request({ requestId: taskId, prompt });
|
|
1393
1506
|
appendMcpOutput('asyncTask.result', { requestId: taskId, prompt });
|
|
1394
|
-
setMcpStatus(
|
|
1507
|
+
setMcpStatus('Result stored in uiPrompts (taskId=' + taskId + ')');
|
|
1395
1508
|
} catch (err) {
|
|
1396
1509
|
appendMcpOutput('asyncTask.error', err?.message || String(err));
|
|
1397
1510
|
setMcpStatus(err?.message || String(err), true);
|
|
@@ -1401,10 +1514,10 @@ const runMcpAsyncTest = async () => {
|
|
|
1401
1514
|
const pollResult = await pollPromise;
|
|
1402
1515
|
if (pollResult.found) {
|
|
1403
1516
|
appendMcpOutput('asyncTask.polled', pollResult.text);
|
|
1404
|
-
setMcpStatus(
|
|
1517
|
+
setMcpStatus('Polled result (taskId=' + taskId + ')');
|
|
1405
1518
|
} else {
|
|
1406
1519
|
appendMcpOutput('asyncTask.timeout', { taskId });
|
|
1407
|
-
setMcpStatus(
|
|
1520
|
+
setMcpStatus('Polling timeout (taskId=' + taskId + ')', true);
|
|
1408
1521
|
}
|
|
1409
1522
|
} catch (err) {
|
|
1410
1523
|
setMcpStatus(err?.message || String(err), true);
|
|
@@ -2444,6 +2557,7 @@ export async function startSandboxServer({ pluginDir, port = 4399, appId = '' })
|
|
|
2444
2557
|
const toolEntry = toolName ? toolMap.get(toolName) : null;
|
|
2445
2558
|
let args = {};
|
|
2446
2559
|
let resultText = '';
|
|
2560
|
+
let toolCallMeta = effectiveCallMeta;
|
|
2447
2561
|
if (!toolEntry) {
|
|
2448
2562
|
resultText = `[error] Tool not registered: ${toolName || 'unknown'}`;
|
|
2449
2563
|
} else {
|
|
@@ -2455,12 +2569,65 @@ export async function startSandboxServer({ pluginDir, port = 4399, appId = '' })
|
|
|
2455
2569
|
args = {};
|
|
2456
2570
|
}
|
|
2457
2571
|
if (!resultText) {
|
|
2572
|
+
const asyncTaskConfig = normalizeAsyncTaskConfig(effectiveCallMeta?.asyncTask, toolEntry.toolName);
|
|
2573
|
+
let taskId = '';
|
|
2574
|
+
if (asyncTaskConfig) {
|
|
2575
|
+
const taskIdKey = asyncTaskConfig.taskIdKey || '';
|
|
2576
|
+
const existingTaskId =
|
|
2577
|
+
taskIdKey && typeof effectiveCallMeta?.[taskIdKey] === 'string'
|
|
2578
|
+
? effectiveCallMeta[taskIdKey].trim()
|
|
2579
|
+
: '';
|
|
2580
|
+
taskId =
|
|
2581
|
+
existingTaskId ||
|
|
2582
|
+
generateTaskId({
|
|
2583
|
+
sessionId: normalizeText(effectiveCallMeta?.sessionId),
|
|
2584
|
+
serverName: toolEntry.serverName,
|
|
2585
|
+
toolName: toolEntry.toolName,
|
|
2586
|
+
});
|
|
2587
|
+
toolCallMeta = mergeCallMeta(effectiveCallMeta, {
|
|
2588
|
+
...(taskIdKey ? { [taskIdKey]: taskId } : null),
|
|
2589
|
+
stream: false,
|
|
2590
|
+
});
|
|
2591
|
+
}
|
|
2458
2592
|
const toolResult = await toolEntry.client.callTool({
|
|
2459
2593
|
name: toolEntry.toolName,
|
|
2460
2594
|
arguments: args,
|
|
2461
|
-
...(
|
|
2595
|
+
...(toolCallMeta ? { _meta: toolCallMeta } : {}),
|
|
2462
2596
|
});
|
|
2463
|
-
|
|
2597
|
+
if (asyncTaskConfig) {
|
|
2598
|
+
if (toolResult?.isError) {
|
|
2599
|
+
resultText = formatMcpToolResult(toolEntry.serverName, toolEntry.toolName, toolResult);
|
|
2600
|
+
} else if (asyncTaskConfig.resultSource && asyncTaskConfig.resultSource !== 'ui_prompts') {
|
|
2601
|
+
resultText =
|
|
2602
|
+
'[' +
|
|
2603
|
+
toolEntry.serverName +
|
|
2604
|
+
'/' +
|
|
2605
|
+
toolEntry.toolName +
|
|
2606
|
+
'] ❌ 不支持的异步结果源: ' +
|
|
2607
|
+
asyncTaskConfig.resultSource;
|
|
2608
|
+
} else {
|
|
2609
|
+
const asyncResult = await waitForUiPromptResult({
|
|
2610
|
+
taskId,
|
|
2611
|
+
config: asyncTaskConfig,
|
|
2612
|
+
callMeta: toolCallMeta,
|
|
2613
|
+
});
|
|
2614
|
+
if (asyncResult.found) {
|
|
2615
|
+
const text = typeof asyncResult.text === 'string' ? asyncResult.text.trim() : '';
|
|
2616
|
+
resultText = text || '(无结果内容)';
|
|
2617
|
+
} else {
|
|
2618
|
+
resultText =
|
|
2619
|
+
'[' +
|
|
2620
|
+
toolEntry.serverName +
|
|
2621
|
+
'/' +
|
|
2622
|
+
toolEntry.toolName +
|
|
2623
|
+
'] ❌ 等待交互待办结果超时 (taskId=' +
|
|
2624
|
+
(taskId || 'unknown') +
|
|
2625
|
+
')';
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
} else {
|
|
2629
|
+
resultText = formatMcpToolResult(toolEntry.serverName, toolEntry.toolName, toolResult);
|
|
2630
|
+
}
|
|
2464
2631
|
}
|
|
2465
2632
|
}
|
|
2466
2633
|
toolTrace.push({ tool: toolName || 'unknown', args, result: resultText });
|