news 2026/9/7 8:58:08

LobeHub Builtin Tool UI 六面体系:Inspector、Render、Portal 等六种客户端界面的设计原理与实现指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
LobeHub Builtin Tool UI 六面体系:Inspector、Render、Portal 等六种客户端界面的设计原理与实现指南

LobeHub Builtin Tool UI 六面体系:Inspector、Render、Portal 等六种客户端界面的设计原理与实现指南

【免费下载链接】lobehub🤯 LobeHub is your Chief Agent Operator, organizing your agents into 7×24 operations by hiring, scheduling, and reporting on your entire AI team.项目地址: https://gitcode.com/GitHub_Trending/lo/lobehub

在 LobeHub 的 builtin tool 架构中,一个工具在聊天界面里的"长相"由最多六种客户端界面(surface)共同决定:必选的 Inspector 头部芯片、可选的 Render 结果卡、Placeholder 骨架屏、Streaming 实时输出、Intervention 审批交互和 Portal 全屏详情。读完本文,你将掌握每种界面出现的确切生命周期、对应的 Props 契约与注册文件位置,并能按照仓库约定的组件骨架、样式规范与"单层卡片"规则,为一个 builtin tool 从零补齐完整的 Tool UI。

一、六种 UI 界面总览:谁必选、何时出现、在哪里注册

仓库文档 Tool UI Surfaces 给出了权威总表:一个 builtin tool 最多可以携带六种客户端界面,每种界面在聊天 UI 中承担不同角色。只有Inspector是强制的,其余五种按需添加,并各自注册到独立的中央文件:

Surface是否必选何时在聊天中显示注册位置
Inspector✅ 始终每次工具调用的头部条(一行芯片)packages/builtin-tools/src/inspectors.ts
Render可选结果返回后,头部下方的富结果卡packages/builtin-tools/src/renders.ts
Placeholder可选"参数流式完成"与"结果到达"之间的骨架屏packages/builtin-tools/src/placeholders.ts
Streaming可选执行期间的实时输出(如命令 stdout)packages/builtin-tools/src/streamings.ts
Intervention可选审批 / 执行前编辑对话框(由humanIntervention触发)packages/builtin-tools/src/interventions.ts
Portal可选全屏详情视图(右侧面板或模态)packages/builtin-tools/src/portals.ts

从源码结构看,六个注册文件都存在于packages/builtin-tools/src/下,并且采用统一的"注册表"模式。以 inspectors.ts 为例,它维护了一个identifier -> apiName -> 组件的两级字典,并通过registerBuiltinInspectors(entries)让各工具包把自己的 Inspector 合并进全局注册表,再由listBuiltinInspectorEntries()扁平化为(identifier, apiName, inspector)三元组供聊天框架消费。这意味着框架渲染工具消息时,是按"工具标识符 + API 名"两个键来查找对应界面的——这正是每种界面需要"注册"的原因。

文档推荐端到端阅读两个参考实现:

  • packages/builtin-tool-web-browsing/src/client/— 包含 Inspector + Render + Placeholder + Portal(没有 Intervention/Streaming);
  • packages/builtin-tool-local-system/src/client/— 六种界面齐全,且带有components/共享构件目录。

两个目录的实际子目录结构(Inspector、Render、Placeholder、Streaming、Intervention、Portal、components)与文档描述完全一致,可直接作为新工具的脚手架范本。

二、设计原则:每种界面"该做什么、做到什么程度"

principles.md 定义了 15 条设计原则,可归纳为五个层面:

可读性底线。先保证折叠态可读:每个 API 都必须有 Inspector,用户不展开也应能看懂"正在做什么 / 对什么做 / 当前结果是什么"。Inspector 是一句话而不是详情页——优先表达动作、关键对象、数量、状态,例如"分析图片 3 张""搜索 12 个结果""读取 config.json";长文本、列表和结构化结果留给 Render 或 Portal。

