Skip to content

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 用法不至于炸:

  1. 回调里的存储异常被吞掉(只 console.warn),写满不打断聊天;
  2. 存档载荷有尺寸上限——persistMaxChars(默认 256KB),超限自动从最旧 一轮裁起,至少保留最后一轮;
  3. 内存历史也有上限——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; }

机灵兔开放平台 · 组织应用开发者文档