先把一条真实业务链跑通
- 先用 query 跑通第一条真实请求,验证 Token、softwareId、tenantId 和页面上下文都能带进来。
- 优先消费结构化返回,不只看 answer,也要让宿主继续使用 intent、taskState、nextAction、followups 和 cards。
- 把菜单、权限、用户画像通过 sync 或适配器映射接进来,避免只靠自然语言猜上下文。
- 最后再接媒体上传、ASR、OCR 和反馈闭环,不要一开始就把所有能力混在一起。
这页不讲空泛概念,只讲怎么把第一条请求、宿主适配、结构化返回和多模态入口真正接起来。 先把最小链路跑通,再逐步加同步、反馈和媒体网关。
Entry Points
First Request
curl -X POST https://ziin.shenliu.cc/api/ziin/query \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer editor|softwareId:erp-suite|tenantId:tenant-east-cn|userId:u-1001' \
-d '{
"question": "为什么入库单提交失败?",
"mode": "embedded-agent",
"module": "inventory",
"context": {
"route": "/inventory/inbound/create",
"pageTitle": "入库管理",
"selectedMenu": "仓储中心 / 入库管理",
"platform": "web"
},
"inputs": [
{
"type": "error-dialog",
"content": "报错弹窗:无权限提交入库单"
}
]
}'{
"traceId": "q_20260415_0001",
"conversationId": "conv_20260415_0001",
"intent": "ship_create",
"confidence": 0.91,
"taskState": "ship_drafting",
"riskLevel": "draft",
"routingVerdict": {
"mode": "next_step",
"confidence": 0.91,
"source": "strong_agent",
"reason": "ship_create_draft_guidance",
"nextActionPolicy": "ask_clarification"
},
"answer": "可以,已先按成都方向整理成订单草稿。还需要确认收货地址和件重体。",
"nextAction": {
"type": "create_order_draft",
"payload": {
"destinationCity": "成都",
"cargoName": "日用品",
"weight": "300公斤"
}
},
"followups": [
"直接帮我生成草稿",
"我只是先咨询",
"补一下收货地址"
],
"cards": [
{
"type": "draft",
"title": "订单草稿建议",
"data": {
"destinationCity": "成都",
"cargoName": "日用品",
"weight": "300公斤"
}
}
],
"permissions": [
"inventory.inbound.create",
"inventory.inbound.read"
],
"steps": [
"进入入库管理页面",
"先完成必填字段并保存单据",
"联系具备审批权限的角色继续提交"
],
"warnings": [
"当前结果受宿主权限上下文限制"
],
"sources": [
{
"assetCode": "ka_001",
"title": "入库管理操作手册",
"type": "manual"
}
],
"suggestedActions": [
"如无提交按钮,请联系管理员开通提交权限"
],
"actionPlans": [
{
"id": "explain-permission-gap",
"label": "查看权限缺口",
"description": "先确认当前角色缺的是哪个动作权限。",
"action": "explain_permission",
"payload": {
"permissionKey": "inventory.write",
"roleCode": "warehouse_clerk"
}
},
{
"id": "focus-follow-up-field",
"label": "继续追问",
"description": "继续补充报错或字段上下文。",
"action": "focus_field",
"payload": {
"fieldKey": "followUpQuestion",
"target": "[data-ziin-field=\"follow-up-question\"]",
"behavior": "focus"
}
}
]
}[
{
"action": "navigate",
"payload": {
"route": "/orders/list",
"openInNewTab": false
}
},
{
"action": "scroll_to",
"payload": {
"target": "[data-page-id=\"orders_list\"]",
"behavior": "smooth"
}
},
{
"action": "fill_field",
"payload": {
"fieldKey": "customerName",
"target": "[name=\"customerName\"]",
"behavior": "fill",
"fieldValue": "请补充 customerName"
}
}
]新宿主不要从 answer 里反推动作。助手任务推进优先读 routingVerdict、taskState、nextAction、followups、cards;SDK 或宿主页面动作计划继续读 actionPlans。两层动作都必须由宿主白名单 executor 执行。
保留用户原始口语,不要先过度改写。系统怎么问,智引就先吃什么。
最好由宿主直接传模块和当前路由,避免只靠自然语言去猜当前业务位置。
权限边界最好来自宿主可信上下文,而不是由模型自由推断。
截图、语音、文档、报错弹窗都统一挂在 inputs 里,再进入识别和问答链路。
softwareId、tenantId、userId、question、当前 route 或页面标识。没有这些字段,智引只能退化成普通 FAQ。
pageTitle、selectedMenu、platform、module、当前角色或权限码。它们决定回答能否贴近当前业务页面。
菜单树、权限点、用户画像、知识资产、错误库和反馈数据。先跑通第一条链路,再逐步同步。
先用 viewer/editor/admin 这类粗粒度角色降级接入,并在 warnings 里明确结果不是最终权限判断。
{
"question": "这个页面怎么提交?",
"mode": "ui-assistant",
"softwareId": "erp-suite",
"tenantId": "tenant-east-cn",
"userId": "u-1001",
"module": "inventory",
"context": {
"route": "/inventory/inbound/create",
"pageTitle": "入库管理",
"selectedMenu": "仓储中心 / 入库管理",
"platform": "web"
}
}需 API Key。官网悬浮助手和在线控制台可走站内演示通道,正式对外接口调用则应按商用 Key 管理。
控制接口已收口。知识导入、同步、运营、供应商配置和审计接口不建议长期公开。
需商务申请。公开资料可用于评估和联调,但不等于生产商用授权。
Host Types
这是最常见的第一步。适合已有 Web 业务系统、BFF 或 Node 服务的项目。现在 Web 侧已经不止是裸 query,也可以直接从公网 ESM 路径挂最小 widget。
import { ZiinClient } from "@ziin/sdk-js";
const client = new ZiinClient({
baseUrl: "https://ziin.shenliu.cc",
token: "editor|softwareId:erp-suite|tenantId:tenant-east-cn|userId:u-1001",
});
const result = await client.query({
question: "怎么做入库单?",
mode: "api-engine",
module: "inventory",
context: {
route: "/inventory/inbound/create",
platform: "web",
},
});
console.log(result.answer);const response = await fetch("https://ziin.shenliu.cc/api/ziin/query", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer editor|softwareId:erp-suite|tenantId:tenant-east-cn|userId:u-1001",
},
body: JSON.stringify({
question: "这个模块是干嘛的?",
mode: "ui-assistant",
module: "inventory",
context: {
route: "/inventory/inbound/create",
pageTitle: "入库管理",
selectedMenu: "仓储中心 / 入库管理",
platform: "web",
},
}),
});
const result = await response.json();
assistantPanel.render({
answer: result.answer,
intent: result.intent,
routingVerdict: result.routingVerdict,
taskState: result.taskState,
nextAction: result.nextAction,
followups: result.followups,
cards: result.cards,
});import {
createHostRuntimeAdapter,
createActionPlanExecutor,
createZiinWidget,
} from "https://ziin.shenliu.cc/sdk/js/index.js";
const runtimeAdapter = createHostRuntimeAdapter({
adapterConfig: {
clientConfig: {
baseUrl: "https://ziin.shenliu.cc",
token: "editor|softwareId:erp-suite|tenantId:tenant-east-cn|userId:u-1001",
},
defaults: {
mode: "ui-assistant",
module: "inventory",
softwareId: "erp-suite",
tenantId: "tenant-east-cn",
userId: "u-1001",
},
},
getContext: () => ({
route: window.location.pathname,
pageTitle: document.title,
selectedMenu: readSelectedMenuFromHost(),
platform: "web",
pageId: readStablePageId(),
availableActions: ["navigate", "focus_field", "scroll_to", "open_dialog"],
}),
});
createZiinWidget({
runtimeAdapter,
actionExecutor: createActionPlanExecutor({
allowActions: ["navigate", "focus_field", "scroll_to", "open_dialog"],
handlers: {
navigate: (plan) => router.push(plan.route || "/"),
focusField: (plan) => focusHostField(plan.target || ""),
scrollTo: (plan) => document.querySelector(plan.target || "")?.scrollIntoView({ behavior: "smooth", block: "center" }),
openDialog: (plan) => openDialog(plan.target),
},
}),
defaultOpen: true,
title: "智引操作内核",
subtitle: "权限感知式操作指引",
buttonLabel: "打开使用助手",
contextLabel: "当前页面",
theme: {
accentColor: "#c55b2d",
panelWidth: 420,
borderRadius: 26,
},
events: {
onQuerySuccess: ({ response }) => console.log("ziin.trace", response.traceId),
onActionExecuted: ({ plan }) => console.log("ziin.action", plan.id),
},
});
await widget.refreshContext();端的 UI 不同,但协议不应该分裂。小程序、App 和 H5 都应共用同一套 Query 结构。
wx.request({
url: "https://ziin.shenliu.cc/api/ziin/query",
method: "POST",
header: {
"Content-Type": "application/json",
Authorization: "Bearer editor|softwareId:erp-suite|tenantId:tenant-east-cn|userId:u-1001",
},
data: {
question: "报错是什么意思?",
mode: "mini-program",
module: "inventory",
context: {
route: "/pages/inventory/inbound/create",
pageTitle: "入库管理",
selectedMenu: "仓储中心 / 入库管理",
platform: "mini-program",
},
},
});推荐 Token 组织成 role|softwareId:...|tenantId:...|userId:...,当前项目已支持从这个格式中解析 `softwareId`、`tenantId` 和 `userId`。
宿主 AI 保留自己的语气、品牌和会话层,Ziin 只负责提供软件知识、权限判断和结构化答案。
如果你想先让客户直观看到“结构化 Agent 不是纸面字段”,先打开 /embed。当前已经能直接演示intent、taskState、nextAction、cards 和宿主 executor 日志如何一起工作。
import { createHostAdapter } from "@ziin/sdk-js";
const hostAdapter = createHostAdapter({
clientConfig: {
baseUrl: "https://ziin.shenliu.cc",
token: "editor|softwareId:erp-suite|tenantId:tenant-east-cn|userId:u-1001",
},
defaults: {
mode: "embedded-agent",
softwareId: "erp-suite",
tenantId: "tenant-east-cn",
userId: "u-1001",
context: {
platform: "web",
selectedMenu: "仓储中心 / 入库管理",
},
},
});
const result = await hostAdapter.query({
question: "为什么入库单提交失败?",
module: "inventory",
context: {
route: "/inventory/inbound/create",
pageTitle: "入库管理",
pageId: "inventory_inbound_create",
recordId: "IN-20260417-001",
permissionCodes: ["inventory.write"],
errors: [{ code: "NO_PERMISSION", message: "当前用户无提交权限" }],
},
});import {
createHostRuntimeAdapter,
createActionPlanExecutor,
} from "https://ziin.shenliu.cc/sdk/js/index.js";
import { executeFloatingAssistantNextAction } from "./floating-ziin-assistant-action-executor";
const runtimeAdapter = createHostRuntimeAdapter({
adapterConfig: {
clientConfig: {
baseUrl: "https://ziin.shenliu.cc",
token: "editor|softwareId:erp-suite|tenantId:tenant-east-cn|userId:u-1001",
},
defaults: {
mode: "ui-assistant",
module: "inventory",
softwareId: "erp-suite",
tenantId: "tenant-east-cn",
userId: "u-1001",
},
},
getContext: () => ({
route: window.location.pathname,
pageTitle: document.title,
selectedMenu: readSelectedMenuFromHost(),
platform: "web",
pageId: readStablePageId(),
availableActions: ["navigate", "focus_field", "scroll_to", "open_dialog"],
}),
});
const executor = createActionPlanExecutor({
allowActions: ["navigate", "focus_field", "scroll_to", "open_dialog"],
handlers: {
navigate: (plan) => router.push(plan.payload?.route || plan.route || "/"),
focusField: (plan) =>
focusHostField(plan.payload && "target" in plan.payload ? plan.payload.target || "" : plan.target || ""),
scrollTo: (plan) =>
document
.querySelector(plan.payload && "target" in plan.payload ? plan.payload.target || "" : plan.target || "")
?.scrollIntoView({ behavior: "smooth", block: "center" }),
openDialog: (plan) =>
openDialog(plan.payload && "dialogId" in plan.payload ? plan.payload.dialogId : plan.target),
},
});
const result = await runtimeAdapter.query({
question: "这个页面下一步怎么做?",
});
if (result.nextAction) {
executeFloatingAssistantNextAction({
action: result.nextAction,
response: { answer: result.answer },
context: {
routerPush: (route) => router.push(route),
setQuestion,
setTextMode,
setStatus,
focusInput: () => inputRef.current?.focus(),
submitQuestion: (question) => runtimeAdapter.query({ question }),
},
});
}
if (result.actionPlans?.[0]) {
await executor(result.actionPlans[0]);
}const ziin = await fetch("https://ziin.shenliu.cc/api/ziin/query", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer editor|softwareId:erp-suite|tenantId:tenant-east-cn|userId:u-1001",
},
body: JSON.stringify({
question: userMessage,
mode: "embedded-agent",
module: currentModule,
context: currentPageContext,
inputs: multimodalInputs,
}),
}).then((r) => r.json());
return llm.rewriteForUser({
answer: ziin.answer,
intent: ziin.intent,
routingVerdict: ziin.routingVerdict,
taskState: ziin.taskState,
nextAction: ziin.nextAction,
followups: ziin.followups,
cards: ziin.cards,
warnings: ziin.warnings,
actions: ziin.actionPlans,
});RuoYi-Cloud 只是一个宿主,不是产品中心。以后接任何系统,都应只做 adapter,不要重写 core。
curl -X POST https://ziin.shenliu.cc/api/ziin/sync \
-H 'Content-Type: application/json' \
-d '{
"softwareId": "erp-suite",
"tenant": {
"id": "tenant-east-cn",
"softwareId": "erp-suite",
"name": "华东制造集团"
},
"menuNodes": [
{
"id": "mn-sync-001",
"module": "inventory",
"name": "入库管理",
"route": "/inventory/inbound/create",
"permissionKey": "inventory.write"
}
],
"permissionPoints": [
{
"roleCode": "warehouse_clerk",
"permissionKey": "inventory.write"
}
]
}'adapter:
softwareId: erp-demo
tenantMode: single
defaultPlatform: web
mapping:
roles:
"采购员": purchaser
"仓库管理员": warehouse_admin
permissions:
"inventory:inbound:create": "inventory.inbound.create"
"inventory:inbound:read": "inventory.inbound.read"Multimodal
真正商用不是“接个 OCR/ASR 接口”就结束,而是要建设媒体解析网关和统一任务链路。
import { ZiinClient } from "@ziin/sdk-js";
const client = new ZiinClient({
baseUrl: "https://ziin.shenliu.cc",
});
const created = await client.uploadAndCreateAsrTask({
tenantId: "tenant-east-cn",
userId: "u-1001",
source: "app",
scene: "voice-ticket",
language: "zh",
file: {
fileName: file.name,
mimeType: file.type,
bytes: file,
},
});const detail = await client.waitForTask(created.task.taskId, {
intervalMs: 2000,
maxAttempts: 45,
});
if (detail.status !== "success") {
console.error(detail.error);
}
console.log(detail.result);[
{ "type": "text", "content": "为什么提交失败?" },
{ "type": "error-dialog", "content": "提交失败:无权限提交入库单" },
{ "type": "ocr-text", "content": "OCR 结果:单据状态不允许过账" },
{ "type": "image", "content": "https://cdn.example.com/error.png", "mimeType": "image/png" },
{ "type": "document", "content": "https://cdn.example.com/manual.pdf", "mimeType": "application/pdf" }
]不要每个业务页各做一套语音、拍照、文本和附件逻辑。更稳的方式是先抽一层统一输入技能,再把 payload 路由给各业务技能。
type MultimodalEntryPayload = {
scene: "assistant" | "order_create" | "order_detail" | string
intent?: "lookup" | "shipping_consult" | "exception_judge" | string
mode: "voice" | "text" | "image" | "file" | "mixed"
text?: string
audio?: { fileId?: string; durationMs?: number; transcript?: string }
images?: Array<{ fileId?: string; name?: string }>
files?: Array<{ fileId?: string; name?: string; mimeType?: string }>
businessContext?: {
route?: string
pageTitle?: string
selectedMenu?: string
platform?: string
}
createdAt: string
}curl -X POST https://omnimodal.shenliu.cc/api/v1/realtime/sessions \
-H 'Content-Type: application/json' \
-H 'x-omnimodal-api-key: YOUR_OMNIMODAL_KEY' \
-d '{
"tenantId": "YOUR_TENANT_ID",
"scene": "ziin-public-site-voice",
"mode": "voice",
"audioConfig": {
"codec": "pcm16",
"sampleRate": 16000,
"channelCount": 1
},
"businessContext": {
"sourceSystem": "ziin",
"channel": "floating-assistant"
}
}'Delivery Standard