@leeoohoo/ui-apps-devkit 0.1.13 → 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.13",
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 '';
@@ -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 });