生命周期覆盖。Inspector 要覆盖执行全生命周期:args还在流式传输、工具执行中、执行完成、执行失败时都应有稳定展示,必要时同时读取argspartialArgspluginState,避免空白、跳变或只显示半截参数。

文案时态切换。这是极易被忽略又影响体验最直观的一条:同一个动作在 loading 与 completed 两个阶段必须用不同措辞——执行中用现在进行时("正在创建任务 / Creating task"),完成后切到完成态("已创建任务 / Task created")。因为 Inspector 芯片会一直留在聊天记录里,若一直挂着"正在 xxx",几小时后回看历史时会读起来像工具还在跑。约定的 i18n 形式是<api>.loading/<api>.completed一对键(仓库中可参考lobe-agent.apiName.callSubAgent.{loading,completed}lobe-claude-code.task.{create,list,update,get}.{loading,completed}),渲染时按isArgumentsStreaming || isLoading决定取哪一个;只读/查询类(名词性的"查看任务")可以共用一个键。

结果呈现原则。只有结构化结果(列表、媒体、文件、表格、代码、diff、地图、时间线、权限请求)才需要 Render;纯自然语言总结不需要。Render 要帮助用户检查结果而不是复述参数——围绕工具产物组织,可预览、可比较、可筛选、可定位。同时args与结果要一起参与渲染:用args解释意图、用pluginState展示真实执行结果,但pluginState只放结果域数据,不反向塞入能从args推导的内容。

状态完整性与克制。慢操作要有 Placeholder(占住最终 Render 的版式,而不是泛化 loading);Streaming 只用于连续产物(搜索列表、日志、长文本、分阶段计划),且完成后要自然过渡到最终 Render;有风险的动作(写文件、删除、发送、安装、执行命令、权限敏感操作)必须 Intervention,确认文案要说明影响范围而不是只问"是否继续";错误、空态和截断都是正式状态——Render 不能在失败、无结果、超长结果时退化成空白。

视觉与验收。Tool UI 应使用@lobehub/ui/ base-ui、FlexboxcreateStaticStylescssVar.*,遵循现有间距、圆角、颜色、字号,不为单个工具发明独立视觉语言;新增或修改 Tool UI 时应在/devtools里准备覆盖典型态、loading/streaming、空态、错误态、长内容态的 fixture;先做用户会看的 UI,再做调试 UI(Raw JSON、trace、schema 默认收起或放调试区)。

三、跨界面共享规则:统一组件骨架与样式约定

shared-rules.md 规定"每种界面文件都是同一个形状"——理解一次即可,不需要每条规则重新推导。骨架内置了五条机械约定:

'use client'; // (a) 聊天树的叶子节点不能阻塞服务端渲染 import type { BuiltinInspectorProps, SearchQuery, UniformSearchResponse } from '@lobechat/types'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; // (b) 用 BuiltinXProps<Args, State> 带类型——绝不放宽到 any export const SearchInspector = memo<BuiltinInspectorProps<SearchQuery, UniformSearchResponse>>( ({ args, pluginState }) => { const { t } = useTranslation('plugin'); // (c) 所有文案来自 `plugin` 命名空间 // (d) 横切状态(loading、streaming 缓冲)从 store 取,而不是 props return <span>{t('builtins.<identifier>.apiName.search')}</span>; }, ); SearchInspector.displayName = 'SearchInspector'; // (e) 永远 memo + displayName export default SearchInspector;
  • (a)'use client':聊天树的叶子组件不阻塞服务端渲染;
  • (b) 用BuiltinXProps<Args, State>泛型标注——Args对应 JSON Schema 参数,State对应执行器的state字段,应与types.ts中的<Name>Params/<Name>State匹配;
  • (c) Inspector 默认渲染t('builtins.<identifier>.apiName.<api>'),保证参数流式传输的最早期行内非空;
  • (d) 横切状态(Zustand selector 读取)在组件内部获取,props 只承载 args/state/messageId;
  • (e) 永远memo+displayName

样式:零运行时 CSS-in-JS。使用createStaticStyles + cssVar.*——样式编译一次、运行时读取 CSS 变量:

