@moraya/core 0.2.0 → 0.4.0
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/README.md +215 -41
- package/dist/ai/drivers/claude.d.ts +6 -0
- package/dist/ai/drivers/claude.js +229 -0
- package/dist/ai/drivers/claude.js.map +1 -0
- package/dist/ai/drivers/gemini.d.ts +6 -0
- package/dist/ai/drivers/gemini.js +212 -0
- package/dist/ai/drivers/gemini.js.map +1 -0
- package/dist/ai/drivers/index.d.ts +14 -0
- package/dist/ai/drivers/index.js +617 -0
- package/dist/ai/drivers/index.js.map +1 -0
- package/dist/ai/drivers/ollama.d.ts +8 -0
- package/dist/ai/drivers/ollama.js +158 -0
- package/dist/ai/drivers/ollama.js.map +1 -0
- package/dist/ai/drivers/openai.d.ts +7 -0
- package/dist/ai/drivers/openai.js +225 -0
- package/dist/ai/drivers/openai.js.map +1 -0
- package/dist/ai/drivers/tool-bridge.d.ts +37 -0
- package/dist/ai/drivers/tool-bridge.js +138 -0
- package/dist/ai/drivers/tool-bridge.js.map +1 -0
- package/dist/ai/drivers/types.d.ts +2 -0
- package/dist/ai/drivers/types.js +1 -0
- package/dist/ai/drivers/types.js.map +1 -0
- package/dist/ai/drivers/util.d.ts +13 -0
- package/dist/ai/drivers/util.js +40 -0
- package/dist/ai/drivers/util.js.map +1 -0
- package/dist/ai/image.d.ts +37 -0
- package/dist/ai/image.js +36 -0
- package/dist/ai/image.js.map +1 -0
- package/dist/ai/index.d.ts +37 -0
- package/dist/ai/index.js +826 -0
- package/dist/ai/index.js.map +1 -0
- package/dist/ai/types.d.ts +92 -0
- package/dist/ai/types.js +1 -0
- package/dist/ai/types.js.map +1 -0
- package/dist/ai/voice.d.ts +42 -0
- package/dist/ai/voice.js +34 -0
- package/dist/ai/voice.js.map +1 -0
- package/dist/chat-markdown/index.d.ts +82 -0
- package/dist/chat-markdown/index.js +165 -0
- package/dist/chat-markdown/index.js.map +1 -0
- package/dist/i18n/locales/ar.json +806 -732
- package/dist/i18n/locales/de.json +912 -838
- package/dist/i18n/locales/en.json +34 -5
- package/dist/i18n/locales/es.json +952 -876
- package/dist/i18n/locales/fr.json +1784 -1708
- package/dist/i18n/locales/hi.json +1808 -1734
- package/dist/i18n/locales/ja.json +839 -765
- package/dist/i18n/locales/ko.json +1783 -1709
- package/dist/i18n/locales/pt.json +894 -820
- package/dist/i18n/locales/ru.json +812 -738
- package/dist/i18n/locales/zh-CN.json +34 -5
- package/dist/i18n/locales/zh-Hant.json +1039 -965
- package/dist/types-CwM77g7u.d.ts +88 -0
- package/package.json +26 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/ai/catalog.ts","../../src/ai/drivers/tool-bridge.ts","../../src/ai/drivers/util.ts","../../src/ai/drivers/claude.ts","../../src/ai/drivers/openai.ts","../../src/ai/drivers/gemini.ts","../../src/ai/drivers/ollama.ts","../../src/ai/drivers/index.ts","../../src/ai/chat.ts","../../src/ai/image.ts","../../src/ai/voice.ts"],"sourcesContent":["/**\n * Provider catalog — default models, base URLs, labels, id aliases.\n * Baselined on the desktop app. Pure data; no host imports.\n */\nimport type { AIProvider } from './types'\n\nexport const DEFAULT_MODELS: Record<AIProvider, string[]> = {\n claude: ['claude-opus-4-6', 'claude-sonnet-4-6', 'claude-haiku-4-5-20251001'],\n openai: ['gpt-5.2', 'gpt-5.2-pro', 'gpt-5', 'gpt-5-mini', 'o4-mini', 'gpt-4o', 'gpt-4o-mini', 'o3', 'o3-mini'],\n gemini: ['gemini-3.1-pro-preview', 'gemini-3-flash-preview', 'gemini-2.5-flash', 'gemini-2.5-flash-lite', 'gemini-2.0-pro-exp'],\n deepseek: ['deepseek-chat', 'deepseek-reasoner'],\n ollama: ['llama3.3', 'llama3.2', 'qwen2.5', 'qwen2.5-coder', 'phi4', 'gemma3', 'deepseek-r1', 'mistral', 'codellama'],\n grok: ['grok-4', 'grok-4-1-fast-reasoning', 'grok-4-1-fast-non-reasoning', 'grok-code-fast-1', 'grok-3'],\n mistral: ['mistral-large-latest', 'mistral-small-latest', 'magistral-medium-latest', 'magistral-small-latest', 'codestral-latest', 'devstral-latest'],\n glm: ['glm-5', 'glm-4-plus', 'glm-4-air', 'glm-4-flash', 'glm-z1-flash', 'glm-z1-air'],\n minimax: ['MiniMax-M2.5', 'MiniMax-M2.5-highspeed', 'MiniMax-Text-01'],\n doubao: [],\n custom: [],\n 'local-mlx': ['Qwen2.5-1.5B-Instruct-4bit'],\n 'local-llama': ['qwen2.5-1.5b-instruct-q4'],\n}\n\nexport const PROVIDER_BASE_URLS: Record<AIProvider, string> = {\n claude: 'https://api.anthropic.com',\n openai: 'https://api.openai.com',\n gemini: 'https://generativelanguage.googleapis.com',\n deepseek: 'https://api.deepseek.com',\n ollama: 'http://localhost:11434',\n grok: 'https://api.x.ai',\n mistral: 'https://api.mistral.ai',\n glm: 'https://open.bigmodel.cn/api/paas/v4',\n minimax: 'https://api.minimax.io/v1',\n doubao: 'https://ark.cn-beijing.volces.com/api/v3',\n custom: '',\n 'local-mlx': '',\n 'local-llama': '',\n}\n\nexport const PROVIDER_LABELS: Record<AIProvider, string> = {\n claude: 'Claude',\n openai: 'OpenAI',\n gemini: 'Gemini',\n deepseek: 'DeepSeek',\n ollama: 'Ollama',\n grok: 'Grok',\n mistral: 'Mistral',\n glm: 'GLM',\n minimax: 'MiniMax',\n doubao: 'Doubao',\n custom: 'Custom (OpenAI-compatible)',\n 'local-mlx': 'On-device (iOS)',\n 'local-llama': 'On-device (Android)',\n}\n\n/** Legacy/foreign provider ids → canonical `AIProvider`. */\nexport const PROVIDER_ALIASES: Record<string, AIProvider> = {\n anthropic: 'claude',\n}\n\n/** Normalize an incoming provider id (applies aliases). */\nexport function normalizeProvider(id: string): AIProvider {\n return (PROVIDER_ALIASES[id] ?? id) as AIProvider\n}\n","/**\n * Tool formatting + response parsing, shared by all consumers.\n * Ported verbatim from the desktop app's tool-bridge (MCP-specific glue like\n * `mcpToolsToToolDefs` stays in the desktop repo — it has no place in core).\n */\nimport type { AIProvider, ToolDefinition, ToolCallRequest } from '../types'\n\nconst GEMINI_UNSUPPORTED_KEYS = new Set([\n 'additionalProperties', '$schema', '$id', '$ref', '$defs', 'definitions',\n 'patternProperties', 'unevaluatedProperties', 'dependentRequired',\n 'dependentSchemas', 'const',\n])\n\nfunction sanitizeGeminiSchema(schema: unknown): unknown {\n if (Array.isArray(schema)) return schema.map(sanitizeGeminiSchema)\n if (schema && typeof schema === 'object') {\n const out: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(schema as Record<string, unknown>)) {\n if (GEMINI_UNSUPPORTED_KEYS.has(key)) continue\n out[key] = sanitizeGeminiSchema(value)\n }\n return out\n }\n return schema\n}\n\n/** Format tools into provider-specific request-body fields (merge into body). */\nexport function formatToolsForProvider(\n provider: AIProvider,\n tools: ToolDefinition[],\n): Record<string, unknown> {\n if (tools.length === 0) return {}\n switch (provider) {\n case 'claude':\n return {\n tools: tools.map(t => ({ name: t.name, description: t.description, input_schema: t.input_schema })),\n }\n case 'gemini':\n return {\n tools: [{\n functionDeclarations: tools.map(t => ({\n name: t.name,\n description: t.description,\n parameters: sanitizeGeminiSchema(t.input_schema),\n })),\n }],\n }\n default:\n // OpenAI-compatible (openai/deepseek/grok/mistral/glm/minimax/doubao/custom/ollama)\n return {\n tools: tools.map(t => ({\n type: 'function',\n function: { name: t.name, description: t.description, parameters: t.input_schema },\n })),\n }\n }\n}\n\nexport function parseClaudeToolCalls(data: Record<string, unknown>): {\n toolCalls: ToolCallRequest[]; textContent: string; stopReason: string\n} {\n const content = data.content as Array<Record<string, unknown>> | undefined\n const stopReason = (data.stop_reason as string) || 'end_turn'\n const toolCalls: ToolCallRequest[] = []\n let textContent = ''\n if (content) {\n for (const block of content) {\n if (block.type === 'tool_use') {\n toolCalls.push({ id: block.id as string, name: block.name as string, arguments: (block.input as Record<string, unknown>) || {} })\n } else if (block.type === 'text') {\n textContent += block.text as string\n }\n }\n }\n return { toolCalls, textContent, stopReason }\n}\n\nexport function parseOpenAIToolCalls(data: Record<string, unknown>): {\n toolCalls: ToolCallRequest[]; textContent: string; stopReason: string\n} {\n const choices = data.choices as Array<Record<string, unknown>> | undefined\n if (!choices || choices.length === 0) return { toolCalls: [], textContent: '', stopReason: 'stop' }\n const choice = choices[0]!\n const message = choice.message as Record<string, unknown> | undefined\n const finishReason = (choice.finish_reason as string) || 'stop'\n const textContent = (message?.content as string) || ''\n const toolCalls: ToolCallRequest[] = []\n const rawToolCalls = message?.tool_calls as Array<Record<string, unknown>> | undefined\n if (rawToolCalls) {\n for (const tc of rawToolCalls) {\n const fn = tc.function as Record<string, unknown>\n let args: Record<string, unknown> = {}\n try { args = JSON.parse(fn.arguments as string) } catch { /* truncated */ }\n toolCalls.push({ id: tc.id as string, name: fn.name as string, arguments: args })\n }\n }\n return { toolCalls, textContent, stopReason: finishReason === 'tool_calls' ? 'tool_use' : finishReason }\n}\n\nexport function parseGeminiToolCalls(data: Record<string, unknown>): {\n toolCalls: ToolCallRequest[]; textContent: string; stopReason: string\n} {\n const candidates = data.candidates as Array<Record<string, unknown>> | undefined\n if (!candidates || candidates.length === 0) return { toolCalls: [], textContent: '', stopReason: 'stop' }\n const content = candidates[0]!.content as Record<string, unknown> | undefined\n const parts = content?.parts as Array<Record<string, unknown>> | undefined\n const toolCalls: ToolCallRequest[] = []\n let textContent = ''\n if (parts) {\n for (const part of parts) {\n if (part.functionCall) {\n const fc = part.functionCall as Record<string, unknown>\n const thoughtSignature =\n (part.thoughtSignature as string | undefined) ?? (fc.thoughtSignature as string | undefined)\n toolCalls.push({\n id: `gemini-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,\n name: fc.name as string,\n arguments: (fc.args as Record<string, unknown>) || {},\n ...(thoughtSignature ? { providerMeta: { thoughtSignature } } : {}),\n })\n } else if (part.text) {\n textContent += part.text as string\n }\n }\n }\n return { toolCalls, textContent, stopReason: toolCalls.length > 0 ? 'tool_use' : 'stop' }\n}\n\nexport function buildClaudeToolResultMessages(\n toolResults: Array<{ callId: string; content: string; isError?: boolean }>,\n): Record<string, unknown> {\n return {\n role: 'user',\n content: toolResults.map(r => ({\n type: 'tool_result', tool_use_id: r.callId, content: r.content, is_error: r.isError || false,\n })),\n }\n}\n\nexport function buildOpenAIToolResultMessages(\n toolResults: Array<{ callId: string; name: string; content: string }>,\n): Array<Record<string, unknown>> {\n return toolResults.map(r => ({ role: 'tool', tool_call_id: r.callId, content: r.content }))\n}\n","import { PROVIDER_BASE_URLS } from '../catalog'\nimport type { AIProviderConfig } from '../types'\n\nexport function resolveBaseUrl(config: AIProviderConfig, fallback: string): string {\n return config.baseUrl || PROVIDER_BASE_URLS[config.provider] || fallback\n}\n\n/** Build an OpenAI-compatible endpoint, avoiding a double version prefix. */\nexport function openaiEndpoint(baseUrl: string, path: string): string {\n const clean = baseUrl.replace(/\\/+$/, '')\n if (/\\/v\\d+$/.test(clean)) return `${clean}${path}`\n return `${clean}/v1${path}`\n}\n\nexport const NOOP_FOLD = {\n pushEnvelope() { return undefined },\n finish() { return { stopReason: 'end_turn' } },\n}\n","import type { AIProviderConfig, AIRequest, AIResponse, ChatMessage, ToolCallRequest } from '../types'\nimport type { TransportRequest } from '../transport'\nimport type { AIDriver, StreamFold } from './types'\nimport { formatToolsForProvider, parseClaudeToolCalls } from './tool-bridge'\nimport { resolveBaseUrl } from './util'\n\nfunction buildClaudeMessages(messages: ChatMessage[]): Array<Record<string, unknown>> {\n const result: Array<Record<string, unknown>> = []\n for (const msg of messages) {\n if (msg.role === 'assistant' && msg.toolCalls && msg.toolCalls.length > 0) {\n const content: Array<Record<string, unknown>> = []\n if (msg.content) content.push({ type: 'text', text: msg.content })\n for (const tc of msg.toolCalls) content.push({ type: 'tool_use', id: tc.id, name: tc.name, input: tc.arguments })\n result.push({ role: 'assistant', content })\n } else if (msg.role === 'tool') {\n const lastMsg = result[result.length - 1]\n const toolResultBlock = { type: 'tool_result', tool_use_id: msg.toolCallId, content: msg.content, is_error: msg.isError || false }\n if (lastMsg && lastMsg.role === 'user' && Array.isArray(lastMsg.content) &&\n (lastMsg.content as Array<Record<string, unknown>>).every(b => b.type === 'tool_result')) {\n (lastMsg.content as Array<Record<string, unknown>>).push(toolResultBlock)\n } else {\n result.push({ role: 'user', content: [toolResultBlock] })\n }\n } else if (msg.role === 'user' && msg.images && msg.images.length > 0) {\n const content: Array<Record<string, unknown>> = []\n for (const img of msg.images) content.push({ type: 'image', source: { type: 'base64', media_type: img.mimeType, data: img.base64 } })\n if (msg.content) content.push({ type: 'text', text: msg.content })\n result.push({ role: 'user', content })\n } else {\n result.push({ role: msg.role, content: msg.content })\n }\n }\n return result\n}\n\nexport const claudeDriver: AIDriver = {\n supportsStreaming: true,\n\n buildChatRequest(config, request, stream): TransportRequest {\n const baseUrl = resolveBaseUrl(config, 'https://api.anthropic.com')\n const systemMessages = request.messages.filter(m => m.role === 'system')\n const chatMessages = request.messages.filter(m => m.role !== 'system')\n\n const body: Record<string, unknown> = {\n model: request.model || config.model,\n max_tokens: request.maxTokens ?? config.maxTokens ?? 41920,\n messages: buildClaudeMessages(chatMessages),\n }\n if (stream) body.stream = true\n if (systemMessages.length > 0) body.system = systemMessages.map(m => m.content).join('\\n')\n const temperature = request.temperature ?? config.temperature\n if (temperature !== undefined) body.temperature = temperature\n const topP = request.topP ?? config.topP\n if (topP !== undefined) body.top_p = topP\n // Anthropic caps stop_sequences (~5) — trim quietly.\n if (request.stop && request.stop.length > 0) body.stop_sequences = request.stop.slice(0, 5)\n if (request.tools && request.tools.length > 0) Object.assign(body, formatToolsForProvider('claude', request.tools))\n\n return {\n provider: 'claude',\n configId: config.id,\n method: 'POST',\n url: `${baseUrl}/v1/messages`,\n headers: {\n 'anthropic-version': '2023-06-01',\n // Required for direct browser fetch; harmless when proxied via Rust.\n 'anthropic-dangerous-direct-browser-access': 'true',\n },\n body: JSON.stringify(body),\n auth: { scheme: 'header', headerName: 'x-api-key' },\n }\n },\n\n parseResponse(json, _config): AIResponse {\n const parsed = parseClaudeToolCalls(json)\n const usage = json.usage as Record<string, number> | undefined\n return {\n content: parsed.textContent,\n model: (json.model as string) || _config.model,\n usage: { inputTokens: usage?.input_tokens || 0, outputTokens: usage?.output_tokens || 0 },\n ...(parsed.toolCalls.length > 0 ? { toolCalls: parsed.toolCalls } : {}),\n stopReason: parsed.stopReason,\n }\n },\n\n createStreamFold(): StreamFold {\n const partials = new Map<number, { id: string; name: string; json: string }>()\n const toolCalls: ToolCallRequest[] = []\n let stopReason = 'end_turn'\n let inputTokens = 0\n let outputTokens = 0\n return {\n pushEnvelope(raw) {\n let v: Record<string, unknown>\n try { v = JSON.parse(raw) } catch { return undefined }\n switch (v.type as string) {\n case 'message_start': {\n const u = (v.message as Record<string, unknown> | undefined)?.usage as Record<string, number> | undefined\n if (u?.input_tokens) inputTokens = u.input_tokens\n break\n }\n case 'content_block_delta': {\n const delta = v.delta as Record<string, unknown> | undefined\n if (delta?.type === 'text_delta') return (delta.text as string) || undefined\n if (delta?.type === 'input_json_delta') {\n const p = partials.get(v.index as number)\n if (p) p.json += (delta.partial_json as string) || ''\n }\n break\n }\n case 'content_block_start': {\n const block = v.content_block as Record<string, unknown> | undefined\n if (block?.type === 'tool_use') partials.set(v.index as number, { id: block.id as string, name: block.name as string, json: '' })\n break\n }\n case 'content_block_stop': {\n const p = partials.get(v.index as number)\n if (p) {\n try { toolCalls.push({ id: p.id, name: p.name, arguments: JSON.parse(p.json || '{}') }) } catch { /* truncated */ }\n partials.delete(v.index as number)\n }\n break\n }\n case 'message_delta': {\n const d = v.delta as Record<string, unknown> | undefined\n if (d?.stop_reason) stopReason = d.stop_reason === 'tool_use' ? 'tool_use' : (d.stop_reason as string)\n const u = v.usage as Record<string, number> | undefined\n if (u?.output_tokens) outputTokens = u.output_tokens\n break\n }\n }\n return undefined\n },\n finish() {\n const usage = (inputTokens || outputTokens) ? { inputTokens, outputTokens } : undefined\n return { ...(toolCalls.length > 0 ? { toolCalls } : {}), stopReason, ...(usage ? { usage } : {}) }\n },\n }\n },\n}\n","import type { AIProviderConfig, AIRequest, AIResponse, ChatMessage, ToolCallRequest } from '../types'\nimport type { TransportRequest } from '../transport'\nimport type { AIDriver, StreamFold } from './types'\nimport { formatToolsForProvider, parseOpenAIToolCalls } from './tool-bridge'\nimport { resolveBaseUrl, openaiEndpoint } from './util'\n\nfunction buildOpenAIMessages(messages: ChatMessage[]): Array<Record<string, unknown>> {\n return messages.map(msg => {\n if (msg.role === 'assistant' && msg.toolCalls && msg.toolCalls.length > 0) {\n return {\n role: 'assistant',\n content: msg.content || null,\n tool_calls: msg.toolCalls.map(tc => ({\n id: tc.id, type: 'function', function: { name: tc.name, arguments: JSON.stringify(tc.arguments) },\n })),\n }\n } else if (msg.role === 'tool') {\n return { role: 'tool', tool_call_id: msg.toolCallId, content: msg.content }\n } else if (msg.role === 'user' && msg.images && msg.images.length > 0) {\n const content: Array<Record<string, unknown>> = []\n for (const img of msg.images) content.push({ type: 'image_url', image_url: { url: `data:${img.mimeType};base64,${img.base64}` } })\n if (msg.content) content.push({ type: 'text', text: msg.content })\n return { role: 'user', content }\n }\n return { role: msg.role, content: msg.content }\n })\n}\n\n/** OpenAI-compatible driver: openai/deepseek/grok/mistral/glm/minimax/doubao/custom. */\nexport const openaiDriver: AIDriver = {\n supportsStreaming: true,\n\n buildChatRequest(config, request, stream): TransportRequest {\n const baseUrl = resolveBaseUrl(config, 'https://api.openai.com')\n const body: Record<string, unknown> = {\n model: request.model || config.model,\n max_tokens: request.maxTokens ?? config.maxTokens ?? 41920,\n temperature: request.temperature ?? config.temperature ?? 0.7,\n messages: buildOpenAIMessages(request.messages),\n }\n if (stream) {\n body.stream = true\n // Ask for token usage in the final SSE chunk. Limited to OpenAI/DeepSeek —\n // some other OpenAI-compatible endpoints reject unknown body fields.\n if (config.provider === 'openai' || config.provider === 'deepseek') {\n body.stream_options = { include_usage: true }\n }\n }\n const topP = request.topP ?? config.topP\n if (topP !== undefined) body.top_p = topP\n // OpenAI rejects >4 stop sequences (400) — trim quietly.\n if (request.stop && request.stop.length > 0) body.stop = request.stop.slice(0, 4)\n if (request.tools && request.tools.length > 0) Object.assign(body, formatToolsForProvider(config.provider, request.tools))\n\n // Proxy-provider key: deepseek keeps its own (for Rust auth), everything\n // else OpenAI-compatible maps to 'openai'.\n const proxyProvider = config.provider === 'deepseek' ? 'deepseek' : 'openai'\n return {\n provider: proxyProvider,\n configId: config.id,\n method: 'POST',\n url: openaiEndpoint(baseUrl, '/chat/completions'),\n headers: {},\n body: JSON.stringify(body),\n auth: { scheme: 'bearer' },\n }\n },\n\n parseResponse(json, _config): AIResponse {\n const parsed = parseOpenAIToolCalls(json)\n const usage = json.usage as Record<string, number> | undefined\n return {\n content: parsed.textContent,\n model: (json.model as string) || _config.model,\n usage: { inputTokens: usage?.prompt_tokens || 0, outputTokens: usage?.completion_tokens || 0 },\n ...(parsed.toolCalls.length > 0 ? { toolCalls: parsed.toolCalls } : {}),\n stopReason: parsed.stopReason,\n }\n },\n\n createStreamFold(): StreamFold {\n const toolMap = new Map<number, { id: string; name: string; args: string }>()\n let stopReason = 'end_turn'\n let usage: { inputTokens: number; outputTokens: number } | undefined\n return {\n pushEnvelope(raw) {\n let v: Record<string, unknown>\n try { v = JSON.parse(raw) } catch { return undefined }\n const u = v.usage as Record<string, number> | undefined\n if (u) usage = { inputTokens: u.prompt_tokens || 0, outputTokens: u.completion_tokens || 0 }\n const choices = v.choices as Array<Record<string, unknown>> | undefined\n if (!choices || choices.length === 0) return undefined\n const choice = choices[0]!\n const fr = choice.finish_reason as string | null\n if (fr) stopReason = fr === 'tool_calls' ? 'tool_use' : fr === 'length' ? 'max_tokens' : fr\n const delta = choice.delta as Record<string, unknown> | undefined\n const rawTC = delta?.tool_calls as Array<Record<string, unknown>> | undefined\n if (rawTC) {\n for (const tc of rawTC) {\n const idx = (tc.index as number) ?? 0\n const fn = tc.function as Record<string, unknown> | undefined\n let entry = toolMap.get(idx)\n if (!entry) { entry = { id: (tc.id as string) || '', name: '', args: '' }; toolMap.set(idx, entry) }\n if (tc.id) entry.id = tc.id as string\n if (fn?.name) entry.name = fn.name as string\n if (fn?.arguments) entry.args += fn.arguments as string\n }\n }\n return (delta?.content as string) || undefined\n },\n finish() {\n const toolCalls: ToolCallRequest[] = []\n for (const [, entry] of [...toolMap.entries()].sort((a, b) => a[0] - b[0])) {\n let args: Record<string, unknown> = {}\n try { args = JSON.parse(entry.args || '{}') } catch { continue }\n toolCalls.push({ id: entry.id, name: entry.name, arguments: args })\n }\n return { ...(toolCalls.length > 0 ? { toolCalls } : {}), stopReason, ...(usage ? { usage } : {}) }\n },\n }\n },\n}\n","import type { AIRequest, AIResponse, ChatMessage, ToolCallRequest } from '../types'\nimport type { TransportRequest } from '../transport'\nimport type { AIDriver, StreamFold } from './types'\nimport { formatToolsForProvider, parseGeminiToolCalls } from './tool-bridge'\nimport { resolveBaseUrl } from './util'\n\nfunction buildGeminiContents(messages: ChatMessage[]): Array<Record<string, unknown>> {\n return messages.map(msg => {\n if (msg.role === 'assistant' && msg.toolCalls && msg.toolCalls.length > 0) {\n const parts: Array<Record<string, unknown>> = []\n if (msg.content) parts.push({ text: msg.content })\n for (const tc of msg.toolCalls) {\n const sig = tc.providerMeta?.thoughtSignature as string | undefined\n const part: Record<string, unknown> = { functionCall: { name: tc.name, args: tc.arguments } }\n if (sig) part.thoughtSignature = sig\n parts.push(part)\n }\n return { role: 'model', parts }\n } else if (msg.role === 'tool') {\n return { role: 'user', parts: [{ functionResponse: { name: msg.toolName, response: { content: msg.content } } }] }\n }\n if (msg.role === 'user' && msg.images && msg.images.length > 0) {\n const parts: Array<Record<string, unknown>> = []\n for (const img of msg.images) parts.push({ inlineData: { mimeType: img.mimeType, data: img.base64 } })\n if (msg.content) parts.push({ text: msg.content })\n return { role: 'user', parts }\n }\n return { role: msg.role === 'assistant' ? 'model' : 'user', parts: [{ text: msg.content }] }\n })\n}\n\nexport const geminiDriver: AIDriver = {\n supportsStreaming: true,\n\n buildChatRequest(config, request, stream): TransportRequest {\n const baseUrl = resolveBaseUrl(config, 'https://generativelanguage.googleapis.com')\n const systemMessages = request.messages.filter(m => m.role === 'system')\n const chatMessages = request.messages.filter(m => m.role !== 'system')\n\n const generationConfig: Record<string, unknown> = {\n maxOutputTokens: request.maxTokens ?? config.maxTokens ?? 41920,\n temperature: request.temperature ?? config.temperature ?? 0.7,\n }\n const topP = request.topP ?? config.topP\n if (topP !== undefined) generationConfig.topP = topP\n if (request.stop && request.stop.length > 0) generationConfig.stopSequences = request.stop\n\n const body: Record<string, unknown> = { contents: buildGeminiContents(chatMessages), generationConfig }\n if (systemMessages.length > 0) body.systemInstruction = { parts: [{ text: systemMessages.map(m => m.content).join('\\n') }] }\n if (request.tools && request.tools.length > 0) Object.assign(body, formatToolsForProvider('gemini', request.tools))\n\n const model = request.model || config.model\n const verb = stream ? 'streamGenerateContent?alt=sse' : 'generateContent'\n return {\n provider: 'gemini',\n configId: config.id,\n method: 'POST',\n url: `${baseUrl}/v1beta/models/${model}:${verb}`,\n headers: {},\n body: JSON.stringify(body),\n auth: { scheme: 'query', queryParam: 'key' },\n }\n },\n\n parseResponse(json, config): AIResponse {\n const parsed = parseGeminiToolCalls(json)\n const usage = json.usageMetadata as Record<string, number> | undefined\n return {\n content: parsed.textContent,\n model: config.model,\n usage: { inputTokens: usage?.promptTokenCount || 0, outputTokens: usage?.candidatesTokenCount || 0 },\n ...(parsed.toolCalls.length > 0 ? { toolCalls: parsed.toolCalls } : {}),\n stopReason: parsed.stopReason === 'tool_use' ? 'tool_use' : 'end_turn',\n }\n },\n\n createStreamFold(): StreamFold {\n const toolCalls: ToolCallRequest[] = []\n let stopReason = 'end_turn'\n let usage: { inputTokens: number; outputTokens: number } | undefined\n return {\n pushEnvelope(raw) {\n let v: Record<string, unknown>\n try { v = JSON.parse(raw) } catch { return undefined }\n const um = v.usageMetadata as Record<string, number> | undefined\n if (um) usage = { inputTokens: um.promptTokenCount || 0, outputTokens: um.candidatesTokenCount || 0 }\n const candidates = v.candidates as Array<Record<string, unknown>> | undefined\n const cand = candidates?.[0]\n if (!cand) return undefined\n const parts = (cand.content as Record<string, unknown> | undefined)?.parts as Array<Record<string, unknown>> | undefined\n let text = ''\n if (parts) {\n for (const part of parts) {\n if (part.functionCall) {\n const fc = part.functionCall as Record<string, unknown>\n const sig = (part.thoughtSignature as string | undefined) ?? (fc.thoughtSignature as string | undefined)\n toolCalls.push({\n id: `gemini-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,\n name: fc.name as string,\n arguments: (fc.args as Record<string, unknown>) || {},\n ...(sig ? { providerMeta: { thoughtSignature: sig } } : {}),\n })\n stopReason = 'tool_use'\n } else if (part.text) {\n text += part.text as string\n }\n }\n }\n return text || undefined\n },\n finish() { return { ...(toolCalls.length > 0 ? { toolCalls } : {}), stopReason, ...(usage ? { usage } : {}) } },\n }\n },\n}\n","import type { AIResponse, ChatMessage, ToolCallRequest } from '../types'\nimport type { TransportRequest } from '../transport'\nimport type { AIDriver } from './types'\nimport { formatToolsForProvider } from './tool-bridge'\nimport { resolveBaseUrl, NOOP_FOLD } from './util'\n\nfunction buildOllamaMessages(messages: ChatMessage[]): Array<Record<string, unknown>> {\n return messages.map(msg => {\n if (msg.role === 'assistant' && msg.toolCalls && msg.toolCalls.length > 0) {\n return {\n role: 'assistant',\n content: msg.content || '',\n tool_calls: msg.toolCalls.map(tc => ({\n id: tc.id, type: 'function', function: { name: tc.name, arguments: JSON.stringify(tc.arguments) },\n })),\n }\n } else if (msg.role === 'tool') {\n return { role: 'tool', tool_call_id: msg.toolCallId, content: msg.content }\n }\n const result: Record<string, unknown> = { role: msg.role, content: msg.content }\n if (msg.role === 'user' && msg.images && msg.images.length > 0) result.images = msg.images.map(img => img.base64)\n return result\n })\n}\n\n/** Ollama uses its native /api/chat. PC always calls it non-streaming, so the\n * orchestrator one-shots it (supportsStreaming=false). */\nexport const ollamaDriver: AIDriver = {\n supportsStreaming: false,\n\n buildChatRequest(config, request, _stream): TransportRequest {\n const baseUrl = resolveBaseUrl(config, 'http://localhost:11434')\n const body: Record<string, unknown> = {\n model: request.model || config.model,\n messages: buildOllamaMessages(request.messages),\n stream: false,\n options: {\n temperature: request.temperature ?? config.temperature ?? 0.7,\n num_predict: request.maxTokens ?? config.maxTokens ?? 41920,\n },\n }\n if (request.tools && request.tools.length > 0) Object.assign(body, formatToolsForProvider('ollama', request.tools))\n return {\n provider: 'ollama',\n configId: config.id,\n method: 'POST',\n url: `${baseUrl}/api/chat`,\n headers: {},\n body: JSON.stringify(body),\n auth: { scheme: 'none' },\n }\n },\n\n parseResponse(json, _config): AIResponse {\n const message = json.message as Record<string, unknown> | undefined\n const toolCalls: ToolCallRequest[] = []\n const rawToolCalls = message?.tool_calls as Array<Record<string, unknown>> | undefined\n if (rawToolCalls) {\n for (const tc of rawToolCalls) {\n const fn = tc.function as Record<string, unknown>\n toolCalls.push({\n id: `ollama-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,\n name: fn.name as string,\n arguments: (fn.arguments as Record<string, unknown>) || {},\n })\n }\n }\n return {\n content: (message?.content as string) || '',\n model: (json.model as string) || _config.model,\n usage: { inputTokens: (json.prompt_eval_count as number) || 0, outputTokens: (json.eval_count as number) || 0 },\n ...(toolCalls.length > 0 ? { toolCalls } : {}),\n stopReason: toolCalls.length > 0 ? 'tool_use' : 'end_turn',\n }\n },\n\n createStreamFold() { return NOOP_FOLD },\n}\n","import type { AIProvider } from '../types'\nimport type { AIDriver } from './types'\nimport { claudeDriver } from './claude'\nimport { openaiDriver } from './openai'\nimport { geminiDriver } from './gemini'\nimport { ollamaDriver } from './ollama'\n\n/** Resolve the driver for an HTTP provider. On-device providers (local-mlx /\n * local-llama) are NOT HTTP — consumers handle those locally and never call\n * the orchestrator for them, so this throws to catch misuse. */\nexport function getDriver(provider: AIProvider): AIDriver {\n switch (provider) {\n case 'claude':\n return claudeDriver\n case 'gemini':\n return geminiDriver\n case 'ollama':\n return ollamaDriver\n case 'openai':\n case 'deepseek':\n case 'grok':\n case 'mistral':\n case 'glm':\n case 'minimax':\n case 'doubao':\n case 'custom':\n return openaiDriver\n default:\n throw new Error(`No HTTP driver for provider: ${provider}`)\n }\n}\n\nexport { claudeDriver, openaiDriver, geminiDriver, ollamaDriver }\nexport type { AIDriver, StreamFold } from './types'\n","/**\n * Chat orchestrator — composes a per-provider driver with a platform transport.\n * Platform-agnostic: no fetch, no Tauri. Streaming uses a small callback→async\n * generator pump (ported from the desktop streamViaProxy backpressure logic).\n */\nimport type { AIProviderConfig, AIRequest, AIResponse, AIStreamEvent } from './types'\nimport type { AITransport } from './transport'\nimport { getDriver } from './drivers'\nimport type { AIDriver } from './drivers/types'\n\n/** Attach the config's key reference to a built request. Drivers stay key-free;\n * the transport decides what to do with it (PC: Keychain override guard;\n * web: apply per the auth descriptor). On PC `apiKey` is the `'***'` sentinel\n * (the real key lives in the OS Keychain), so the real key never enters core. */\nfunction withKey(treq: ReturnType<AIDriver['buildChatRequest']>, config: AIProviderConfig) {\n if (config.apiKey !== undefined) treq.apiKey = config.apiKey\n return treq\n}\n\nexport async function sendChat(\n config: AIProviderConfig,\n request: AIRequest,\n transport: AITransport,\n signal?: AbortSignal,\n): Promise<AIResponse> {\n const driver = getDriver(config.provider)\n const treq = withKey(driver.buildChatRequest(config, request, false), config)\n const res = await transport.fetch(treq, signal)\n if (res.status >= 400) {\n throw new Error(`AI request failed (${res.status}): ${res.body.slice(0, 300)}`)\n }\n let json: Record<string, unknown>\n try { json = JSON.parse(res.body) } catch { throw new Error('AI returned non-JSON response') }\n return driver.parseResponse(json, config)\n}\n\nexport async function* streamChat(\n config: AIProviderConfig,\n request: AIRequest,\n transport: AITransport,\n signal?: AbortSignal,\n): AsyncGenerator<AIStreamEvent> {\n const driver = getDriver(config.provider)\n const canStream = driver.supportsStreaming && (transport.canStream?.(config.provider) ?? true)\n\n // Providers the transport can't stream (e.g. gemini/ollama on the Rust proxy)\n // → one-shot, then surface as a single delta + terminal event. Preserves PC.\n if (!canStream) {\n const resp = await sendChat(config, request, transport, signal)\n if (resp.content) yield { delta: resp.content }\n yield {\n done: true,\n ...(resp.toolCalls ? { toolCalls: resp.toolCalls } : {}),\n ...(resp.usage ? { usage: resp.usage } : {}),\n ...(resp.stopReason ? { stopReason: resp.stopReason } : {}),\n }\n return\n }\n\n const treq = withKey(driver.buildChatRequest(config, request, true), config)\n const fold = driver.createStreamFold()\n\n const queue: string[] = []\n let ended = false\n let err: unknown = null\n let notify: (() => void) | null = null\n const wake = () => { notify?.(); notify = null }\n\n const pump = transport.stream(treq, {\n ...(signal ? { signal } : {}),\n onText: (delta) => { if (delta) { queue.push(delta); wake() } },\n onEnvelope: (rawJson) => { const d = fold.pushEnvelope(rawJson); if (d) { queue.push(d); wake() } },\n }).then(() => { ended = true; wake() }).catch((e) => { err = e; ended = true; wake() })\n\n let yieldCount = 0\n while (!ended || queue.length > 0) {\n if (signal?.aborted) throw new DOMException('Aborted', 'AbortError')\n if (queue.length > 0) {\n yield { delta: queue.shift()! }\n // Periodically cede the event loop so the UI stays responsive.\n if (++yieldCount % 15 === 0) await new Promise<void>(r => setTimeout(r, 0))\n } else if (!ended) {\n await new Promise<void>(resolve => { notify = resolve })\n }\n }\n if (err) throw err instanceof Error ? err : new Error(String(err))\n await pump\n const fin = fold.finish()\n yield {\n done: true,\n ...(fin.toolCalls ? { toolCalls: fin.toolCalls } : {}),\n ...(fin.usage ? { usage: fin.usage } : {}),\n stopReason: fin.stopReason,\n }\n}\n","/**\n * Shared image generation — the OpenAI-compatible `/images/generations` path\n * used by every Moraya image provider that speaks that protocol (web's\n * openai-image + doubao-image; desktop's openai/grok/doubao/custom). The\n * request body + response extraction live here once; each app keeps its own\n * transport (fetch vs Tauri proxy), key handling, guards, and error mapping.\n *\n * Desktop's gemini-predict + qwen/DashScope two-phase providers are NOT here —\n * they stay in the desktop app (different protocols, no web counterpart).\n */\nimport { openaiEndpoint } from './drivers/util'\n\nexport interface GeneratedImage {\n /** Public URL (caller may re-host). */\n url?: string\n /** Base64 image bytes (no `data:` prefix). */\n b64Json?: string\n /** Provider's safety/clarity-revised prompt, if any. */\n revisedPrompt?: string\n}\n\nexport interface ImageGenRequest {\n prompt: string\n n?: number\n size?: string\n model?: string\n responseFormat?: 'url' | 'b64_json'\n signal?: AbortSignal\n}\n\nexport interface ImageGenResult {\n provider: string\n model: string\n images: GeneratedImage[]\n durationMs: number\n}\n\n/** OpenAI-compatible `/images/generations` endpoint (avoids double /v1). */\nexport function imageEndpoint(baseUrl: string): string {\n return openaiEndpoint(baseUrl, '/images/generations')\n}\n\n/** Build the OpenAI-compatible images request body. `n` is included only when\n * the caller passes it (already clamped per its model/capabilities) — some\n * models (e.g. Doubao Seedream) require `n` to be absent. */\nexport function buildOpenAIImageBody(\n model: string,\n req: { prompt: string; n?: number; size?: string; responseFormat?: 'url' | 'b64_json' },\n): Record<string, unknown> {\n const body: Record<string, unknown> = {\n model,\n prompt: req.prompt,\n response_format: req.responseFormat ?? 'url',\n }\n if (req.n !== undefined) body.n = req.n\n if (req.size) body.size = req.size\n return body\n}\n\n/** Extract images from an OpenAI-compatible `{ data: [...] }` response. */\nexport function extractOpenAIImages(json: unknown): GeneratedImage[] {\n const data = (json as { data?: Array<{ url?: string; b64_json?: string; revised_prompt?: string }> }).data\n if (!Array.isArray(data)) return []\n return data.map(d => ({\n ...(d.url ? { url: d.url } : {}),\n ...(d.b64_json ? { b64Json: d.b64_json } : {}),\n ...(d.revised_prompt ? { revisedPrompt: d.revised_prompt } : {}),\n }))\n}\n","/**\n * Voice/STT + realtime (端到端) provider TYPES + catalogs — shared so the\n * provider definitions live in one place. NOTE: the functional transports\n * (WebSocket for STT/realtime) stay platform-specific and are NOT in core yet:\n * desktop uses a Rust WS proxy (keys hidden); the web/mobile app has no cloud\n * voice backend, and a browser WS would expose API keys (against Moraya's\n * security model). When a backend exists, an AIRealtimeTransport adapter slots\n * in alongside the chat AITransport.\n */\n\n// ── Speech-to-text ──────────────────────────────────────────────────────────\nexport type SpeechProvider =\n | 'deepgram'\n | 'gladia'\n | 'assemblyai'\n | 'azure-speech'\n | 'aws-transcribe'\n | 'custom'\n\nexport interface SpeechProviderConfig {\n id: string\n provider: SpeechProvider\n apiKey: string\n baseUrl?: string\n model: string\n language: string\n region?: string\n awsAccessKey?: string\n awsSecretKey?: string\n}\n\n// ── Realtime full-duplex voice (端到端) ───────────────────────────────────────\nexport type RealtimeVoiceProvider =\n | 'gemini-live'\n | 'openai-realtime'\n | 'doubao-realtime'\n | 'qwen-realtime'\n | 'stepfun-realtime'\n | 'tongyi-bailing'\n | 'amazon-nova-sonic'\n\nexport interface RealtimeVoiceAIConfig {\n id: string\n provider: RealtimeVoiceProvider\n apiKey?: string\n baseUrl?: string\n model: string\n voice?: string\n region?: string\n /** Doubao Realtime: X-Api-App-ID. */\n appId?: string\n accessKeyId?: string\n secretAccessKey?: string\n sessionToken?: string\n extra?: Record<string, string>\n}\n\nexport const REALTIME_VOICE_DEFAULT_MODELS: Record<RealtimeVoiceProvider, string[]> = {\n 'gemini-live': ['gemini-live-2.5-flash-preview-native-audio'],\n 'openai-realtime': ['gpt-realtime'],\n 'doubao-realtime': ['doubao-realtime'],\n 'qwen-realtime': ['qwen-realtime'],\n 'stepfun-realtime': ['step-audio-chat'],\n 'tongyi-bailing': ['tongyi-bailing-realtime'],\n 'amazon-nova-sonic': ['amazon.nova-sonic-v1'],\n}\n\nexport const REALTIME_VOICE_BASE_URLS: Record<RealtimeVoiceProvider, string> = {\n 'gemini-live': 'wss://generativelanguage.googleapis.com/ws',\n 'openai-realtime': 'wss://api.openai.com/v1/realtime',\n 'doubao-realtime': 'wss://openspeech.bytedance.com/api/v3/realtime/dialogue',\n 'qwen-realtime': 'wss://dashscope.aliyuncs.com/api-ws/v1/inference',\n 'stepfun-realtime': 'wss://api.stepfun.com/v1/realtime',\n 'tongyi-bailing': 'wss://dashscope.aliyuncs.com/api-ws/v1/inference',\n 'amazon-nova-sonic': 'wss://transcribestreaming.{region}.amazonaws.com:8443',\n}\n\nexport const REALTIME_VOICE_PROVIDER_NAMES: Record<RealtimeVoiceProvider, string> = {\n 'gemini-live': 'Gemini Live',\n 'openai-realtime': 'OpenAI Realtime',\n 'doubao-realtime': 'Doubao Realtime',\n 'qwen-realtime': 'Qwen Realtime',\n 'stepfun-realtime': 'StepFun Realtime',\n 'tongyi-bailing': 'Tongyi Bailing',\n 'amazon-nova-sonic': 'Amazon Nova Sonic',\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared AI provider types — baselined on the Moraya desktop app, reconciled
|
|
3
|
+
* with the web/mobile app. Platform-agnostic: no host/native imports, no Node
|
|
4
|
+
* APIs. Transport (HTTP/SSE execution + key injection) is supplied by the
|
|
5
|
+
* consumer via the `AITransport` adapter in `./transport`.
|
|
6
|
+
*/
|
|
7
|
+
/** Chat/LLM provider ids. PC's 11 + the 2 on-device ids used by web/mobile.
|
|
8
|
+
* Note: there is no `anthropic` — web migrates `anthropic` → `claude`. */
|
|
9
|
+
type AIProvider = 'claude' | 'openai' | 'gemini' | 'deepseek' | 'ollama' | 'grok' | 'mistral' | 'glm' | 'minimax' | 'doubao' | 'custom' | 'local-mlx' | 'local-llama';
|
|
10
|
+
interface ImageAttachment {
|
|
11
|
+
id?: string;
|
|
12
|
+
/** "image/jpeg", "image/png", … */
|
|
13
|
+
mimeType: string;
|
|
14
|
+
/** base64-encoded data WITHOUT the `data:` prefix. */
|
|
15
|
+
base64: string;
|
|
16
|
+
previewUrl?: string;
|
|
17
|
+
fileName?: string;
|
|
18
|
+
}
|
|
19
|
+
interface ToolCallRequest {
|
|
20
|
+
id: string;
|
|
21
|
+
name: string;
|
|
22
|
+
arguments: Record<string, unknown>;
|
|
23
|
+
/** Provider-specific echo-back metadata (e.g. Gemini thoughtSignature). */
|
|
24
|
+
providerMeta?: Record<string, unknown>;
|
|
25
|
+
}
|
|
26
|
+
interface ToolDefinition {
|
|
27
|
+
name: string;
|
|
28
|
+
description: string;
|
|
29
|
+
input_schema: Record<string, unknown>;
|
|
30
|
+
}
|
|
31
|
+
interface ChatMessage {
|
|
32
|
+
role: 'user' | 'assistant' | 'system' | 'tool';
|
|
33
|
+
content: string;
|
|
34
|
+
/** PC sets it; optional so web can omit. */
|
|
35
|
+
timestamp?: number;
|
|
36
|
+
images?: ImageAttachment[];
|
|
37
|
+
toolCalls?: ToolCallRequest[];
|
|
38
|
+
toolCallId?: string;
|
|
39
|
+
toolName?: string;
|
|
40
|
+
isError?: boolean;
|
|
41
|
+
isSuccess?: boolean;
|
|
42
|
+
}
|
|
43
|
+
interface AIProviderConfig {
|
|
44
|
+
/** configId — used by the Tauri transport for Keychain lookup. */
|
|
45
|
+
id: string;
|
|
46
|
+
provider: AIProvider;
|
|
47
|
+
/** Cloud key (web) or `'***'` sentinel / paste-override (PC). Local: unused. */
|
|
48
|
+
apiKey?: string;
|
|
49
|
+
baseUrl?: string;
|
|
50
|
+
model: string;
|
|
51
|
+
maxTokens?: number;
|
|
52
|
+
temperature?: number;
|
|
53
|
+
topP?: number;
|
|
54
|
+
}
|
|
55
|
+
interface AIRequest {
|
|
56
|
+
messages: ChatMessage[];
|
|
57
|
+
stream?: boolean;
|
|
58
|
+
tools?: ToolDefinition[];
|
|
59
|
+
/** Per-request overrides (web feature); fall back to config.* */
|
|
60
|
+
model?: string;
|
|
61
|
+
temperature?: number;
|
|
62
|
+
maxTokens?: number;
|
|
63
|
+
topP?: number;
|
|
64
|
+
stop?: string[];
|
|
65
|
+
}
|
|
66
|
+
interface AIUsage {
|
|
67
|
+
inputTokens: number;
|
|
68
|
+
outputTokens: number;
|
|
69
|
+
}
|
|
70
|
+
interface AIResponse {
|
|
71
|
+
content: string;
|
|
72
|
+
model: string;
|
|
73
|
+
usage?: AIUsage;
|
|
74
|
+
toolCalls?: ToolCallRequest[];
|
|
75
|
+
stopReason?: 'end_turn' | 'tool_use' | 'max_tokens' | 'stop' | string;
|
|
76
|
+
}
|
|
77
|
+
/** One event yielded by the orchestrator (NOT a raw SSE line). */
|
|
78
|
+
interface AIStreamEvent {
|
|
79
|
+
delta?: string;
|
|
80
|
+
toolCalls?: ToolCallRequest[];
|
|
81
|
+
usage?: AIUsage;
|
|
82
|
+
stopReason?: string;
|
|
83
|
+
done?: boolean;
|
|
84
|
+
}
|
|
85
|
+
/** Result shape kept for the PC `streamAIRequestWithTools` wrapper. */
|
|
86
|
+
interface StreamToolResult {
|
|
87
|
+
content: string;
|
|
88
|
+
toolCalls?: ToolCallRequest[];
|
|
89
|
+
stopReason?: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export type { AIProvider, AIProviderConfig, AIRequest, AIResponse, AIStreamEvent, AIUsage, ChatMessage, ImageAttachment, StreamToolResult, ToolCallRequest, ToolDefinition };
|
package/dist/ai/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Voice/STT + realtime (端到端) provider TYPES + catalogs — shared so the
|
|
3
|
+
* provider definitions live in one place. NOTE: the functional transports
|
|
4
|
+
* (WebSocket for STT/realtime) stay platform-specific and are NOT in core yet:
|
|
5
|
+
* desktop uses a Rust WS proxy (keys hidden); the web/mobile app has no cloud
|
|
6
|
+
* voice backend, and a browser WS would expose API keys (against Moraya's
|
|
7
|
+
* security model). When a backend exists, an AIRealtimeTransport adapter slots
|
|
8
|
+
* in alongside the chat AITransport.
|
|
9
|
+
*/
|
|
10
|
+
type SpeechProvider = 'deepgram' | 'gladia' | 'assemblyai' | 'azure-speech' | 'aws-transcribe' | 'custom';
|
|
11
|
+
interface SpeechProviderConfig {
|
|
12
|
+
id: string;
|
|
13
|
+
provider: SpeechProvider;
|
|
14
|
+
apiKey: string;
|
|
15
|
+
baseUrl?: string;
|
|
16
|
+
model: string;
|
|
17
|
+
language: string;
|
|
18
|
+
region?: string;
|
|
19
|
+
awsAccessKey?: string;
|
|
20
|
+
awsSecretKey?: string;
|
|
21
|
+
}
|
|
22
|
+
type RealtimeVoiceProvider = 'gemini-live' | 'openai-realtime' | 'doubao-realtime' | 'qwen-realtime' | 'stepfun-realtime' | 'tongyi-bailing' | 'amazon-nova-sonic';
|
|
23
|
+
interface RealtimeVoiceAIConfig {
|
|
24
|
+
id: string;
|
|
25
|
+
provider: RealtimeVoiceProvider;
|
|
26
|
+
apiKey?: string;
|
|
27
|
+
baseUrl?: string;
|
|
28
|
+
model: string;
|
|
29
|
+
voice?: string;
|
|
30
|
+
region?: string;
|
|
31
|
+
/** Doubao Realtime: X-Api-App-ID. */
|
|
32
|
+
appId?: string;
|
|
33
|
+
accessKeyId?: string;
|
|
34
|
+
secretAccessKey?: string;
|
|
35
|
+
sessionToken?: string;
|
|
36
|
+
extra?: Record<string, string>;
|
|
37
|
+
}
|
|
38
|
+
declare const REALTIME_VOICE_DEFAULT_MODELS: Record<RealtimeVoiceProvider, string[]>;
|
|
39
|
+
declare const REALTIME_VOICE_BASE_URLS: Record<RealtimeVoiceProvider, string>;
|
|
40
|
+
declare const REALTIME_VOICE_PROVIDER_NAMES: Record<RealtimeVoiceProvider, string>;
|
|
41
|
+
|
|
42
|
+
export { REALTIME_VOICE_BASE_URLS, REALTIME_VOICE_DEFAULT_MODELS, REALTIME_VOICE_PROVIDER_NAMES, type RealtimeVoiceAIConfig, type RealtimeVoiceProvider, type SpeechProvider, type SpeechProviderConfig };
|
package/dist/ai/voice.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// src/ai/voice.ts
|
|
2
|
+
var REALTIME_VOICE_DEFAULT_MODELS = {
|
|
3
|
+
"gemini-live": ["gemini-live-2.5-flash-preview-native-audio"],
|
|
4
|
+
"openai-realtime": ["gpt-realtime"],
|
|
5
|
+
"doubao-realtime": ["doubao-realtime"],
|
|
6
|
+
"qwen-realtime": ["qwen-realtime"],
|
|
7
|
+
"stepfun-realtime": ["step-audio-chat"],
|
|
8
|
+
"tongyi-bailing": ["tongyi-bailing-realtime"],
|
|
9
|
+
"amazon-nova-sonic": ["amazon.nova-sonic-v1"]
|
|
10
|
+
};
|
|
11
|
+
var REALTIME_VOICE_BASE_URLS = {
|
|
12
|
+
"gemini-live": "wss://generativelanguage.googleapis.com/ws",
|
|
13
|
+
"openai-realtime": "wss://api.openai.com/v1/realtime",
|
|
14
|
+
"doubao-realtime": "wss://openspeech.bytedance.com/api/v3/realtime/dialogue",
|
|
15
|
+
"qwen-realtime": "wss://dashscope.aliyuncs.com/api-ws/v1/inference",
|
|
16
|
+
"stepfun-realtime": "wss://api.stepfun.com/v1/realtime",
|
|
17
|
+
"tongyi-bailing": "wss://dashscope.aliyuncs.com/api-ws/v1/inference",
|
|
18
|
+
"amazon-nova-sonic": "wss://transcribestreaming.{region}.amazonaws.com:8443"
|
|
19
|
+
};
|
|
20
|
+
var REALTIME_VOICE_PROVIDER_NAMES = {
|
|
21
|
+
"gemini-live": "Gemini Live",
|
|
22
|
+
"openai-realtime": "OpenAI Realtime",
|
|
23
|
+
"doubao-realtime": "Doubao Realtime",
|
|
24
|
+
"qwen-realtime": "Qwen Realtime",
|
|
25
|
+
"stepfun-realtime": "StepFun Realtime",
|
|
26
|
+
"tongyi-bailing": "Tongyi Bailing",
|
|
27
|
+
"amazon-nova-sonic": "Amazon Nova Sonic"
|
|
28
|
+
};
|
|
29
|
+
export {
|
|
30
|
+
REALTIME_VOICE_BASE_URLS,
|
|
31
|
+
REALTIME_VOICE_DEFAULT_MODELS,
|
|
32
|
+
REALTIME_VOICE_PROVIDER_NAMES
|
|
33
|
+
};
|
|
34
|
+
//# sourceMappingURL=voice.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/ai/voice.ts"],"sourcesContent":["/**\n * Voice/STT + realtime (端到端) provider TYPES + catalogs — shared so the\n * provider definitions live in one place. NOTE: the functional transports\n * (WebSocket for STT/realtime) stay platform-specific and are NOT in core yet:\n * desktop uses a Rust WS proxy (keys hidden); the web/mobile app has no cloud\n * voice backend, and a browser WS would expose API keys (against Moraya's\n * security model). When a backend exists, an AIRealtimeTransport adapter slots\n * in alongside the chat AITransport.\n */\n\n// ── Speech-to-text ──────────────────────────────────────────────────────────\nexport type SpeechProvider =\n | 'deepgram'\n | 'gladia'\n | 'assemblyai'\n | 'azure-speech'\n | 'aws-transcribe'\n | 'custom'\n\nexport interface SpeechProviderConfig {\n id: string\n provider: SpeechProvider\n apiKey: string\n baseUrl?: string\n model: string\n language: string\n region?: string\n awsAccessKey?: string\n awsSecretKey?: string\n}\n\n// ── Realtime full-duplex voice (端到端) ───────────────────────────────────────\nexport type RealtimeVoiceProvider =\n | 'gemini-live'\n | 'openai-realtime'\n | 'doubao-realtime'\n | 'qwen-realtime'\n | 'stepfun-realtime'\n | 'tongyi-bailing'\n | 'amazon-nova-sonic'\n\nexport interface RealtimeVoiceAIConfig {\n id: string\n provider: RealtimeVoiceProvider\n apiKey?: string\n baseUrl?: string\n model: string\n voice?: string\n region?: string\n /** Doubao Realtime: X-Api-App-ID. */\n appId?: string\n accessKeyId?: string\n secretAccessKey?: string\n sessionToken?: string\n extra?: Record<string, string>\n}\n\nexport const REALTIME_VOICE_DEFAULT_MODELS: Record<RealtimeVoiceProvider, string[]> = {\n 'gemini-live': ['gemini-live-2.5-flash-preview-native-audio'],\n 'openai-realtime': ['gpt-realtime'],\n 'doubao-realtime': ['doubao-realtime'],\n 'qwen-realtime': ['qwen-realtime'],\n 'stepfun-realtime': ['step-audio-chat'],\n 'tongyi-bailing': ['tongyi-bailing-realtime'],\n 'amazon-nova-sonic': ['amazon.nova-sonic-v1'],\n}\n\nexport const REALTIME_VOICE_BASE_URLS: Record<RealtimeVoiceProvider, string> = {\n 'gemini-live': 'wss://generativelanguage.googleapis.com/ws',\n 'openai-realtime': 'wss://api.openai.com/v1/realtime',\n 'doubao-realtime': 'wss://openspeech.bytedance.com/api/v3/realtime/dialogue',\n 'qwen-realtime': 'wss://dashscope.aliyuncs.com/api-ws/v1/inference',\n 'stepfun-realtime': 'wss://api.stepfun.com/v1/realtime',\n 'tongyi-bailing': 'wss://dashscope.aliyuncs.com/api-ws/v1/inference',\n 'amazon-nova-sonic': 'wss://transcribestreaming.{region}.amazonaws.com:8443',\n}\n\nexport const REALTIME_VOICE_PROVIDER_NAMES: Record<RealtimeVoiceProvider, string> = {\n 'gemini-live': 'Gemini Live',\n 'openai-realtime': 'OpenAI Realtime',\n 'doubao-realtime': 'Doubao Realtime',\n 'qwen-realtime': 'Qwen Realtime',\n 'stepfun-realtime': 'StepFun Realtime',\n 'tongyi-bailing': 'Tongyi Bailing',\n 'amazon-nova-sonic': 'Amazon Nova Sonic',\n}\n"],"mappings":";AAyDO,IAAM,gCAAyE;AAAA,EACpF,eAAe,CAAC,4CAA4C;AAAA,EAC5D,mBAAmB,CAAC,cAAc;AAAA,EAClC,mBAAmB,CAAC,iBAAiB;AAAA,EACrC,iBAAiB,CAAC,eAAe;AAAA,EACjC,oBAAoB,CAAC,iBAAiB;AAAA,EACtC,kBAAkB,CAAC,yBAAyB;AAAA,EAC5C,qBAAqB,CAAC,sBAAsB;AAC9C;AAEO,IAAM,2BAAkE;AAAA,EAC7E,eAAe;AAAA,EACf,mBAAmB;AAAA,EACnB,mBAAmB;AAAA,EACnB,iBAAiB;AAAA,EACjB,oBAAoB;AAAA,EACpB,kBAAkB;AAAA,EAClB,qBAAqB;AACvB;AAEO,IAAM,gCAAuE;AAAA,EAClF,eAAe;AAAA,EACf,mBAAmB;AAAA,EACnB,mBAAmB;AAAA,EACnB,iBAAiB;AAAA,EACjB,oBAAoB;AAAA,EACpB,kBAAkB;AAAA,EAClB,qBAAqB;AACvB;","names":[]}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @moraya/core/chat-markdown — streaming-safe markdown → HTML for AI chat bubbles.
|
|
3
|
+
*
|
|
4
|
+
* Pure-function, framework-agnostic. Runs in browsers, Node SSR, Edge runtimes,
|
|
5
|
+
* and Workers. Zero dependencies on KaTeX or highlight.js — consumers wire those
|
|
6
|
+
* in via `math` / `highlight` callbacks, which lets bundlers tree-shake them out
|
|
7
|
+
* for chat apps that don't need math (e.g. mobile chat saves ~280 KB).
|
|
8
|
+
*
|
|
9
|
+
* Streaming safety: every call is idempotent. Half-written code fences, math, or
|
|
10
|
+
* links produced by LLM SSE chunking degrade to readable HTML — never throws.
|
|
11
|
+
*
|
|
12
|
+
* Security posture (matches the locked-down config Moraya mobile shipped to
|
|
13
|
+
* production):
|
|
14
|
+
* - `html: false` — raw HTML in the model's reply is escaped, not rendered
|
|
15
|
+
* - `linkify: true` — bare URLs become anchors
|
|
16
|
+
* - All `<a>` tags get `target="_blank"` + `rel="noopener noreferrer"` by
|
|
17
|
+
* default (overridable via `linkAttrs`)
|
|
18
|
+
* - URL protocol whitelist: only http/https/mailto/tel (markdown-it default
|
|
19
|
+
* `validateLink` + our explicit deny of javascript:/data:text/html/vbscript:)
|
|
20
|
+
*
|
|
21
|
+
* Design note (v0.4.0): the public surface is one function + one options
|
|
22
|
+
* object. The locked plan called for `math: boolean` ergonomics, but pivoted
|
|
23
|
+
* to callback-form during implementation because tsup's `splitting: false`
|
|
24
|
+
* means any internal `import 'katex'` would land in every consumer's bundle,
|
|
25
|
+
* defeating the tree-shake intent. Callbacks let consumers BYO renderer (or
|
|
26
|
+
* skip math entirely on mobile) with zero loss of ergonomics — the README
|
|
27
|
+
* shows the 5-line wiring pattern.
|
|
28
|
+
*/
|
|
29
|
+
interface ChatMarkdownLinkAttrs {
|
|
30
|
+
/** Default `'_blank'`. Pass `'_self'` for in-app navigation contexts. */
|
|
31
|
+
target?: string;
|
|
32
|
+
/** Default `'noopener noreferrer'`. */
|
|
33
|
+
rel?: string;
|
|
34
|
+
}
|
|
35
|
+
interface ChatMarkdownOptions {
|
|
36
|
+
/**
|
|
37
|
+
* Math renderer. When provided, `$inline$` and `$$display$$` math is parsed
|
|
38
|
+
* and passed to this callback. When omitted, math syntax renders as plain
|
|
39
|
+
* text (the dollar signs are kept verbatim).
|
|
40
|
+
*
|
|
41
|
+
* Typical wiring with KaTeX:
|
|
42
|
+
* import katex from 'katex'
|
|
43
|
+
* { math: (latex, display) => katex.renderToString(latex, { displayMode: display, throwOnError: false }) }
|
|
44
|
+
*/
|
|
45
|
+
math?: (latex: string, displayMode: boolean) => string;
|
|
46
|
+
/**
|
|
47
|
+
* Code block highlighter. When provided, fenced code blocks are passed to
|
|
48
|
+
* this callback (raw source + language tag). Return rendered inner HTML
|
|
49
|
+
* (NOT including <pre><code>); the caller wraps it. Return `null` to fall
|
|
50
|
+
* back to the default escaped <code>.
|
|
51
|
+
*
|
|
52
|
+
* Typical wiring with highlight.js:
|
|
53
|
+
* import hljs from 'highlight.js'
|
|
54
|
+
* { highlight: (code, lang) => {
|
|
55
|
+
* if (lang && hljs.getLanguage(lang)) {
|
|
56
|
+
* return hljs.highlight(code, { language: lang, ignoreIllegals: true }).value
|
|
57
|
+
* }
|
|
58
|
+
* return null
|
|
59
|
+
* } }
|
|
60
|
+
*/
|
|
61
|
+
highlight?: (code: string, lang: string) => string | null;
|
|
62
|
+
/** Override default link attributes. */
|
|
63
|
+
linkAttrs?: ChatMarkdownLinkAttrs;
|
|
64
|
+
/**
|
|
65
|
+
* Pre-process the markdown input before parsing. Use for app-specific
|
|
66
|
+
* tokens like @mentions, custom slash commands, or local i18n expansion.
|
|
67
|
+
* Whatever you return is fed straight to markdown-it.
|
|
68
|
+
*
|
|
69
|
+
* Mobile uses this to expand `@<id>` into a bold `**@Title**` pill.
|
|
70
|
+
*/
|
|
71
|
+
preprocess?: (raw: string) => string;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Render a markdown string to safe HTML for chat-bubble display.
|
|
75
|
+
*
|
|
76
|
+
* Safe to inject via `dangerouslySetInnerHTML` (React), `{@html}` (Svelte),
|
|
77
|
+
* `v-html` (Vue), or `.innerHTML` (vanilla). No <script>, no event handlers,
|
|
78
|
+
* no javascript: / data:text/html URLs.
|
|
79
|
+
*/
|
|
80
|
+
declare function renderChatMarkdown(input: string, opts?: ChatMarkdownOptions): string;
|
|
81
|
+
|
|
82
|
+
export { type ChatMarkdownLinkAttrs, type ChatMarkdownOptions, renderChatMarkdown };
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// src/chat-markdown/index.ts
|
|
2
|
+
import MarkdownIt from "markdown-it";
|
|
3
|
+
var DEFAULT_TARGET = "_blank";
|
|
4
|
+
var DEFAULT_REL = "noopener noreferrer";
|
|
5
|
+
var cache = /* @__PURE__ */ new WeakMap();
|
|
6
|
+
var defaultInstance = null;
|
|
7
|
+
function buildInstance(opts) {
|
|
8
|
+
const target = opts?.linkAttrs?.target ?? DEFAULT_TARGET;
|
|
9
|
+
const rel = opts?.linkAttrs?.rel ?? DEFAULT_REL;
|
|
10
|
+
const md = new MarkdownIt({
|
|
11
|
+
html: false,
|
|
12
|
+
linkify: true,
|
|
13
|
+
typographer: false,
|
|
14
|
+
breaks: true,
|
|
15
|
+
// markdown-it's `highlight` callback wraps the result in <pre><code>
|
|
16
|
+
// itself if we return a string; we let it pass through verbatim and
|
|
17
|
+
// wrap on its own when the user-provided highlighter handles a lang.
|
|
18
|
+
highlight: opts?.highlight ? (code, lang) => {
|
|
19
|
+
try {
|
|
20
|
+
const out = opts.highlight(code, lang);
|
|
21
|
+
if (out == null) return "";
|
|
22
|
+
return out;
|
|
23
|
+
} catch {
|
|
24
|
+
return "";
|
|
25
|
+
}
|
|
26
|
+
} : void 0
|
|
27
|
+
});
|
|
28
|
+
const defaultValidateLink = md.validateLink.bind(md);
|
|
29
|
+
md.validateLink = (url) => {
|
|
30
|
+
const trimmed = url.trim().toLowerCase();
|
|
31
|
+
if (trimmed.startsWith("javascript:")) return false;
|
|
32
|
+
if (trimmed.startsWith("vbscript:")) return false;
|
|
33
|
+
if (trimmed.startsWith("data:text/html")) return false;
|
|
34
|
+
return defaultValidateLink(url);
|
|
35
|
+
};
|
|
36
|
+
const defaultLinkOpen = md.renderer.rules.link_open ?? ((tokens, idx, options, _env, self) => self.renderToken(tokens, idx, options));
|
|
37
|
+
md.renderer.rules.link_open = (tokens, idx, options, env, self) => {
|
|
38
|
+
const token = tokens[idx];
|
|
39
|
+
token.attrSet("target", target);
|
|
40
|
+
token.attrSet("rel", rel);
|
|
41
|
+
return defaultLinkOpen(tokens, idx, options, env, self);
|
|
42
|
+
};
|
|
43
|
+
if (opts?.math) {
|
|
44
|
+
installMathRules(md, opts.math);
|
|
45
|
+
}
|
|
46
|
+
return md;
|
|
47
|
+
}
|
|
48
|
+
function getInstance(opts) {
|
|
49
|
+
if (!opts) {
|
|
50
|
+
if (!defaultInstance) defaultInstance = buildInstance(void 0);
|
|
51
|
+
return defaultInstance;
|
|
52
|
+
}
|
|
53
|
+
let inst = cache.get(opts);
|
|
54
|
+
if (!inst) {
|
|
55
|
+
inst = buildInstance(opts);
|
|
56
|
+
cache.set(opts, inst);
|
|
57
|
+
}
|
|
58
|
+
return inst;
|
|
59
|
+
}
|
|
60
|
+
function renderChatMarkdown(input, opts) {
|
|
61
|
+
if (!input) return "";
|
|
62
|
+
const processed = opts?.preprocess ? opts.preprocess(input) : input;
|
|
63
|
+
try {
|
|
64
|
+
return getInstance(opts).render(processed);
|
|
65
|
+
} catch {
|
|
66
|
+
return escapeHtml(processed).replace(/\n/g, "<br>\n");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function escapeHtml(s) {
|
|
70
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
71
|
+
}
|
|
72
|
+
function installMathRules(md, render) {
|
|
73
|
+
md.inline.ruler.after("escape", "math_inline", (state, silent) => {
|
|
74
|
+
if (state.src[state.pos] !== "$") return false;
|
|
75
|
+
if (state.pos > 0 && state.src[state.pos - 1] === "\\") return false;
|
|
76
|
+
if (state.src[state.pos + 1] === "$") return false;
|
|
77
|
+
const start = state.pos + 1;
|
|
78
|
+
let end = start;
|
|
79
|
+
while (end < state.posMax) {
|
|
80
|
+
if (state.src[end] === "$" && state.src[end - 1] !== "\\") break;
|
|
81
|
+
end++;
|
|
82
|
+
}
|
|
83
|
+
if (end >= state.posMax) return false;
|
|
84
|
+
const after = state.src[end + 1];
|
|
85
|
+
if (after && /\d/.test(after)) return false;
|
|
86
|
+
const latex = state.src.slice(start, end);
|
|
87
|
+
if (!latex.trim()) return false;
|
|
88
|
+
if (!silent) {
|
|
89
|
+
const token = state.push("math_inline", "span", 0);
|
|
90
|
+
token.content = latex;
|
|
91
|
+
token.markup = "$";
|
|
92
|
+
}
|
|
93
|
+
state.pos = end + 1;
|
|
94
|
+
return true;
|
|
95
|
+
});
|
|
96
|
+
md.block.ruler.after("blockquote", "math_block", (state, startLine, endLine, silent) => {
|
|
97
|
+
const startPos = state.bMarks[startLine] + state.tShift[startLine];
|
|
98
|
+
const max = state.eMarks[startLine];
|
|
99
|
+
if (startPos + 2 > max) return false;
|
|
100
|
+
if (state.src.slice(startPos, startPos + 2) !== "$$") return false;
|
|
101
|
+
let nextLine = startLine;
|
|
102
|
+
let found = false;
|
|
103
|
+
const sameLineRest = state.src.slice(startPos + 2, max);
|
|
104
|
+
const sameLineClose = sameLineRest.indexOf("$$");
|
|
105
|
+
if (sameLineClose !== -1) {
|
|
106
|
+
found = true;
|
|
107
|
+
const trailing = sameLineRest.slice(sameLineClose + 2).trim();
|
|
108
|
+
if (trailing !== "") {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
for (nextLine = startLine + 1; nextLine < endLine; nextLine++) {
|
|
113
|
+
const lineStart = state.bMarks[nextLine] + state.tShift[nextLine];
|
|
114
|
+
const lineEnd = state.eMarks[nextLine];
|
|
115
|
+
const line = state.src.slice(lineStart, lineEnd);
|
|
116
|
+
if (line.trim().endsWith("$$")) {
|
|
117
|
+
found = true;
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (!found) return false;
|
|
123
|
+
if (silent) return true;
|
|
124
|
+
let latex;
|
|
125
|
+
if (sameLineClose !== -1) {
|
|
126
|
+
latex = sameLineRest.slice(0, sameLineClose).trim();
|
|
127
|
+
} else {
|
|
128
|
+
const closeLineEnd = state.eMarks[nextLine];
|
|
129
|
+
const closeLineStart = state.bMarks[nextLine] + state.tShift[nextLine];
|
|
130
|
+
const closingLine = state.src.slice(closeLineStart, closeLineEnd);
|
|
131
|
+
const closingPos = closingLine.lastIndexOf("$$");
|
|
132
|
+
const startOfBody = startPos + 2;
|
|
133
|
+
const endOfBody = state.bMarks[nextLine] + state.tShift[nextLine] + closingPos;
|
|
134
|
+
latex = state.src.slice(startOfBody, endOfBody).trim();
|
|
135
|
+
}
|
|
136
|
+
const token = state.push("math_block", "div", 0);
|
|
137
|
+
token.block = true;
|
|
138
|
+
token.content = latex;
|
|
139
|
+
token.markup = "$$";
|
|
140
|
+
token.map = [startLine, nextLine + 1];
|
|
141
|
+
state.line = nextLine + 1;
|
|
142
|
+
return true;
|
|
143
|
+
});
|
|
144
|
+
md.renderer.rules.math_inline = (tokens, idx) => {
|
|
145
|
+
const latex = tokens[idx].content;
|
|
146
|
+
try {
|
|
147
|
+
return render(latex, false);
|
|
148
|
+
} catch {
|
|
149
|
+
return `<code>$${escapeHtml(latex)}$</code>`;
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
md.renderer.rules.math_block = (tokens, idx) => {
|
|
153
|
+
const latex = tokens[idx].content;
|
|
154
|
+
try {
|
|
155
|
+
return render(latex, true) + "\n";
|
|
156
|
+
} catch {
|
|
157
|
+
return `<pre><code>$$${escapeHtml(latex)}$$</code></pre>
|
|
158
|
+
`;
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
export {
|
|
163
|
+
renderChatMarkdown
|
|
164
|
+
};
|
|
165
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/chat-markdown/index.ts"],"sourcesContent":["/**\n * @moraya/core/chat-markdown — streaming-safe markdown → HTML for AI chat bubbles.\n *\n * Pure-function, framework-agnostic. Runs in browsers, Node SSR, Edge runtimes,\n * and Workers. Zero dependencies on KaTeX or highlight.js — consumers wire those\n * in via `math` / `highlight` callbacks, which lets bundlers tree-shake them out\n * for chat apps that don't need math (e.g. mobile chat saves ~280 KB).\n *\n * Streaming safety: every call is idempotent. Half-written code fences, math, or\n * links produced by LLM SSE chunking degrade to readable HTML — never throws.\n *\n * Security posture (matches the locked-down config Moraya mobile shipped to\n * production):\n * - `html: false` — raw HTML in the model's reply is escaped, not rendered\n * - `linkify: true` — bare URLs become anchors\n * - All `<a>` tags get `target=\"_blank\"` + `rel=\"noopener noreferrer\"` by\n * default (overridable via `linkAttrs`)\n * - URL protocol whitelist: only http/https/mailto/tel (markdown-it default\n * `validateLink` + our explicit deny of javascript:/data:text/html/vbscript:)\n *\n * Design note (v0.4.0): the public surface is one function + one options\n * object. The locked plan called for `math: boolean` ergonomics, but pivoted\n * to callback-form during implementation because tsup's `splitting: false`\n * means any internal `import 'katex'` would land in every consumer's bundle,\n * defeating the tree-shake intent. Callbacks let consumers BYO renderer (or\n * skip math entirely on mobile) with zero loss of ergonomics — the README\n * shows the 5-line wiring pattern.\n */\n\nimport MarkdownIt from 'markdown-it'\n\nexport interface ChatMarkdownLinkAttrs {\n /** Default `'_blank'`. Pass `'_self'` for in-app navigation contexts. */\n target?: string\n /** Default `'noopener noreferrer'`. */\n rel?: string\n}\n\nexport interface ChatMarkdownOptions {\n /**\n * Math renderer. When provided, `$inline$` and `$$display$$` math is parsed\n * and passed to this callback. When omitted, math syntax renders as plain\n * text (the dollar signs are kept verbatim).\n *\n * Typical wiring with KaTeX:\n * import katex from 'katex'\n * { math: (latex, display) => katex.renderToString(latex, { displayMode: display, throwOnError: false }) }\n */\n math?: (latex: string, displayMode: boolean) => string\n\n /**\n * Code block highlighter. When provided, fenced code blocks are passed to\n * this callback (raw source + language tag). Return rendered inner HTML\n * (NOT including <pre><code>); the caller wraps it. Return `null` to fall\n * back to the default escaped <code>.\n *\n * Typical wiring with highlight.js:\n * import hljs from 'highlight.js'\n * { highlight: (code, lang) => {\n * if (lang && hljs.getLanguage(lang)) {\n * return hljs.highlight(code, { language: lang, ignoreIllegals: true }).value\n * }\n * return null\n * } }\n */\n highlight?: (code: string, lang: string) => string | null\n\n /** Override default link attributes. */\n linkAttrs?: ChatMarkdownLinkAttrs\n\n /**\n * Pre-process the markdown input before parsing. Use for app-specific\n * tokens like @mentions, custom slash commands, or local i18n expansion.\n * Whatever you return is fed straight to markdown-it.\n *\n * Mobile uses this to expand `@<id>` into a bold `**@Title**` pill.\n */\n preprocess?: (raw: string) => string\n}\n\nconst DEFAULT_TARGET = '_blank'\nconst DEFAULT_REL = 'noopener noreferrer'\n\n// One markdown-it instance per distinct (linkAttrs × highlight) tuple. Keeps\n// the hot path allocation-free for the overwhelmingly common case (caller\n// always passes the same opts).\nconst cache = new WeakMap<ChatMarkdownOptions, MarkdownIt>()\nlet defaultInstance: MarkdownIt | null = null\n\nfunction buildInstance(opts: ChatMarkdownOptions | undefined): MarkdownIt {\n const target = opts?.linkAttrs?.target ?? DEFAULT_TARGET\n const rel = opts?.linkAttrs?.rel ?? DEFAULT_REL\n\n const md = new MarkdownIt({\n html: false,\n linkify: true,\n typographer: false,\n breaks: true,\n // markdown-it's `highlight` callback wraps the result in <pre><code>\n // itself if we return a string; we let it pass through verbatim and\n // wrap on its own when the user-provided highlighter handles a lang.\n highlight: opts?.highlight\n ? (code: string, lang: string) => {\n try {\n const out = opts.highlight!(code, lang)\n if (out == null) return ''\n return out\n } catch {\n // Highlighter errors must never break the bubble — fall back\n // to escaped default rendering by returning empty string.\n return ''\n }\n }\n : undefined,\n })\n\n // Deny javascript:/vbscript:/data:text/html URLs at the parser level. Belt\n // & suspenders alongside `html: false` — markdown-it already validates\n // URLs by default, but we make the policy explicit and unbypassable.\n const defaultValidateLink = md.validateLink.bind(md)\n md.validateLink = (url: string): boolean => {\n const trimmed = url.trim().toLowerCase()\n if (trimmed.startsWith('javascript:')) return false\n if (trimmed.startsWith('vbscript:')) return false\n if (trimmed.startsWith('data:text/html')) return false\n return defaultValidateLink(url)\n }\n\n // Force target + rel on every <a> emitted by markdown-it.\n const defaultLinkOpen = md.renderer.rules.link_open\n ?? ((tokens, idx, options, _env, self) => self.renderToken(tokens, idx, options))\n md.renderer.rules.link_open = (tokens, idx, options, env, self) => {\n const token = tokens[idx]!\n token.attrSet('target', target)\n token.attrSet('rel', rel)\n return defaultLinkOpen(tokens, idx, options, env, self)\n }\n\n // Math: inline + block. We register custom inline + block rules so the\n // KaTeX callback only runs when math syntax is actually present.\n if (opts?.math) {\n installMathRules(md, opts.math)\n }\n\n return md\n}\n\nfunction getInstance(opts?: ChatMarkdownOptions): MarkdownIt {\n if (!opts) {\n if (!defaultInstance) defaultInstance = buildInstance(undefined)\n return defaultInstance\n }\n let inst = cache.get(opts)\n if (!inst) {\n inst = buildInstance(opts)\n cache.set(opts, inst)\n }\n return inst\n}\n\n/**\n * Render a markdown string to safe HTML for chat-bubble display.\n *\n * Safe to inject via `dangerouslySetInnerHTML` (React), `{@html}` (Svelte),\n * `v-html` (Vue), or `.innerHTML` (vanilla). No <script>, no event handlers,\n * no javascript: / data:text/html URLs.\n */\nexport function renderChatMarkdown(\n input: string,\n opts?: ChatMarkdownOptions,\n): string {\n if (!input) return ''\n const processed = opts?.preprocess ? opts.preprocess(input) : input\n // markdown-it itself never throws on malformed input — the catch here is\n // a belt-and-suspenders measure for downstream plugin failures (e.g. a\n // user-supplied highlighter that throws synchronously). On error we fall\n // back to the input escaped as plain text so the bubble still renders.\n try {\n return getInstance(opts).render(processed)\n } catch {\n return escapeHtml(processed).replace(/\\n/g, '<br>\\n')\n }\n}\n\nfunction escapeHtml(s: string): string {\n return s\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''')\n}\n\n/**\n * Install $...$ (inline) and $$...$$ (block) math rules on a markdown-it\n * instance. The rules are intentionally lightweight: we look for raw `$`\n * delimiters and let the consumer's callback do the actual LaTeX → HTML\n * conversion. On callback failure (invalid LaTeX, KaTeX throw, etc.) we\n * fall back to the original `$...$` text rendered as a <code> span — this\n * is what PC desktop has shipped for years.\n */\nfunction installMathRules(\n md: MarkdownIt,\n render: (latex: string, displayMode: boolean) => string,\n): void {\n // Inline: $...$ where the closing $ is not preceded by whitespace, and the\n // delimiters aren't escaped. We're deliberately strict to avoid matching\n // dollar-amount usages like \"$5 and $10\" — the closing $ must be followed\n // by a non-digit or end-of-string.\n md.inline.ruler.after('escape', 'math_inline', (state, silent) => {\n if (state.src[state.pos] !== '$') return false\n // Reject escaped \\$\n if (state.pos > 0 && state.src[state.pos - 1] === '\\\\') return false\n // Reject $$ (handled by block rule)\n if (state.src[state.pos + 1] === '$') return false\n\n const start = state.pos + 1\n let end = start\n while (end < state.posMax) {\n if (state.src[end] === '$' && state.src[end - 1] !== '\\\\') break\n end++\n }\n if (end >= state.posMax) return false\n // Require the char after closing $ to NOT be a digit (so \"$5\" doesn't match)\n const after = state.src[end + 1]\n if (after && /\\d/.test(after)) return false\n\n const latex = state.src.slice(start, end)\n if (!latex.trim()) return false\n\n if (!silent) {\n const token = state.push('math_inline', 'span', 0)\n token.content = latex\n token.markup = '$'\n }\n state.pos = end + 1\n return true\n })\n\n // Block: $$...$$ on its own line(s).\n md.block.ruler.after('blockquote', 'math_block', (state, startLine, endLine, silent) => {\n const startPos = state.bMarks[startLine]! + state.tShift[startLine]!\n const max = state.eMarks[startLine]!\n if (startPos + 2 > max) return false\n if (state.src.slice(startPos, startPos + 2) !== '$$') return false\n\n // Find closing $$ on a subsequent line (could be same line for single-line displays)\n let nextLine = startLine\n let found = false\n // First check if closing is on the same line: $$ formula $$\n const sameLineRest = state.src.slice(startPos + 2, max)\n const sameLineClose = sameLineRest.indexOf('$$')\n if (sameLineClose !== -1) {\n found = true\n // Confirm nothing after the closing $$ except whitespace\n const trailing = sameLineRest.slice(sameLineClose + 2).trim()\n if (trailing !== '') {\n return false\n }\n } else {\n // Multi-line — scan forward.\n for (nextLine = startLine + 1; nextLine < endLine; nextLine++) {\n const lineStart = state.bMarks[nextLine]! + state.tShift[nextLine]!\n const lineEnd = state.eMarks[nextLine]!\n const line = state.src.slice(lineStart, lineEnd)\n if (line.trim().endsWith('$$')) {\n found = true\n break\n }\n }\n }\n if (!found) return false\n if (silent) return true\n\n let latex: string\n if (sameLineClose !== -1) {\n latex = sameLineRest.slice(0, sameLineClose).trim()\n } else {\n const closeLineEnd = state.eMarks[nextLine]!\n const closeLineStart = state.bMarks[nextLine]! + state.tShift[nextLine]!\n const closingLine = state.src.slice(closeLineStart, closeLineEnd)\n const closingPos = closingLine.lastIndexOf('$$')\n const startOfBody = startPos + 2\n const endOfBody = state.bMarks[nextLine]! + state.tShift[nextLine]! + closingPos\n latex = state.src.slice(startOfBody, endOfBody).trim()\n }\n\n const token = state.push('math_block', 'div', 0)\n token.block = true\n token.content = latex\n token.markup = '$$'\n token.map = [startLine, nextLine + 1]\n\n state.line = nextLine + 1\n return true\n })\n\n md.renderer.rules.math_inline = (tokens, idx) => {\n const latex = tokens[idx]!.content\n try {\n return render(latex, false)\n } catch {\n return `<code>$${escapeHtml(latex)}$</code>`\n }\n }\n\n md.renderer.rules.math_block = (tokens, idx) => {\n const latex = tokens[idx]!.content\n try {\n return render(latex, true) + '\\n'\n } catch {\n return `<pre><code>$$${escapeHtml(latex)}$$</code></pre>\\n`\n }\n }\n}\n"],"mappings":"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","names":[]}
|