@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leeoohoo/ui-apps-devkit",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "description": "ChatOS UI Apps DevKit (CLI + templates + sandbox) for building installable ChatOS UI Apps plugins.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -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, `mcp-task:${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 = `AsyncTask result for ${taskId}`;
1494
+ const resultText = 'AsyncTask result for ' + taskId;
1382
1495
 
1383
1496
  appendMcpOutput('asyncTask.request', { message, callMeta });
1384
1497
  appendMcpOutput('asyncTask.ack', ack);
1385
- setMcpStatus(`ACK: ${taskId} (waiting for uiPrompts result)...`);
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(`Result stored in uiPrompts (taskId=${taskId})`);
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(`Polled result (taskId=${taskId})`);
1517
+ setMcpStatus('Polled result (taskId=' + taskId + ')');
1405
1518
  } else {
1406
1519
  appendMcpOutput('asyncTask.timeout', { taskId });
1407
- setMcpStatus(`Polling timeout (taskId=${taskId})`, true);
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
- ...(effectiveCallMeta ? { _meta: effectiveCallMeta } : {}),
2595
+ ...(toolCallMeta ? { _meta: toolCallMeta } : {}),
2462
2596
  });
2463
- resultText = formatMcpToolResult(toolEntry.serverName, toolEntry.toolName, toolResult);
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 });