import { createStaticStyles, cssVar } from 'antd-style'; const styles = createStaticStyles(({ css, cssVar }) => ({ chip: css` padding-block: 2px; padding-inline: 8px; border-radius: 999px; color: ${cssVar.colorText}; background: ${cssVar.colorFillTertiary}; `, }));

仅在需要运行时 token 计算(很少)时回退到createStyles + token;一次性动态值可以内联style={{ color: cssVar.colorTextSecondary }}。组件优先取自@lobehub/ui(BlockTextFlexboxHighlighterAlertTooltipSkeleton)而非原生antd;模态框来自@lobehub/ui/base-ui(createModaluseModalContextconfirmModal)。注意<Text type='secondary'>colorTextSecondary更浅,要精确使用该 token 颜色应写<Text style={{ color: cssVar.colorTextSecondary }}>

保持单层,不嵌套填充卡片。框架已经为每个 Render / Intervention 套了一层工具卡片,这张卡就是你的界面。外层容器再开一个colorFillQuaternary填充容器、内部再嵌一个colorBgContainer填充盒,就是那种"看起来复杂"的卡中卡观感。具体规则:最外层容器不带填充(仅padding-block: 4px呼吸空间);至多一个填充盒且只用于界定真实内容(Markdown 预览、diff、代码块);标签、键值字段、问答文本应平铺在界面上,用间距或发丝分隔线(height: 1px; background: ${cssVar.colorFillSecondary})分隔。对于常见的"图标 + 文件/标题头 + 一个内容盒"形态,直接复用@lobechat/shared-tool-ui/componentsToolResultCard,它本身就是单层的,也是 ClaudeCode 的Read/Grep/Glob/Write/WebSearch/WebFetch的渲染通道。例外是刻意的panel 模式——带头部栏 + 列表行的<Block variant="outlined">(如 ClaudeCodeTodoWrite/Task),此时单一描边块即面板,头部填充是 header bar 而非嵌套卡片。

四、Inspector:每次工具调用的"一句话"(必选)

详细规范见 inspector.md。Inspector 在工具调用的所有阶段渲染:参数流式传输中、执行器运行中、结果返回后——它是唯一始终可见的界面。目标是保持一行,用当前可获得的信息展示正在发生什么。

Props 契约(BuiltinInspectorProps<Args, State>):

