uos1231234's picture
agent-shell published to Hugging Face
06c3755
Raw
History Blame Contribute Delete
9.15 kB
// v0.20 rendering base: the silent rendering service.
//
// §5.3 of v0.20 plan. The base is the single built-in subscriber to the
// RenderingSignalBus. It renders markdown → HTML, stores artifacts, and
// exposes a frontend-facing interface (onArtifact events)。
//
// Dependency direction (C1): signal-bus → base. Base does NOT import
// loop / registry / protocol. It only imports:
// - renderMarkdown (pure function)
// - ArtifactStore (content-addressed store)
// - RenderingSignalBus type (subscription)
// - Logger
//
// v0.24: startPreviewServer 已注释掉(用户拍板 2026-09-07)。理由:信号关是
// 唯一前后端中转站——前端经 artifact.get 命令取 HTML(Bearer 鉴权 + 会话边界),
// preview server 若启用会成为 gate 之外的第二条无鉴权数据面(/list 一览全部
// 产物、无会话隔离)。观测需求由 webapp ArtifactTab(iframe srcDoc)与
// examples 的写文件形态覆盖。实现保留在下方注释中备查,禁止在宿主装配中启用。
// import { createServer, type Server } from 'node:http'
import { renderMarkdown } from './render-md.js'
import { ArtifactStore } from '../im/tools/artifact-store.js'
import type { RenderingSignalBus, ArtifactSignal } from './signal-bus.js'
import type { Logger } from '../shared/logger.js'
export type ArtifactHandle = {
id: string
kind: 'markdown' | 'html'
title: string
source: string
at: number
}
export type RenderingBase = {
/** Render md directly (non-signal path); returns handle and broadcasts. */
renderMarkdown(md: string, meta: { title: string; source: string }): ArtifactHandle
/** Snapshot of all handles in creation order. */
handles(): readonly ArtifactHandle[]
/** Frontend subscriber; returns unsubscribe. */
onArtifact(cb: (h: ArtifactHandle) => void): () => void
/** Get stored HTML by artifact id. */
getHtml(id: string): string | undefined
/**
* v0.21: 公开的 signal 处理入口——信号关的 rendering 接线器经此转发
* ArtifactSignal(bus → Gate → base 顺序)。autoSubscribe:false 时这是
* 唯一的信号入口。
*/
handleSignal(signal: ArtifactSignal): void
}
type ConsoleLike = {
warn(msg: string, fields?: Record<string, unknown>): void
info(msg: string, fields?: Record<string, unknown>): void
}
export function createRenderingBase(
bus: RenderingSignalBus,
opts?: {
logger?: Logger
store?: ArtifactStore
/**
* v0.21: 是否自动订阅 bus(默认 true,向后兼容)。信号关装配传 false,
* 由 Gate 的 rendering 接线器经 handleSignal 转发(bus → Gate → base)。
*/
autoSubscribe?: boolean
},
): RenderingBase {
const store: ArtifactStore = opts?.store ?? new ArtifactStore()
const logger: ConsoleLike =
opts?.logger ?? {
warn: (msg, fields) => console.warn(msg, fields ?? ''),
info: (msg, fields) => console.info(msg, fields ?? ''),
}
const handles: ArtifactHandle[] = []
const subscribers: Array<(h: ArtifactHandle) => void> = []
// v0.20 P3 fix: 去重检查。重复 emit 同 id 的 signal 不会产生重复 handle。
const hasHandle = (id: string): boolean => handles.some((h) => h.id === id)
const broadcast = (h: ArtifactHandle): void => {
for (const cb of subscribers) {
try {
cb(h)
} catch (e) {
logger.warn('rendering: onArtifact subscriber threw', {
id: h.id,
error: e instanceof Error ? e.message : String(e),
})
}
}
}
/** Core pipeline: render → store → handle → broadcast. */
const pipeline = (md: string, meta: { title: string; source: string }): ArtifactHandle => {
const html = renderMarkdown(md)
const id = store.put(html)
// v0.20 P3 fix: 去重。同 id 的 handle 已存在则跳过 push(避免重复 emit 产生重复项)。
if (hasHandle(id)) {
return handles.find((h) => h.id === id)!
}
const handle: ArtifactHandle = {
id,
kind: 'markdown' as const,
title: meta.title,
source: meta.source,
at: Date.now(),
}
handles.push(handle)
broadcast(handle)
return handle
}
// v0.21: signal 处理逻辑提为公开入口 handleSignal——信号关(Signal Gate)
// 的 rendering 接线器经它转发(bus → Gate → base,用户拍板的顺序)。
// autoSubscribe 默认 true 保持既有行为(base 直连 bus);信号关装配时
// 以 autoSubscribe:false 创建 base,由 Gate 侧转发。
const handleSignal = (signal: ArtifactSignal): void => {
if (signal.kind === 'markdown') {
try {
pipeline(signal.content, { title: signal.title, source: signal.source })
} catch (e) {
logger.warn('rendering: render failed', {
kind: signal.kind,
error: e instanceof Error ? e.message : String(e),
})
}
return
}
// artifact-ref: fetch stored html, construct handle, broadcast.
try {
const html = store.get(signal.artifactId)
// v0.20 P3 fix: 去重。同 id 的 handle 已存在则跳过 push。
if (hasHandle(signal.artifactId)) {
return
}
const handle: ArtifactHandle = {
id: signal.artifactId,
kind: 'html' as const,
title: signal.title,
source: signal.source,
at: Date.now(),
}
handles.push(handle)
broadcast(handle)
} catch (e) {
logger.warn('rendering: artifact-ref lookup failed', {
artifactId: signal.artifactId,
error: e instanceof Error ? e.message : String(e),
})
}
}
if (opts?.autoSubscribe !== false) {
// Subscribe to signals — base is the single built-in subscriber.
bus.onSignal(handleSignal)
}
const renderMarkdownMethod = (
md: string,
meta: { title: string; source: string },
): ArtifactHandle => pipeline(md, meta)
const handlesMethod = (): readonly ArtifactHandle[] => [...handles]
const onArtifact = (cb: (h: ArtifactHandle) => void): (() => void) => {
subscribers.push(cb)
return () => {
const idx = subscribers.indexOf(cb)
if (idx >= 0) subscribers.splice(idx, 1)
}
}
const getHtml = (id: string): string | undefined => {
try {
return store.get(id)
} catch {
return undefined
}
}
// v0.24: startPreviewServer 注释掉(用户拍板 2026-09-07)——见文件头说明。
// 原实现(备查,勿启用):被动只读 HTTP 端点 /healthz + /list + /artifact/:id,
// 无 Bearer、无会话隔离、绕过信号关。若未来确需 URL 形态的产物分享,必须
// 先补鉴权与会话边界并经用户重新拍板。
//
// const startPreviewServer = (sopts?: { port?: number; host?: string }): Promise<{
// url: string
// close(): Promise<void>
// }> => {
// const host = sopts?.host ?? '127.0.0.1'
// const port = sopts?.port ?? 0
// const server: Server = createServer((req, res) => {
// const url = req.url ?? '/'
// // GET /healthz
// if (url === '/healthz') {
// res.writeHead(200, { 'content-type': 'text/plain' })
// res.end('ok')
// return
// }
// // GET /list
// if (url === '/list') {
// res.writeHead(200, { 'content-type': 'application/json' })
// res.end(JSON.stringify(handles))
// return
// }
// // GET /artifact/:id — charset 必须显式声明:无 charset 时浏览器按本地
// // 默认编码(中文 Windows = GBK)解码 UTF-8 字节,中文全部变乱码。
// const m = /^\/artifact\/([a-f0-9]+)$/.exec(url)
// if (m) {
// const id = m[1]!
// const html = getHtml(id)
// if (html === undefined) {
// res.writeHead(404, { 'content-type': 'text/plain' })
// res.end('not found')
// return
// }
// res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' })
// res.end(html)
// return
// }
// res.writeHead(404, { 'content-type': 'text/plain' })
// res.end('not found')
// })
//
// return new Promise((resolve, reject) => {
// server.on('error', reject)
// server.listen(port, host, () => {
// const addr = server.address()
// const actualPort =
// addr !== null && typeof addr === 'object' ? addr.port : port
// const url = `http://${host}:${actualPort}/`
// logger.info('rendering: preview server start', { url })
// const close = (): Promise<void> =>
// new Promise((resolveClose, rejectClose) => {
// server.close((err) => {
// if (err) rejectClose(err)
// else {
// logger.info('rendering: preview server closed', { url })
// resolveClose()
// }
// })
// })
// resolve({ url, close })
// })
// })
// }
return {
renderMarkdown: renderMarkdownMethod,
handles: handlesMethod,
onArtifact,
getHtml,
handleSignal,
}
}