76 lines
3.8 KiB
JavaScript
76 lines
3.8 KiB
JavaScript
/*
|
|
* Platform-neutral LineUp presentation kernel.
|
|
*
|
|
* It converts untrusted transport payloads into a small, validated set of
|
|
* ConversationItems. UI hosts register trusted renderers for those items;
|
|
* neither a renderer nor a UI Surface needs to parse IM payloads directly.
|
|
*/
|
|
(function exposeLineUpKernel(root, factory) {
|
|
const api = factory();
|
|
if (typeof module === 'object' && module.exports) module.exports = api;
|
|
root.LineUp = Object.assign(root.LineUp || {}, api);
|
|
})(typeof globalThis === 'undefined' ? window : globalThis, function createKernel() {
|
|
const PROTOCOL_VERSION = 1;
|
|
const MAX_PAYLOAD_BYTES = 192 * 1024;
|
|
const UI_TYPES = new Set(['lineup.v1.ui.open', 'lineup.v1.ui.patch', 'lineup.v1.ui.close']);
|
|
|
|
function isObject(value) { return value !== null && typeof value === 'object' && !Array.isArray(value); }
|
|
function byteLength(value) { return new TextEncoder().encode(String(value)).length; }
|
|
function text(value) { return typeof value === 'string' ? value : ''; }
|
|
function item(kind, data, envelope) {
|
|
return Object.freeze({ kind, data: Object.freeze(data), envelope: envelope || null, receivedAt: Date.now() });
|
|
}
|
|
|
|
class ConversationItemFactory {
|
|
decode(rawPayload) {
|
|
const raw = String(rawPayload ?? '');
|
|
if (byteLength(raw) > MAX_PAYLOAD_BYTES) return item('protocol-fallback', { reason: 'payload_too_large' });
|
|
let value;
|
|
try { value = JSON.parse(raw); } catch { return item('markdown', { markdown: raw, source: 'plain' }); }
|
|
if (!isObject(value) || typeof value.type !== 'string') return item('markdown', { markdown: raw, source: 'plain' });
|
|
if (value.v !== PROTOCOL_VERSION || !value.type.startsWith('lineup.v1.')) return this.legacy(value);
|
|
return this.envelope(value);
|
|
}
|
|
|
|
envelope(envelope) {
|
|
const payload = isObject(envelope.payload) ? envelope.payload : {};
|
|
switch (envelope.type) {
|
|
case 'lineup.v1.text':
|
|
return item('markdown', { markdown: text(payload.text), source: 'agent' }, envelope);
|
|
case 'lineup.v1.agent.status':
|
|
return item('agent-status', { status: text(payload.status) || 'unknown', detail: text(payload.detail) }, envelope);
|
|
case 'lineup.v1.agent.progress':
|
|
return item('progress', { title: text(payload.title) || '处理中', percent: Number(payload.percent) || 0, status: text(payload.status) }, envelope);
|
|
case 'lineup.v1.error':
|
|
return item('agent-error', { code: text(payload.code), message: text(payload.message) || '未知错误' }, envelope);
|
|
case 'lineup.v1.app.call':
|
|
return item('app-call', payload, envelope);
|
|
default:
|
|
if (UI_TYPES.has(envelope.type)) return item(envelope.type.slice('lineup.v1.'.length), payload, envelope);
|
|
return item('protocol-fallback', { reason: 'unsupported_type', type: envelope.type }, envelope);
|
|
}
|
|
}
|
|
|
|
legacy(value) {
|
|
if (value.v === undefined && typeof value.type === 'string') return item('legacy-card', value);
|
|
return item('protocol-fallback', { reason: 'unsupported_version', version: value.v, type: value.type });
|
|
}
|
|
}
|
|
|
|
class RendererRegistry {
|
|
constructor() { this.renderers = new Map(); }
|
|
register(kind, renderer) {
|
|
if (typeof kind !== 'string' || typeof renderer !== 'function') throw new TypeError('Renderer registration requires a kind and a function');
|
|
this.renderers.set(kind, renderer);
|
|
return this;
|
|
}
|
|
render(conversationItem, context) {
|
|
const renderer = this.renderers.get(conversationItem.kind) || this.renderers.get('protocol-fallback');
|
|
if (!renderer) throw new Error(`No renderer registered for ${conversationItem.kind}`);
|
|
return renderer(conversationItem, context);
|
|
}
|
|
}
|
|
|
|
return { PROTOCOL_VERSION, MAX_PAYLOAD_BYTES, ConversationItemFactory, RendererRegistry, isObject };
|
|
});
|