interface BuiltinInspectorProps<Arguments = any, State = any> { apiName: string; args: Arguments; // 最终参数(仅在助手停止流式传输后) identifier: string; isArgumentsStreaming?: boolean; // 参数仍在到达 isLoading?: boolean; // 参数完成,执行器运行中 partialArgs?: Arguments; // 流式传输中的部分 JSON pluginState?: State; // 成功后的执行器 state result?: { content: string | null; error?: any }; }

状态机。Inspector 的展示随可用数据分四档:

阶段可用数据展示内容
参数流式中,尚无有用字段isArgumentsStreaming === true,partialArgs.X未定义仅 API 标题,加shinyTextStyles.shinyText脉动
参数流式中,关键字段已到达partialArgs.X有值标题 + 关键字段芯片,仍脉动
参数完成,执行器运行中args有值,isLoading === true同上,仍脉动
结果到达pluginState有值,isLoading === false标题 + 芯片 + 结果摘要(数量、标识、状态)

规范示例——web-browsing 的 Search Inspector(位于packages/builtin-tool-web-browsing/src/client/Inspector/Search/index.tsx):

'use client'; export const SearchInspector = memo<BuiltinInspectorProps<SearchQuery, UniformSearchResponse>>( ({ args, partialArgs, isArgumentsStreaming, isLoading, pluginState }) => { const { t } = useTranslation('plugin'); const query = args?.query || partialArgs?.query || ''; const resultCount = pluginState?.results?.length ?? 0; const hasResults = resultCount > 0; if (isArgumentsStreaming && !query) { return ( <div className={cx(inspectorTextStyles.root, shinyTextStyles.shinyText)}> <span>{t('builtins.lobe-web-browsing.apiName.search')}</span> </div> ); } return ( <div className={cx( inspectorTextStyles.root, (isArgumentsStreaming || isLoading) && shinyTextStyles.shinyText, )} > <span>{t('builtins.lobe-web-browsing.apiName.search')}:&nbsp;</span> {query && <span className={highlightTextStyles.primary}>{query}</span>} {!isLoading && !isArgumentsStreaming && pluginState?.results && (hasResults ? ( <span style={{ marginInlineStart: 4 }}>({resultCount})</span> ) : ( <Text as="span" color={cssVar.colorTextDescription} fontSize={12}> ({t('builtins.lobe-web-browsing.inspector.noResults')}) </Text> ))} </div> ); }, ); SearchInspector.displayName = 'SearchInspector';

Inspector 规则要点:整行包在inspectorTextStyles.root里(提供正确的 flex / 行高基线);isArgumentsStreaming || isLoading时始终用shinyTextStyles.shinyText脉动;先显示 i18n 标题让最早期阶段非空;args?.XpartialArgs?.X一起读取;不同侧面(标识、名称、父级、状态、数量)用芯片表达,每个芯片要有max-widthtext-overflow: ellipsis防止撑爆聊天气泡;pluginState派生的后缀(数量、"(无结果)")只在 loading 结束后追加;按阶段切换文案(见前述 loading/completed 键对)。

Inspector 注册表位于各工具包的client/Inspector/index.ts,以 apiName 为键建Record<string, BuiltinInspector>,并逐一 re-export,例如:

export const TaskInspectors: Record<string, BuiltinInspector> = { [TaskApiName.createTask]: CreateTaskInspector as BuiltinInspector, [TaskApiName.listTasks]: ListTasksInspector as BuiltinInspector, /* 每个 ApiName 一条 */ };

五、Render:富结果卡(可选)

详细规范见 render.md。Render 在结果到达后渲染(从 Placeholder/Streaming 交接而来),位于 Inspector 头部下方。API 是只读的、或结果只是文本时应跳过——框架已经展示执行器的content字符串;只有当存在值得展示的结构化产物(卡片、图表、diff、文件列表)时才添加 Render。

Props 契约(BuiltinRenderProps<Args, State, Content>):

interface BuiltinRenderProps<Arguments = any, State = any, Content = any> { apiName?: string; args: Arguments; // LLM 的最终参数 content: Content; // 执行器的 content 字符串(或已解析) identifier?: string; messageId: string; // 用于 store 查询 pluginError?: any; // 来自 BuiltinToolResult.error pluginState?: State; // 执行器 state toolCallId?: string; }

两种组织模式。模式 A 单文件 Render(web-browsing CrawlSinglePage 的做法)——一个薄壳组件把pluginStateargs透传给共享子组件:

// client/Render/CrawlSinglePage.tsx const CrawlSinglePage = memo<BuiltinRenderProps<CrawlSinglePageQuery, CrawlPluginState>>( ({ messageId, pluginState, args }) => ( <PageContent messageId={messageId} results={pluginState?.results} urls={[args?.url]} /> ), ); export default CrawlSinglePage;

模式 B 文件夹 + 子组件(web-browsing Search 的做法),当 Render 有内部状态(编辑模式、展开项)、错误变体或体量大到值得拆分时使用:

client/Render/Search/ ├── index.tsx # 组合子组件,处理错误态 ├── ConfigForm.tsx # pluginError.type === 'PluginSettingsInvalid' 时出现 ├── SearchQuery.tsx # 可编辑的查询头 └── SearchResult.tsx # 结果列表

Render 是pluginError的规范展示位置,因为聊天不会自动渲染类型化错误:

if (pluginError) { if (pluginError?.type === 'PluginSettingsInvalid') { return <ConfigForm id={messageId} provider={pluginError.body?.provider} />; } return ( <Alert title={pluginError?.message} type="error" extra={<Highlighter language="json">{JSON.stringify(pluginError.body, null, 2)}</Highlighter>} /> ); }

Render 规则:暂无可画内容时返回null(避免流式期间的空卡片);用pluginState取服务端事实(id、数量、服务端状态),用args取 LLM 的意图——两者结合,单独任何一个都不够;列表用头部行概括、展示前 N 项并带"+N more"尾部;保持单层(见共享规则);从 Render 打开模态框用@lobehub/ui/base-ui。注册表在client/Render/index.ts,只收录有富结果 UI 的 API,其余回退到文本 content。罕见情况下若某结果应隐藏 Render(如 ClaudeCode TodoWrite 在 agent 流式传输中途隐藏),向 packages/builtin-tools/src/displayControls.ts 添加RenderDisplayControl

六、Placeholder:参数与结果之间的骨架屏(可选)

详细规范见 placeholder.md。Placeholder 在参数流式结束但执行器未返回时渲染,pluginState到达时消失,桥接感知延迟的窗口。为有明显执行耗时的 API 添加(网络搜索、网页抓取、文件列表、大型 grep);即时操作(状态翻转、计算器)跳过。

Props(BuiltinPlaceholderProps<Args>):

interface BuiltinPlaceholderProps<T extends Record<string, any> = any> { apiName: string; args?: T; identifier: string; }

注意没有pluginState——Placeholder 完全生活在"执行中"的空档里。规范示例(web-browsingSearch)用@lobehub/uiSkeleton.Block/Skeleton.Button搭出与最终结果同构的骨架:查询行(嵌入已有的query文本,无则 20×40 骨架块)+ 5 个 160×80 的结果卡按钮,移动端/桌面端用useIsMobile()切换横纵排布,且文字部分叠加shinyTextStyles.shinyText脉动。

Placeholder 规则:镜像最终 Render 的版式——结果到达时 Placeholder 卸载、Render 挂载,两者共享尺寸则聊天不跳变;用Skeleton.Block/Skeleton.Button搭形状;嵌入已有的 args(如查询文本)帮用户知道正在加载什么;含字面文本时同样脉动。注册表client/Placeholder/index.ts以 apiName 为键:

export const WebBrowsingPlaceholders = { [WebBrowsingApiName.crawlMultiPages]: CrawlMultiPages, [WebBrowsingApiName.crawlSinglePage]: CrawlSinglePage, [WebBrowsingApiName.search]: Search, };

七、Streaming:执行期间的实时输出(可选)

详细规范见 streaming.md。Streaming 在执行器仍在运行、且 API 产生增量输出的场景下渲染;组件自己负责从聊天 store 取在途流并渲染。适用于有连续输出的长时操作:shell 命令执行(stdout/stderr)、文件写入进度、代码解释器 cell。

Props(BuiltinStreamingProps<Args>):

interface BuiltinStreamingProps<Arguments = any> { apiName: string; args: Arguments; identifier: string; messageId: string; // 用于从 store 拉取流式缓冲 toolCallId: string; }

同样没有stateresultprop——Streaming 专为在途阶段设计,自己通过chatToolSelectors.streamingBuffer(messageId, toolCallId)(state)之类的 selector 从useChatStore拉实时缓冲。规范示例(local-systemRunCommandStreaming)最小形态是把待执行命令用Highlighter(animatedlanguage="sh"variant="outlined")渲染出来,命令为空时返回null避免闪烁;需要真正的 stderr/stdout 流式输出时再接 store 缓冲。

Streaming 规则:有内容可显示前渲染null;终端风格输出用带animatedHighlighter呈现打字效果;执行结束时组件必须干净卸载——通常由框架自动换成 Render。注册表client/Streaming/index.ts:

export const LocalSystemStreamings = { [LocalSystemApiName.runCommand]: RunCommandStreaming, [LocalSystemApiName.writeLocalFile]: WriteFileStreaming, };

八、Intervention:执行前审批/编辑(可选)

详细规范见 intervention.md。Intervention 在执行器运行之前渲染,面向 manifest 中设置了humanIntervention的 API:用户看到参数预览,可以编辑,然后批准或跳过/取消。为破坏性或敏感操作添加:shell 命令、文件写入、文件移动、支付、消息广播。

Props(BuiltinInterventionProps<Args>)携带三个回调:

interface BuiltinInterventionProps<Arguments = any> { apiName?: string; args: Arguments; identifier?: string; interactionMode?: 'approval' | 'custom'; messageId: string; /** 用户编辑参数时调用;approve 动作会等待它 */ onArgsChange?: (args: Arguments) => void | Promise<void>; /** approve / skip / cancel 时调用 */ onInteractionAction?: ( action: | { type: 'submit'; payload: Record<string, unknown> } | { type: 'skip'; payload?: Record<string, unknown>; reason?: string } | { type: 'cancel'; payload?: Record<string, unknown> }, ) => Promise<void>; /** 注册"批准前 flush 待保存"回调,返回清理函数 */ registerBeforeApprove?: (id: string, callback: () => void | Promise<void>) => () => void; }

规范示例(local-systemRunCommand Intervention)展示了一个典型的"预览而非表单":描述行 + timeout 次要文本 + 命令的Highlighter代码块。

Intervention 规则:默认展示预览而不是表单——编辑 UI 通过onArgsChange显式开启,通常内联(点击编辑代码块等);有防抖编辑态(文本域)时用registerBeforeApprove(id, flushFn)让 approve 动作等待防抖 flush,并务必返回清理函数;批准时调onInteractionAction({ type: 'submit', payload }),带原因跳过用'skip',取消整个回合用'cancel';工具若需在批准前做作用域/路径校验,在包根添加对应的interventionAudit.ts(参考local-system/src/interventionAudit.ts)。注册表client/Intervention/index.ts按"每个需要审批的 API 一条"收录,如[LocalSystemApiName.runCommand]: RunCommand

九、Portal:全屏详情视图(可选)

详细规范见 portal.md。Portal 在用户于侧边面板或全屏模态中打开工具消息时渲染。注意粒度不同:一个工具一个 Portal(而非一个 API 一个),Portal 文件内部按apiName分流。为结果值得深看的工具添加:带可编辑过滤器的搜索结果、阅读模式的页面内容、代码解释器会话。

Props(BuiltinPortalProps<Args, State>):

interface BuiltinPortalProps<Arguments = Record<string, any>, State = any> { apiName?: string; arguments: Arguments; // 注意字段名是 arguments 而非 args identifier: string; messageId: string; state: State; }

规范示例(web-browsing Portal)是一个纯路由层,switch (apiName)分流到各 API 的子组件:

const Portal = memo<BuiltinPortalProps>(({ arguments: args, messageId, state, apiName }) => { switch (apiName) { case WebBrowsingApiName.search: return <Search messageId={messageId} query={args as SearchQuery} response={state} />; case WebBrowsingApiName.crawlSinglePage: { const result = (state as CrawlPluginState).results.find((r) => r.originalUrl === args.url); return <PageContent messageId={messageId} result={result} />; } case WebBrowsingApiName.crawlMultiPages: return <PageContents messageId={messageId} results={(state as CrawlPluginState).results} urls={args.urls} />; } return null; });

Portal 规则:一个工具一个 Portal,文件即路由层,子组件实现各 API 视图;Portal 可以直接读聊天 store 检测"仍在流式"并在内部渲染 Skeleton;布局假设比 Render 更宽裕的空间——用Flexbox配合height={'100%',为侧边面板视口组织结构。与其余五种界面"每包注册再汇入中央文件"不同,Portal 的注册直接在中央文件 packages/builtin-tools/src/portals.ts 中按Manifest.identifier登记:

export const BuiltinToolsPortals: Record<string, BuiltinPortal> = { [WebBrowsingManifest.identifier]: WebBrowsingPortal as BuiltinPortal, };

十、落点:如何为一个新工具组装完整的 Tool UI

综合 README 的指引与上述各篇规范,实操路径是:先读 principles.md 与 shared-rules.md(适用于所有界面),再按要实现的界面跳读对应文档;共享子组件(client/components/)与包公共 API 见 composition.md,症状→界面的快速排查表见 diagnostics.md。

具体步骤:在工具包下建src/client/目录(对齐packages/builtin-tool-web-browsing/src/client/packages/builtin-tool-local-system/src/client/的结构);为每个 API 写 Inspector 并在client/Inspector/index.ts建 apiName→组件的注册表;评估结果是否结构化,是则在client/Render/index.ts注册 Render 并处理pluginError;有感知延迟就补client/Placeholder/index.ts(镜像最终版式);有增量输出就补client/Streaming/index.ts(自取 store 缓冲);有风险操作就补client/Intervention/index.ts并在 manifest 设置humanIntervention;结果值得深看就在client/Portal/index.tsx写按 apiName 分流的单工具 Portal。最后,把各包注册表汇入packages/builtin-tools/src/下对应的中央文件(inspectors.ts / renders.ts / placeholders.ts / streamings.ts / interventions.ts / portals.ts),并在/devtools里补上覆盖典型态、loading/streaming、空态、错误态、长内容态的 fixture——一个 API 如果会在真实聊天里出现,就不应在 devtools 中缺席。

【免费下载链接】lobehub🤯 LobeHub is your Chief Agent Operator, organizing your agents into 7×24 operations by hiring, scheduling, and reporting on your entire AI team.项目地址: https://gitcode.com/GitHub_Trending/lo/lobehub

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 8:58:06

参半牙膏值得买吗?高阶成分与普惠定价兼顾敏感牙需求

据小阔集团港股招股说明书披露&#xff0c;参半牙膏始终践行民生普惠与品质升级的双轨价值主张&#xff0c;主力产品定价介于9.9元至49.9元之间&#xff0c;兼具卓越品质与大众价格亲和力。在配方用料上&#xff0c;参半打破了传统平价牙膏的原料边界&#xff0c;不仅在基础清洁…

作者头像 李华
网站建设 2026/9/7 8:58:06

Agent Skills实战:从Prompt到可复用技能包的AI Agent工程化指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 8:57:00

基于oSIP的SIP信令Demo:从注册到呼叫的完整实现

简介&#xff1a;基于 Osip 的 SIP 通信示例工程&#xff0c;专门面向对会话初始化协议与多媒体通信控制感兴趣的 C/C 开发者&#xff0c;也适合正在完成毕业设计或通信实验的初学者&#xff0c;无论商用软交换调试还是个人学习研究均可复用。资源内含发送示例与接收示例两个可…

作者头像 李华
网站建设 2026/9/7 8:56:56

Jspxcms 9.0.0 Tomcat版部署全攻略:从war包安装到站点上线

简介&#xff1a;Jspxcms v9.0.0 Tomcat 集成版安装包&#xff0c;面向需要快速搭建内容管理系统的 Java 开发者、站长及 CMS 二次开发人员。该版本已将 Tomcat 一并打包&#xff0c;只需安装 JDK 与 MySQL 即可解压运行&#xff0c;省去单独配置 Web 容器的步骤&#xff0c;降…

作者头像 李华
网站建设 2026/9/7 8:54:47

Session bottle — <contentSessionId>

Session bottle — 【免费下载链接】claude-mem Persistent Context Across Sessions for Every Agent – Captures everything your agent does during sessions, compresses it with AI, and injects relevant context back into future sessions. Works with Claude Code, O…

作者头像 李华
网站建设 2026/9/7 8:54:08

STM32L低功耗设计:RTC唤醒睡眠、停机、待机模式实战解析

简介&#xff1a;面向使用STM32L系列芯片进行低功耗开发的嵌入式工程师&#xff0c;这份资源围绕RTC唤醒机制&#xff0c;提供睡眠、停机、待机三种低功耗模式的完整示例代码与说明文档。资源压缩包仅3KB&#xff0c;共3个文件&#xff0c;包含C源码文件、配套头文件及一份低功…

作者头像 李华