Appearance
AI 对话界面(5 分钟接入)
SDK 内置了一个零依赖的 AI 对话组件(SDK 2.0 大改版):完整 markdown 渲染、 流式逐字输出、停止/重试/重新生成、逐条复制、头像与时间戳、深浅色主题、 中英文文案——一行代码挂到页面里。
组件有三个通道(channel 选项,默认 auto 每轮自动挑最好的):
| 通道 | 对话来自 | 什么时候用 |
|---|---|---|
agent | 客户端内置 Agent(jlt.agent,桌面端) | 页面跑在机灵兔客户端里时自动选用——无需自建后端,Agent 可用用户启用的技能/系统工具/本地数据库,工具调用、本机执行、子任务、生成文件全部实时渲染成卡片 |
endpoint | 你自己的后端中继 | 浏览器联调/移动端的默认路径。页面拿不到 app_secret,模型调用必须走你的后端,中继凭 app token 调平台 /api/open/ai/completions(消耗应用所属组织的钱包,需 ai 能力) |
preview | 本地罐头回复 | 什么都不配时的兜底——零后端演示界面 |
auto 的决策:客户端内(有 window.jlt)→ agent;否则有 endpoint → 中继; 再无 → 预览。agent 不可用(移动端/未授权 ai+local)时自动降级到中继并在 界面上提示,降级过一次即闩锁(后续轮次直达中继,不再反复等桥超时); agentFallback: "error" 可改为直接报错。
1. 后端中继(Express,约 15 行)
js
// 中继契约:POST {messages, stream} → 原样转发平台响应(SSE 透传)
app.post("/api/chat", async (req, res) => {
const upstream = await fetch(`${JLT_BASE}/ai/completions`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${await appToken()}`, // 见「完整示例」的 token 缓存
},
body: JSON.stringify({ messages: req.body.messages, stream: req.body.stream !== false }),
});
if (!upstream.ok) return res.status(502).json({ detail: (await upstream.text()).slice(0, 300) });
if (req.body.stream !== false) {
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Cache-Control", "no-cache");
for await (const chunk of upstream.body) res.write(chunk);
return res.end();
}
res.json(await upstream.json());
});中继是无状态的:组件每次带全量 messages,你不需要存对话。
2. 页面挂载(一行)
html
<script src="https://cos-pub.smabbit.com/open-platform/sdk/2.0.0/jlt-sdk.min.js"></script>
<div id="chat" style="height: 560px"></div>
<script>
const chat = jltSdk.mountChat(document.getElementById("chat"), {
channel: "auto", // 客户端内用 Agent,浏览器走中继(默认值可省略)
endpoint: "/api/chat", // 中继地址(channel 解析为 endpoint 时必需)
});
</script>npm 引入则是 import { mountChat } from "@ai-im/org-app-sdk"。完成——输入、 流式渲染、停止(发送中再点一次)、失败重试、重新生成、逐条复制、清空都已内置。
3. 常用配置
ts
mountChat(el, {
// ── 通道 ──
channel: "auto", // "auto" | "agent" | "endpoint" | "preview"
endpoint: "/api/chat",
model: () => modelSelect.value, // agent 通道模型 id(函数每轮取值;缺省客户端默认)
agentFallback: "endpoint", // agent 不可用时降级中继("error" 只报错)
// ── 展示 ──
title: "报销助手", // 顶部标题
welcome: "把报销单贴给我,我帮你核对", // 欢迎语(传 "" 关闭)
placeholder: "输入问题…",
theme: "auto", // "auto" | "light" | "dark"
locale: "zh-CN", // "zh-CN" | "en"
userAvatar: ctx.user.avatar_url, // 头像 URL(默认关闭;assistantAvatar 同)
timestamps: true, // 显示消息时间戳(默认 false)
// ── endpoint 通道 ──
systemPrompt: "你是报销审核助手,回答简洁。", // 每轮动态前置,不进 UI 历史(对 agent 通道无效——其 system 由受信壳注入)
headers: () => ({ "x-session-id": sid }), // 额外请求头;传函数则每轮解析(token 会轮换的场景)
stream: true, // 后端不支持 SSE 时设 false
buildBody: (messages) => ({ messages, stream: true, temperature: 0.3 }), // 完全自定义请求体
// ── 行为 ──
maxHistory: 40, // 每次请求最多携带的历史条数(agent 壳上限同为 40)
timeoutMs: 120_000, // 单轮上限(默认不限;到点中止,agent 通道同时 agentStop)
onError: (error) => console.warn(error.kind, error.message), // 结构化错误(见下)
onChannelChange: ({ channel, downgradedFrom }) => updateBadge(channel), // 通道变化(连接状态展示)
});返回句柄:chat.stop()(中止当前生成)、chat.retry()(重发失败轮)、 chat.regenerate()(弹出末条回答重跑本轮)、chat.clear()(清空)、 chat.send(text)(编程式发送)、chat.busy(是否生成中)、chat.destroy()。
onError 的载荷(2.0 起是结构化对象,1.x 是 string——迁移只需改读 error.message):
ts
interface ChatError {
kind: "http" | "network" | "timeout" | "aborted" | "agent" | "config" | "parse";
message: string; // 已本地化,可直接展示
status?: number; // http 状态码
channel?: "agent" | "endpoint" | "preview";
}agent 通道的授权与直接调 jlt.agent 一致:需同时被授予 ai + local 能力,每轮开始前桌面壳弹原生确认(任务全文展示,v1.6.1 起不提供「始终允许」); 应用发起的本机命令按危险度分级确认(普通命令可「本轮不再询问」、任务结束即失效; 危险/外发类永不豁免;均不沿用客户端本机 Agent 的 danger_only/信任签名/静默宽免), 本机工具走白名单(代码执行类能力自动跑在云端无网络 Docker 沙箱而非用户机器), 且不开放走用户登录态浏览器的 browser_* 工具。组件提交给 Agent 的 messages 只保留 user/assistant(system 注入 尝试被壳剥离),token/IPC 不进页面。计费扣的是触发它的组织成员自己的额度 (个人/组织钱包精确归因,不是应用的钱),同时会打上归属于本应用的标签, 计入该应用在组织看板与 GET /usage 里的消耗统计。
3.5 会话持久化:存哪里?
initialHistory 恢复 + onHistoryChange 存档(载荷是 ChatEntry[]:在 {role, content} 之外可能带 agent 产物 files/steps 与时间戳 ts), 存储位置你自己挑,三档:
① IndexedDB(内置助手,推荐起步)——配额通常数百 MB 起,长会话/多会话 都放得下;SDK 自带零依赖的 createChatStore(自带预算裁剪、串行异步写入、 错误只警告不炸聊天、隐私模式等不可用环境自动降级为空):
ts
const store = jltSdk.createChatStore({ key: "main" }); // 多会话各用不同 key
jltSdk.mountChat(el, {
endpoint: "/api/chat",
initialHistory: await store.load(), // v1 时代的裸数组存档也兼容
onHistoryChange: store.save,
persistMaxChars: 4 * 1024 * 1024, // 放开组件侧载荷上限,匹配存储预算(默认 5MB)
});
await store.flush(); // 页面卸载前确保在途写入落盘
const sessions = await store.keys(); // 多会话列表(侧栏用)
await store.remove("old"); // 删除指定会话;store.clearAll() 清空整个库
// 组件「清空」会以空数组回调 → 存档自动清空;也可 await store.clear()② 你自己的后端(跨设备必选)——历史本是业务数据:不限量、多设备漫游、 可审计:
ts
mountChat(el, {
endpoint: "/api/chat",
initialHistory: await fetch("/api/chat-history").then((r) => r.json()).catch(() => []),
onHistoryChange: (entries) =>
fetch("/api/chat-history", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(entries) }),
});③ localStorage(仅轻量场景)——单源配额一般只有 ~5MB(Safari 还会更 激进地清理),长会话很容易写满。组件做了三层兜底,naive 用法不至于炸:
- 回调里的存储异常被吞掉(只
console.warn),写满不打断聊天; - 存档载荷有尺寸上限——
persistMaxChars(默认 256KB),超限自动从最旧 一轮裁起,至少保留最后一轮; - 内存历史也有上限——
historyLimit(默认 200 条,恢复存档时同样生效), 超长会话不会把页面内存拖爆。
ts
mountChat(el, {
endpoint: "/api/chat",
initialHistory: JSON.parse(localStorage.getItem("chat") || "[]"),
onHistoryChange: (entries) => localStorage.setItem("chat", JSON.stringify(entries)),
persistMaxChars: 128 * 1024, // localStorage 场景建议调小
});onHistoryChange 在每轮对话完成和清空后触发,systemPrompt 不在内(它本来 就不进历史)。createChatStore 的预算参数:maxChars(默认 5MB)、 maxMessages(默认 2000),超出从最旧一轮裁起。
4. 组合 JSAPI:让对话知道「谁在用」
ts
if (jltSdk.isInJltClient()) {
const ctx = await jltSdk.ready();
mountChat(el, {
endpoint: "/api/chat",
headers: { "x-user-id": ctx.user.user_id }, // 中继侧自行验真(不能只信头部!)
systemPrompt: `当前用户:${ctx.user.nickname}。`,
userAvatar: ctx.user.avatar_url,
});
}WARNING
headers 里的用户标识只是提示——中继必须用可信方式识别用户(例如先走 免登给自己发会话 cookie,再带 cookie 调中继),否则任何人都能 伪造别人的身份跟你聊天。
5. 想改样式?
两种方式,推荐前者:
① 主题变量(SDK 2.0)——组件全部颜色/圆角/字体挂在 --jltc-* 自定义 属性上,一处声明整体换肤:
css
#chat .jltc {
--jltc-accent: #10b981; /* 主色(发送按钮/链接/用户气泡) */
--jltc-bubble-user-bg: #10b981; /* 用户气泡背景(可以是渐变) */
--jltc-radius: 16px; /* 圆角 */
--jltc-font: inherit; /* 字体 */
}可用的变量:--jltc-bg / fg / muted / border / accent / accent-fg / accent-soft / bubble-user-bg / bubble-user-fg / bubble-assistant-bg / bubble-assistant-fg / error / success / warning / code-bg / code-fg / code-bar-bg / radius / radius-sm / font / font-mono / shadow。theme: "dark" 显式指定深色(默认 auto 跟随系统)。
② 类名覆盖(v1 兼容)——类名都以 jltc- 开头(jltc/jltc-list/ jltc-row user|assistant/jltc-bubble user|assistant/jltc-step/jltc-files/ jltc-code/jltc-input…),直接在页面里覆盖即可:
css
.jltc { border-radius: 16px; }
.jltc-bubble.user { background: #10b981; }