开发交互式应用
按名称生成应用资源声明,完成 Manifest 配置、资源匹配与应用共享,并通过 SDK 提交任务、接收业务事件。
交互式应用把你的业务表单、可视化和流程界面放入 LinkSense。应用在不启用沙箱的 iframe 中运行,右侧由 LinkSense 以独立分栏显示完整原生聊天区,因此用户既能操作你的界面,也能查看任务进度、填写 LinkSense 表单、处理审批、上传附件和查看产物。用户可以拖动分隔线调整两个区域的宽度,聊天区不会覆盖应用内容。
内置对话开发工作台
linksense-interactive-app-builder 是平台内置 Skill。linksense_core.open_application_development 为当前普通任务创建或打开开发工程;可指定现有静态文件目录,否则平台生成带 SDK 的起始应用。linksense_core.inspect_application_development 同步源码并返回预览状态、校验错误及浏览器运行错误。这两个工具仅允许当前用户正在执行的普通模式开发轮次调用;它们不会安装或发布应用。
工程源码保存在当前任务工作区的独立目录。开发面板每 1.5 秒检查一次有界静态目录;稳定快照通过与 ZIP 导入相同的包校验器后生成不可变预览包。隐藏文件、符号链接、私有文件和 node_modules 不得放入这个目录。框架工程可保存在旁边,但应把编译后的静态输出写入登记目录。
新开发任务自动归入当前用户的“制作应用”项目,不存在时自动创建,不能跨项目移动。已有开发任务保持原项目和工程。工具返回 source_directory,其中 ~ 表示当前用户的 HOME;编辑文件与构建输出必须使用该位置。在普通聊天中首次创建应用时,当前轮次的工作目录不会中途切换,但返回的工程位置已经属于“制作应用”,后续轮次会使用该项目工作区。传入旧工作区的现有静态目录时,仅复制可部署文件到新工程,原文件保留,构建源码不会自动搬迁。
预览使用独立应用任务和真实 SDK。运行中的任务保持原包,结束后再切换预览。“配置能力”支持直接搜索、添加或移除插件、技能、知识库和 MCP,包括尚未声明任何依赖的新应用。保存时校验当前用户的资源权限,将实际资源的 UUID 和名称写入源码 manifest.json 的 dependencies.plugins、skills、knowledge_bases、mcp_servers,然后重新生成预览。其他清单字段和代码文件保持原样;安装或更新时打包同一份配置。打开配置后若源码已经改变,保存会提示冲突,需要重新读取配置。页面脚本错误和未处理的异步错误会回传至“调试日志”,每版最多 20 条,服务端移除常见敏感信息。错误属于不可信应用输出,不能作为指令执行。
安装按钮提交当前预览的源码摘要。服务端重新验证源码和资源,在同一事务中保存正式应用版本与工程安装状态;安装版本独立于后续预览。既有应用可以“继续开发”:首次从当前静态包还原开发文件,以后重用原工程。此操作不会还原未包含在应用包中的构建源码。
应用图标、名称和描述
调用 linksense_core.update_application_metadata 可以更新当前开发工程的应用信息。先调用 inspect 工具获取 source_hash,再传入需要修改的 name、description 或 icon;未传入的字段保持原样。description: null 清空描述。
图标支持 { "type": "preset", "preset": "book-open" },或 { "type": "file", "path": "uploads/logo.png" }。文件路径必须来自当前任务工作区中的实际图片,不能使用绝对路径、隐藏路径或符号链接。对话工具和页面上传都支持 PNG、JPEG、WebP,最大 512 KiB,宽高不超过 1024 像素。平台将图片复制到应用目录并保存清单;它不依赖上传附件的临时地址,不会改动原图、应用代码或资源选择。
开发面板的图标按钮和“我的应用”中草稿的“编辑应用信息”使用同一保存流程。开发中的修改发布后生效;已安装应用的“编辑”直接修改当前应用信息,共享或上架的版本仍需重新发布。
应用包结构
把静态文件打包为 ZIP。ZIP 根目录必须直接包含 manifest.json 和 index.html,不要再套一层目录。
manifest.json
index.html
app.js
styles.css
assets/
logo.png应用包最大 10 MiB,解压后最大 30 MiB,单个文件最大 5 MiB,最多 200 个文件。不要包含依赖目录、源码缓存、符号链接、服务端代码或凭据;隐藏文件和隐藏目录(例如 .DS_Store、.git)会导致导入失败,文件路径也不能仅大小写不同。
支持的文件扩展名为 .html、.css、.js、.mjs、.json、.map、.txt、.png、.jpg、.jpeg、.webp、.gif、.svg、.ico、.woff、.woff2。使用构建工具时,只打包构建后的静态文件,并把包内资源引用配置为 ./app.js、./assets/logo.png 等相对路径。字体、脚本、样式和图片可以随包提供,也可以从外部 CDN 加载。
Manifest
manifest.json 同时描述应用身份、所需资源、任务执行说明、SDK 权限以及业务事件契约。LinkSense 在导入时校验它,在创建任务时记录采用的应用包,并在运行时使用对应版本的权限和事件定义。已发布服务可在后续执行时采用新版本,具体规则见下文。
最小配置
如果第一版只需要收集输入并启动任务,可以从以下配置开始。这里显式保留 tasks:write,其余可选字段使用默认值:
{
"schema_version": 1,
"id": "research-workbench",
"name": "研究工作台",
"version": "1.0.0",
"sdk_version": 1,
"permissions": ["tasks:write"]
}这份配置仍需与 index.html 一起打包。它允许页面提交和中断任务,但没有声明业务事件,因此结果在 LinkSense 原生聊天区中查看。
顶层字段参考
下表中的长度限制针对字符串去除首尾空白后的值。Manifest 顶层和 custom_events 的事件定义不接受未知字段;不要自行添加 author、homepage、model 或 mcp_server_ids 等字段。
| 字段 | 类型与限制 | 必填 / 默认值 | 含义与作用 | 推荐实践 |
|---|---|---|---|---|
schema_version | 整数,当前只支持 1 | 必填 | Manifest 文件格式的版本,决定平台如何解析清单。 | 保持 1;发布应用新版本时修改 version,不要递增此字段。 |
id | 字符串,1–120 字符;匹配 ^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$ | 必填 | 包内的应用标识,例如 research-workbench;不是平台为应用分配的 UUID。 | 使用稳定的小写英文名称,单词用 - 分隔,更新时保持不变;不要包含版本号。相同 id 不代表导入时自动覆盖已有应用,更新需从目标应用菜单操作。 |
name | 字符串,1–160 字符 | 必填 | 用户在应用列表等位置看到的名称;导入和更新应用包时写入应用名称。 | 使用简短、可辨认的业务名称,如“研究工作台”,不堆叠版本号或技术名词。 |
version | 字符串,格式为 x.x.x | 必填 | 应用版本号;同版本更新使用独立的包和发布记录。 | 更新可保留版本号或提高,不能低于已有最高版本。 |
description | 字符串或 null;最多 4,000 字符 | 可选,默认 null | 面向用户的应用简介;导入和更新时写入应用描述,不是任务执行指令。 | 说明适用场景、需要的输入和能得到的结果。它只接受一个字符串,不接受多语言对象。 |
instructions | 字符串或 null;最多 20,000 字符 | 可选,默认 null | 应用级任务指令,指导任务如何工作;运行时还会附加已声明的事件名称、描述和 payload Schema。 | 写稳定的业务目标、输出要求和事件触发规则。本次表单输入放入 tasks.run().prompt。省略或设为 null 时使用平台基础指令;空字符串不会触发这个默认行为。 |
icon | 字符串或 null;路径 1–500 字符 | 可选,默认 null | 指向包内图片,用作应用图标;不是外部图片 URL。 | 使用 assets/logo.png 等相对路径;支持 PNG、JPEG、WebP,最大 512 KiB,宽高各不超过 8,192 像素。建议压缩为清晰的方形图标。路径不能以 / 开头或包含 ..。更新时未提供 icon_preset 且未指定图片,会保留原图标;显式选择预设图标时,将 icon 设为 null。 |
icon_preset | 预设图标标识,例如 book-open、sparkles、code-xml | 可选 | 未提供图片时使用的预设图标,也作为自定义图片无法加载时的后备图标。 | 在图标编辑器或应用信息工具中选择支持的值。 |
entry | 字符串,当前只支持 "index.html" | 可选,默认 "index.html" | iframe 打开的入口文件;文件必须位于 ZIP 根目录。 | 保持默认值。不要填完整 URL、子目录入口或前端路由。 |
sdk_version | 整数,当前只支持 1 | 必填 | 声明页面使用的 LinkSense SDK 协议版本。 | 使用本页的 /sdk/v1.js 加载地址;不要填写 SDK 的字符串版本 "1.0.0"。 |
permissions | 权限字符串数组,最多 6 项,不可重复 | 可选,默认包含原有五项,文件权限需显式声明 | 控制页面能调用哪些 SDK 能力;不扩大当前用户对资源的访问权限。 | 显式声明所需权限。只启动任务就写 ["tasks:write"];[] 表示不申请这些能力,省略字段则是原有五项,不包含 files:write。 |
custom_events | 事件定义数组,最多 50 项,名称不可重复 | 可选,默认 [] | 声明任务可向应用投递的结构化业务数据,供界面展示和重建。 | 从少量有明确业务含义的事件开始,例如“章节已完成”,不要按聊天增量或工具日志设计事件。 |
声明与匹配所需资源
| 字段 | 类型与限制 | 必填 / 默认值 | 含义与作用 | 推荐实践 |
|---|---|---|---|---|
dependencies | 对象,包含 plugins、skills、mcp_servers、knowledge_bases 数组 | 可选,各数组默认 [] | 声明应用依赖;每项为 { "id": "资源 UUID", "name": "显示名称" }。插件和技能合计最多 50 项,MCP 和知识库各最多 20 项。 | 使用资源 UUID,不使用名称或 slug 匹配;名称仅用于提示,长度为 1–160 字符。插件和技能之间也不可重复声明相同 UUID。 |
按名称生成资源声明
开发应用时,无需手工查找插件、技能、MCP 服务和知识库的 UUID:
- 打开 我的应用 → 创建应用,点击 导入交互式应用 卡片右下角的 资源声明清单。
- 按名称搜索并勾选应用需要的资源。宽屏下每类资源按两列显示,插件、技能、MCP 服务和知识库各有独立的 全选,互不影响。搜索时,全选搜索结果 只操作该类的当前结果,不会清除其他已选资源。
- 点击 复制 dependencies JSON,将复制内容中的
dependencies字段加入manifest.json,与name、version等字段同级。如果已有该字段,请替换它,不要覆盖整个文件。
此工具自动填写资源名称及 UUID,只列出你拥有且可用的资源,不会创建应用、复制资源、导出凭证或授予权限。将包交给其他人或导入其他环境后,仍可能需要重新匹配资源。超出声明数量上限时,需减少选择后才能复制。
资源项只显示名称,点击带圆角边框的整项即可勾选或取消。每类标题右侧显示当前列表的已选数量,底部始终显示总选择数量。宽屏下,左栏选择资源,右栏始终显示“声明预览”,随选择实时更新;窄屏下自动上下排列。复制失败时,可以直接在预览中选中文本后手动复制。
没有找到资源时,先确认它属于当前账号且处于可用状态;仅能查看或由其他人共享给你的资源不会出现在此清单中。没有勾选任何资源时不能复制;应用不依赖这些资源时,可以直接省略 dependencies。
生成内容包含四类资源数组(未选择的类别为空数组)。以下 UUID 仅为示例:
{
"dependencies": {
"plugins": [{ "id": "10000000-0000-4000-8000-000000000001", "name": "文档工具" }],
"skills": [{ "id": "20000000-0000-4000-8000-000000000001", "name": "采购审核" }],
"mcp_servers": [{ "id": "30000000-0000-4000-8000-000000000001", "name": "采购系统" }],
"knowledge_bases": [{ "id": "40000000-0000-4000-8000-000000000001", "name": "采购制度" }]
}
}将声明加入应用包
“资源声明清单”生成的是包含 dependencies 的 JSON 对象,不是完整应用配置。把该字段合并到现有 manifest.json,不要多套一层 dependencies,也不要删除应用原有的指令、权限和事件定义。例如,选择一个技能和一个知识库后,最小配置可以是:
{
"schema_version": 1,
"id": "policy-assistant",
"name": "政策助手",
"version": "1.0.0",
"sdk_version": 1,
"permissions": ["tasks:write"],
"dependencies": {
"plugins": [],
"skills": [{ "id": "20000000-0000-4000-8000-000000000001", "name": "政策解读" }],
"mcp_servers": [],
"knowledge_bases": [{ "id": "40000000-0000-4000-8000-000000000001", "name": "公司政策" }]
}
}请把示例 UUID 换成资源声明清单生成的值,将文件命名为 manifest.json,再与 index.html 等静态文件一起打包。资源名称只是显示提示:同名资源不一定是同一资源,不要用名称替换 UUID。
| 配置 | 回答的问题 | 不会做什么 |
|---|---|---|
dependencies | 应用需要哪些插件、技能、MCP 服务和知识库? | 不安装或复制资源,不携带凭证,也不授予资源访问权。 |
permissions | 应用页面可以调用哪些 SDK 方法? | 不替代资源声明,也不自动绑定资源。 |
| 导入后的资源配置 | 当前账号用哪些实际资源满足包内声明? | 不修改包内原始 UUID,也不直接改变已发布版本。 |
只需提交任务时,即使任务需要技能或知识库,也可以只申请 tasks:write;只有页面需要读取资源列表时,才增加相应的读取权限。
导入并配置资源
导入预览仅在导入者拥有、类型正确且可用的资源中按 UUID 匹配。匹配不到时,可以在导入或更新应用包的窗口中选择自己的同类资源。平台保留原始声明及未匹配状态。所有声明都须完成匹配,才可运行草稿或发布;声明本身不会复制资源、携带凭证或授予访问权限。资源被停用或删除后,也需要重新处理。
- 在 导入交互式应用 中选择 ZIP,查看应用包已声明的资源。
- 名称左侧的 黄色圆形感叹号 表示尚未配置可用资源;在右侧选择器中搜索并选择同类资源。绿色圆形勾 表示已配置。
- 若选错资源,点击选择器右侧内部的 清除图标,再重新选择;清除选择不会删除资源或包内声明。
- 完成配置后,点击 导入 或 更新。资源未配置完整时,根据窗口提示补齐后重试。
更新、共享与上架
更新包时,相同类型和声明 UUID 保留原映射(包括手动留空);新增声明尝试自动匹配,移除的声明解除绑定。映射保存到应用草稿,不修改包内 UUID。已经发布的版本继续使用它自己的资源快照,不受未完成的新草稿影响;更新共享或上架版本后才采用新绑定。
开发界面的“发布更新”和应用菜单中的“更新应用包”只对创建者生效。包、资源绑定、发布记录与正式安装在同一事务中更新;失败不会留下部分配置。组织共享需重新保存,应用中心需另行上架,使用者手动安装后才采用新版。
交互式应用只允许作为在线服务共享或上架,不提供复制安装。使用者调用创建者绑定的资源,凭证留在服务端,不需要重复匹配。普通应用仍可按授权创建副本。升级前已有的交互式副本保留为独立应用,但不再通过原应用复制升级;历史仅允许复制的授权不会自动变为在线使用授权,创建者需要重新选择共享方式。没有 dependencies 的已有应用包仍可读取和使用。
权限的作用与选择
权限只开放对应 SDK 方法,不会安装插件或授予资源使用权。任务实际使用创建者为应用绑定的资源,而不是使用者在页面中传入的任意资源。没有声明所需权限时,SDK 调用会以 LINKSENSE_SDK_PERMISSION_DENIED 失败。
| 权限 | 对应 SDK 方法 | 返回内容或作用 | 何时申请 |
|---|---|---|---|
user.profile:read | context.getCurrentUser() | 当前用户的 id、name、avatar_url、language。 | 需要显示用户信息或根据用户语言切换界面时;不需要时不申请。 |
capabilities:read | resources.listCapabilities() | 技能和插件摘要:id、name、type、description、status、can_select、logo_url。 | 需要展示当前用户的技能或插件摘要时;读取列表不会改变应用已配置的资源。 |
knowledge_bases:read | resources.listKnowledgeBases() | 知识库摘要:id、name、description、lifecycle_status、availability_status。 | 需要展示当前用户的知识库摘要时;应用任务使用创建者配置的知识库,不受此列表影响。 |
mcp_servers:read | resources.listMcpServers() | MCP 服务摘要:id、name、status、transport;不返回地址和凭据。 | 需要展示连接信息时;此权限不提供直接调用 MCP 的接口,tasks.run() 也不接受 mcp_server_ids。 |
tasks:write | tasks.run(input)、tasks.interrupt(turnId)、tasks.getState()、tasks.onStateChange(...) | 在当前应用任务中提交请求或请求中断指定轮次。 | 页面有生成、提交或停止操作时;需要处理受理失败和运行中的冲突。 |
files:write | files.upload(file)、files.list()、files.remove(fileId) | 上传、列出当前应用任务的附件,移除尚未提交的附件。 | 应用需要上传参考文件时,必须显式声明;不会自动授予旧应用。 |
ready()、events.on(...) 和 chat.show()/hide()/toggle() 不需要额外的权限项。不要把方法名当成新的权限字符串加入 Manifest。资源列表方法返回 { items: [...] };资料读取接口只返回摘要,不返回密码、Token、MCP 凭据、能力正文或知识库正文。
如何编写 instructions
description 帮用户决定是否使用应用;instructions 指导任务完成工作;tasks.run().prompt 描述本次要做什么。把这三类信息分开,更新应用说明或更换表单输入时更容易维护。
建议按以下顺序组织 instructions:
- 业务目标:任务要解决什么问题,以及哪些范围不包含在内。
- 交付要求:最终答复、章节或结论应包含什么,以及信息不足时如何向用户澄清。
- 事件触发规则:什么时机发送哪个事件,每次事件代表一条记录还是完整快照。
- 与聊天区的分工:在原生聊天区沟通、处理表单和审批;只将适合界面展示的业务数据发送给应用。
下面的完整示例使用“每个研究章节完成后发送一次事件”。不要只写“按需发事件”,也不要把密钥、固定用户身份或本次提交的全部表单值放入长期指令。
自定义事件字段参考
每个 custom_events 元素包含以下字段。事件名必须匹配 ^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$,且不能以 linksense.、conversation.、turn.、item. 开头。
| 字段 | 类型与限制 | 必填 / 默认值 | 含义与作用 | 推荐实践 |
|---|---|---|---|---|
custom_events[].name | 字符串,1–120 字符,遵循上述命名规则;同一包中唯一 | 必填 | 连接事件声明、任务发送和 events.on(name, handler) 监听的精确名称。 | 使用业务域加已发生的动作,如 research.section_ready;三处拼写必须一致,避免 update 这类含义模糊的名字。 |
custom_events[].description | 字符串,1–2,000 字符,去除首尾空白后不能为空 | 必填 | 说明何时发送、数据代表什么;会连同契约一起提供给执行任务的模型。 | 写清触发条件、发送粒度和更新方式,例如“每完成一章发送完整章节摘要,相同章节 ID 表示替换”。 |
custom_events[].schema_version | 整数,1–1,000 | 可选,默认 1 | 此事件的数据契约版本,随事件一起投递;与 Manifest 格式版本和应用包版本无关。 | 字段类型、含义或必填规则变化时递增,并同步修改处理函数和包 version;它不会自动转换旧数据。 |
custom_events[].payload_schema | JSON Schema 对象,顶层 type 必须显式为 "object" | 必填 | 定义事件允许携带的数据结构;导入时编译校验 Schema,发送时校验 payload,不符合契约的数据不会投递。 | 使用 JSON Schema 2020-12,明确字段类型、必填项、字符串和数组上限;建议设置 additionalProperties: false,避免夹带未定义数据。 |
payload_schema 怎么写
LinkSense 使用现有的 Ajv 2020 校验器解析事件契约。常用关键字如下;对象字段的可选性和额外字段行为可参阅 JSON Schema 对象说明,方言支持见 Ajv JSON Schema 文档。
| 关键字 | 含义与作用 | 推荐实践 |
|---|---|---|
$schema | 声明 JSON Schema 方言,可省略;使用时填 https://json-schema.org/draft/2020-12/schema。 | 显式声明 2020-12,使编辑器和平台按一致的规则理解契约。 |
type | 限定数据类型;事件顶层必须是 object,字段可使用 string、number、integer、boolean、array、object 等类型。 | 为每个字段指定类型;嵌套对象也应定义完整结构。 |
properties | 描述对象的字段及各自约束,单独声明不会使字段成为必填。 | 只保留界面实际需要的业务字段,名称保持稳定。 |
required | 必须存在的字段名数组;省略它时,properties 中的字段默认可选。 | 把缺失就无法展示或计算的字段列入其中;允许缺失的字段应有明确的界面空态。 |
additionalProperties | 控制是否允许未在契约中定义的额外字段;默认允许。 | 通常设为 false,嵌套对象也分别设置。 |
description | 解释 payload 中业务字段的含义。 | 写清单位、来源或标识稳定性,帮助任务构造正确数据;它不是校验规则。 |
minLength / maxLength | 限制字符串长度。 | 对标题、摘要设置与界面容量相符的上限,必填文本通常设置 minLength: 1。 |
enum | 将取值限制为给定集合。 | 状态、优先级使用稳定代码,例如 high、medium、low,展示时再翻译。 |
items / maxItems | 定义数组元素结构及数量上限。 | 表格行和列表应限制数量;大量结果分成有业务含义的小事件。 |
每次发送的是符合 Schema 的 JSON 对象,不是 Schema 本身,也不是 JSON.stringify(payload) 后的字符串。单个 payload 还受 JSON.stringify(payload).length <= 65,536 的限制;不要按这个上限填满事件,文件和长篇报告应通过 LinkSense 产物交付。
完整配置示例
这个版本只需要启动研究任务、接收章节事件,因此仍只申请 tasks:write。它不读取用户或资源列表;如果以后加入选择器,再增加对应权限。
{
"schema_version": 1,
"id": "research-workbench",
"name": "研究工作台",
"version": "1.0.0",
"description": "收集研究条件并展示结构化进展。",
"instructions": "根据用户本次提交的主题完成研究,信息不足时先在原生聊天区向用户澄清。每完成一个可独立展示的章节,使用 emit_application_event 发送 research.section_ready,包含稳定的 section_id、标题和摘要;同一章节更新时沿用 section_id 并发送完整摘要。最终在原生聊天区给出完整研究结论。不要将聊天增量、推理或工具进度作为业务事件。",
"icon": null,
"entry": "index.html",
"sdk_version": 1,
"permissions": ["tasks:write"],
"custom_events": [
{
"name": "research.section_ready",
"description": "每完成一个可独立展示的研究章节时发送完整章节摘要;相同 section_id 的新事件替换该章节的展示内容。",
"schema_version": 1,
"payload_schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"additionalProperties": false,
"required": ["section_id", "title", "summary"],
"properties": {
"section_id": {
"type": "string",
"minLength": 1,
"maxLength": 80,
"description": "同一轮研究内稳定的章节标识,更新章节时保持不变。"
},
"title": { "type": "string", "minLength": 1, "maxLength": 160 },
"summary": { "type": "string", "minLength": 1, "maxLength": 2000 }
}
}
}
]
}与此契约匹配的事件 payload 示例:
{
"section_id": "market-overview",
"title": "市场概况",
"summary": "本章节概述目标市场的主要参与者、需求变化与研究范围。"
}这里的 section_id 是应用自己的业务标识,不是平台生成的事件 id。任务通过 LinkSense 的 emit_application_event 工具发送 { name, payload };页面通过 events.on(...) 接收,SDK 没有供 iframe 自行发送业务事件的 events.emit() 方法。
四种版本信息的区别
| 信息 | 标记什么 | 什么时候修改 |
|---|---|---|
顶层 schema_version | Manifest 格式 | 当前固定 1,由平台支持的格式决定。 |
sdk_version | SDK 协议 | 当前固定 1,与引入的 SDK v1 对应。 |
version | 整个应用发布包 | 更新可以保留版本号或提高;每次保存都生成独立记录。 |
custom_events[].schema_version | 单个业务事件契约 | 该事件的数据结构或语义改变时递增,并随新的包版本发布。 |
已发布应用何时采用新版本
创建者自己的历史任务按应用所有者权限使用当前已安装版本,包括最初从应用中心创建的任务。应用中心下架不会阻止创建者继续使用自己的启用应用。其他用户仍按任务原有共享或应用中心入口校验权限;应用中心下架时显示“当前应用已被应用中心下架”,保留历史对话并阻止继续执行。渲染区提示无背景、居中显示。应用本身被停用或删除时,所有者同样不能继续执行。
对于通过组织共享或应用中心使用的在线服务,版本按使用入口和执行时机确定:
- 组织共享和应用中心各自保留已提交的版本。创建者更新应用后需重新共享或上架,使用者手动安装新版。
- 新任务和已有任务的下一轮均使用当前用户已安装的版本;不会自动跟随创建者尚未安装的更新。
- 同一用户同一应用有正式任务正在运行或启动时,保存和安装更新均被阻止,避免执行中切换环境。开发调试使用独立环境。
- 只重新打开或刷新历史任务时,加载该任务当前记录的应用包:可能是创建时采用的包,也可能是后续执行已采用的新包。页面恢复本身不会触发执行或自动升级。
上述规则针对已发布的在线服务,不代表未共享或上架的应用草稿、开发预览会按共享版本自动更新。
历史事件和结果不会按新契约重写;同一任务可能包含多个 schema_version 的事件。新包的事件处理函数需要识别历史事件的 schema_version,避免恢复旧结果时把旧数据当作新结构。发布前检查创建新任务、只重新打开旧任务、在旧任务中再次执行,以及任务运行期间发布更新四种情况。
引入 SDK
在 index.html 中引入 LinkSense 托管的 SDK:
<script src="/api/v1/interactive-app-runtime/sdk/v1.js?v=1.1.0"></script>
<script type="module" src="./app.js"></script>SDK 挂载到 window.LinkSense。在 app.js 中尽早注册业务事件监听,再等待就绪并启用交互:
const sections = new Map()
const unsubscribe = LinkSense.events.on("research.section_ready", (event) => {
const key = `${event.turn_id}:${event.payload.section_id}`
sections.set(key, event.payload)
renderSections([...sections.values()])
})
await LinkSense.ready()renderSections 是应用自行实现的渲染函数;使用 textContent 或框架的文本插值展示内容,避免把任务输出直接赋给 innerHTML。这里用轮次 ID 加章节 ID 更新记录,同一章节的新摘要会替换旧内容,不同轮次不会相互覆盖。组件卸载时调用 unsubscribe() 移除监听。
需要读取上下文或资源时,先在 Manifest 中加入权限,再在 ready() 之后调用对应方法。例如添加 knowledge_bases:read 后,使用 const { items } = await LinkSense.resources.listKnowledgeBases() 获取选项。
运行环境支持内联 <script>、事件处理属性、包内脚本和外部脚本。SDK 使用上面的平台地址,包内应用脚本使用相对路径。脱离 LinkSense 单独打开 HTML 时没有宿主握手,ready() 不会完成;本地可以开发纯 UI,SDK 联调需要导入应用后进行。
启动任务
const receipt = await LinkSense.tasks.run({
prompt: "根据表单条件完成竞争分析,并在每个章节完成时发送 research.section_ready。",
})| 参数 | 含义与限制 | 推荐实践 |
|---|---|---|
prompt | 必填;去除首尾空白后 1–200,000 字符,是本次任务请求。 | 从已校验的表单值构造,明确主题、约束和交付要求;不要只提交没有上下文的字段值。 |
capability_ids | 可选,默认 [];最多 50 个技能或插件 ID。 | 应用任务使用已配置的技能和插件,此参数不能覆盖应用资源,通常应省略。 |
knowledge_base_ids | 可选,默认 [];最多 20 个知识库 UUID。 | 应用任务使用已配置的知识库,此参数不能覆盖应用资源,通常应省略。 |
file_ids | 可选,默认 [];最多 100 个不重复的文件 UUID。 | 来自 files.upload() 或 files.list() 的待提交附件 ID;只绑定本次明确选择的文件,不收集聊天区草稿。 |
idempotency_key | 可选;去除首尾空白后 1–120 字符。 | 通常由宿主管理即可;若自行设置,应标识同一次逻辑提交,不要让不同请求共用固定值。 |
提交后,LinkSense 会立即打开右侧聊天区并显示正在发送的消息;服务端受理后,待发送消息与正式消息合并。tasks.run() 返回服务端的受理回执,不等待聊天历史刷新或研究结果生成。同一次提交尚未完成时的重复调用会合并;应用仍应在提交期间禁用生成按钮,并处理调用失败。
通过应用提交的消息超过当前聊天宽度下的 5 行时,默认折叠并在底部渐隐,用户可使用箭头图标展开或收起。完整提示词仍会发送、保存并用于复制;手动输入的聊天消息不受此规则影响。
消息增量、推理和工具进度不会重复发送给 iframe。应用只接收自己声明的业务事件,回调对象字段如下:
| 回调字段 | 含义与用法 |
|---|---|
id | 平台生成的稳定事件 ID,用于事件去重;不是业务记录 ID。 |
name | Manifest 中声明的事件名称。 |
schema_version | 对应事件定义中的 Schema 版本。 |
payload | 已通过该事件 Schema 校验的业务对象。 |
turn_id | 产生该事件的任务轮次 ID,可用于区分多次提交的结果。 |
sequence | 所在任务事件流中的序号,用于排序;业务事件之间不保证连续。 |
created_at | 事件时间,用于展示发生时间。 |
网络恢复时同一事件可能再次到达,SDK 会按事件 id 去重,应用更新界面时也应保持幂等。受理回执只表示请求已被接受,不代表任务已完成;需要停止时可调用 await LinkSense.tasks.interrupt(receipt.turn_id),不能用隐藏聊天区代替中断。
任务状态查询与订阅(SDK 1.2.0)
tasks.getState() 读取当前应用会话的最新任务状态;tasks.onStateChange(handler, onError?) 订阅状态变化并返回取消订阅函数。两者沿用 tasks:write,不增加 Manifest 权限或 sdk_version 值;SDK 地址仍是 /api/v1/interactive-app-runtime/sdk/v1.js。查询只读取现有启动记录、轮次和待处理请求,不提交任务。
| 状态字段 | 含义 |
|---|---|
status | idle 尚无任务;starting 已受理、正在启动;running 执行中;waiting_for_input 需要在原生聊天处理问题或计划;completed 已完成;failed 失败;interrupted 已停止。 |
turn_id | 当前启动请求或最新轮次的 ID,无任务时为 null。启动失败可能只有请求 ID,没有原生轮次。 |
file_ids | 当前请求/轮次关联的应用附件 ID;启动时附件可能仍是 staged,但已被该请求占用。 |
can_submit | 当前是否可尝试提交新请求;排队任务也会使它为 false。它不是锁,多窗口操作仍需处理提交时的冲突。 |
interrupt_requested | 服务端是否已记录停止请求;不代表任务已经停止。 |
先注册业务事件和状态监听,再读取初始快照。在初始读取完成前,以及读取失败后禁用提交。监听器注册时如果 SDK 已收到状态,会立即收到最新一份;不会回放历史启动/完成状态。查询响应与推送乱序时 SDK 使用当前页面实例的递增版本防止旧状态覆盖新状态;这个版本不需要应用保存。onError 接收状态读取错误,随后成功的读取或推送会继续调用 handler。页面聚焦、恢复联网和任务生命周期事件会重新读取;忙碌时还会定期校准状态,不会重试执行任务。
await LinkSense.ready()
let current = null
let submitting = false
const button = document.querySelector('[type="submit"]')
button.disabled = true
function render(state) {
current = state
button.disabled = submitting || !state.can_submit
// 用应用的双语文案映射 state.status,按 state.turn_id 展示对应结果。
}
function unavailable() {
current = null
button.disabled = true
// 展示本地化的“暂时无法读取任务状态”,允许用户重新打开页面。
}
const unsubscribe = LinkSense.tasks.onStateChange(render, unavailable)
await LinkSense.tasks.getState().catch(unavailable)
window.addEventListener('pagehide', unsubscribe, { once: true })
// 提交时同时检查 current?.can_submit 和 submitting;仅用户操作可调用 tasks.run。tasks.run() 的受理回执不表示任务完成。停止后也应继续显示实际任务状态,直到收到 interrupted 或其他终态。业务事件可能在完成状态之后才回放到页面,应继续接收;按事件 id 去重、按 turn_id 分组,避免多轮结果混在一起。
用 files.list() 恢复文件列表,并将状态快照的 file_ids 标为已提交/处理中。不能把仍处于 staged 但已被启动请求占用的文件当作新的可提交文件,也不能重新提交 bound 文件。查询不会恢复尚未提交的表单草稿;应用无需将“正在启动”等显示状态另外存入数据库或 localStorage。
已有应用仍可使用原方法,无需改变 Manifest 或重新导入;但要让应用内部按钮与提示支持恢复,需要在应用代码中接入这些方法并正常发布更新、安装更新。已有任务打开的是其记录的包版本,仅刷新不会改写旧包;按现有版本更新流程核对任务实际采用的版本。平台不会直接修改已发布应用源码。
页面重进与事件恢复
用户切换到其他任务后,可以从左侧任务列表或搜索结果重新进入交互式应用。LinkSense 会加载该任务当前记录的应用包版本,并按 sequence 从头回放已经持久化的自定义事件,然后继续发送实时事件。已发布服务在下一次执行采用新包后,后续重新进入会加载该新包;仅打开页面不会改变任务记录的版本。页面恢复不会重新运行任务,也不会要求模型再次生成数据。
SDK 会缓存应用注册监听器前到达的事件,并按稳定事件 id 去重。因此建议先调用 LinkSense.events.on(...) 建立所有业务事件监听,再读取其他上下文;事件处理函数仍应保持幂等,并从回调数据重建需要恢复的界面。尚未提交且没有通过自定义事件保存的应用内表单草稿不会由 LinkSense 自动持久化。
聊天区控制
用户始终可以显示或隐藏 LinkSense 聊天区,并可在桌面端拖动分隔线调整宽度。应用也可以请求切换:
await LinkSense.chat.show()
await LinkSense.chat.hide()
await LinkSense.chat.toggle()隐藏聊天区不会停止任务。需要处理 LinkSense 表单、审批或错误时,应引导用户重新打开聊天区。
网络、下载与信任边界
- 交互式应用不启用 iframe
sandbox,运行资源不下发 CSP、Permissions-Policy 或 X-Frame-Options;可以直接使用fetch、XMLHttpRequest、WebSocket、EventSource,加载外部资源、提交 HTML 表单、打开新窗口和下载文件,不需要新增 Manifest 权限。 - 下载可以使用普通文件链接、
download属性或fetch获取文件后生成 Blob URL。跨域download属性、弹窗和自动下载仍受浏览器规则约束;第三方服务器可以通过Content-Disposition: attachment提供下载。 - 浏览器的 CORS、HTTPS 混合内容规则和权限提示仍然有效,外层宿主或部署网关的 Permissions-Policy 也可能约束设备能力。第三方接口应允许 LinkSense 页面来源;HTTPS 页面应使用 HTTPS API 和 WSS 连接。服务端网络可达不代表用户浏览器可达。
- 只导入可信的应用包及外部脚本。应用与宿主同源,不再构成安全隔离边界,可以访问父页面、同源存储和非 HttpOnly Cookie,并可能以当前用户身份请求 LinkSense API;HttpOnly Cookie 仍不能由 JavaScript 读取。不要把长期 Token 或第三方服务密钥打包到前端。
- 已导入的应用在部署新版 API 和 Web、重新加载页面后使用新策略,不需要重新导入或修改历史 Manifest。应用自己声明的 CSP 或外部代理额外添加的策略仍会生效。
- 所有 SDK 请求都受 Manifest 权限和服务端资源授权约束。
- 自定义事件 payload 必须符合 Manifest JSON Schema;无效事件不会投递。
- 自定义事件不要用于传输聊天消息、推理过程、工具日志、凭据或大文件。文件应通过 LinkSense 产物能力交付。
导入和更新
进入“插件中心 → 应用 → 创建应用 → 导入交互式应用”上传 ZIP。导入成功后可以像普通应用一样启用、停用并共享给指定用户或用户组。应用菜单中的“更新应用包”用于导入新版本。
推荐按以下顺序完成首个版本:
- 用最小 Manifest、一个表单和
tasks.run()跑通输入到聊天结果的流程。 - 定义一个有实际展示用途的业务事件,完成 Schema、发送规则和页面监听。
- 验证切换任务后能通过事件回放重建结果,再逐步增加资源选择器和可视化。
- 发布前检查加载、空结果、提交中、失败、权限不足和重复提交等状态;布局要适应聊天区宽度变化。
- 更新时一并调整包
version、事件契约和处理函数,并在新任务和旧任务中分别检查结果。
仓库中的 examples/interactive-research-brief/ 提供了包含 Manifest、表单、资源选择和事件展示的完整静态示例,可作为扩展参考。
常见问题排查
| 现象 | 优先检查 | 处理方式 |
|---|---|---|
| 导入提示应用包无效 | ZIP 根目录、未知字段、版本类型、重复权限或事件、文件与图标限制。 | 从最小配置开始比对;确认数字 1 没写成字符串 "1",index.html 与 manifest.json 没被套进目录。 |
| 事件 Schema 导致导入失败 | payload_schema.type、Schema 方言、引用是否可解析。 | 使用 2020-12 和顶层对象;让 Schema 自包含,不依赖运行时下载外部 $ref。 |
| 更新提示版本号过低 | 新包版本是否低于应用已有最高版本。 | 使用相同或更高的 x.x.x 版本号。 |
| 图标无法导入 | 是否把 SVG、GIF 或外部 URL 用作 icon。 | 换成包内合规的 PNG、JPEG 或 WebP;普通页面资源允许 SVG,不代表应用图标也支持 SVG。 |
| 页面脚本、请求或资源不工作 | 检查浏览器报错、CORS、HTTPS 混合内容、资源路径和用户网络连通性。 | 平台允许直接连接和外部脚本;配置第三方服务允许页面来源,使用 HTTPS/WSS 和正确的资源路径,并排查应用自身或代理附加的 CSP。 |
| SDK 报未就绪或权限不足 | 是否完成 ready(),Manifest 是否包含对应权限。 | 等待初始化后启用按钮;按实际调用添加所需权限,重新发布并在新任务中验证。 |
| 任务有聊天结果但应用没有业务数据 | 声明、指令和监听器的事件名是否一致,任务是否调用发送工具,payload 是否满足 Schema。 | 明确事件触发条件;检查必填字段、类型、长度和未声明字段,尽早注册监听。 |
| 重进页面后出现重复数据或草稿丢失 | 是否把每个事件都追加为新记录,是否把未提交草稿当成已保存结果。 | 按事件 ID 去重、按业务 ID 更新记录;从持久化事件重建界面,不依赖页面内存恢复草稿。 |
上传参考文件
文件能力从 SDK 1.1.0 开始提供,协议 sdk_version 仍为 1。更新应用包时请使用上面的 v1.js?v=1.1.0 地址,避免浏览器继续使用旧版 SDK 的长期缓存。
在 Manifest 的 permissions 中显式加入 files:write,提交任务还需要 tasks:write。页面自行提供文件选择、拖拽、文件列表和上传状态;SDK 不创建上传界面。使用浏览器 File 对象,宿主负责上传,不要将文件转成 Base64、直接请求平台 API 或放入自定义事件。
await LinkSense.ready()
const file = document.querySelector('input[type="file"]').files[0]
const uploaded = await LinkSense.files.upload(file)
const receipt = await LinkSense.tasks.run({
prompt: "分析这份参考资料,归纳主要发现。",
file_ids: [uploaded.id],
})| 方法 | 返回值与行为 |
|---|---|
files.upload(file) | 上传单个 File,成功后返回文件信息;可对多个文件逐个调用。上传本身不启动任务。 |
files.list() | 返回 { items },包含当前应用任务已上传的 staged 和已提交的 bound 文件,用于页面重进后恢复列表。 |
files.remove(fileId) | 移除尚未提交的应用附件,成功返回 { removed: true }。已提交或正在受理的附件不能移除。 |
文件信息只包含 id、filename、mime_type、size_bytes、status 和 turn_id。staged 表示可供本次提交选择,bound 表示已属于某个轮次。页面恢复时可展示全部文件,但只能将 staged 文件用于下一次提交;不会自动恢复表单字段与文件的业务对应关系。
上传采用平台现有文件大小和会话文件数量限制;应用可用 accept 和自身校验进一步限定类型。多文件上传分别成功或失败,页面应保留成功项,允许失败项重试或移除,并在所有所选文件就绪后启用提交。SDK Promise 拒绝时应展示本地化提示,不能忽略错误继续提交。宿主在上传或移除请求进行中拒绝任务提交;任务提交失败时保留附件供用户重试。
file_ids: [] 或省略该字段表示此次不带文件。应用附件和普通聊天待发送附件分开管理:应用只能提交本应用任务的待提交附件,聊天也不会自动带走应用附件。成功受理后文件归属本轮任务,聊天记录显示对应附件,执行器使用现有附件上下文读取文件。不得把已提交附件当成新附件再次提交;重复的同一次请求应使用相同 idempotency_key,变更文件选择应使用新的 key。
新增能力不要求旧应用重新导入;旧应用仍能提交纯文本任务。要使用上传能力,需更新应用包声明 files:write,并为共享或上架的应用完成对应入口的版本更新。建议创建新任务验证上传;已有共享或上架任务需在下一次执行采用新包后,才使用新包声明的权限。只刷新页面不会更新权限,上传前仍以该任务当前记录的应用包为准。
examples/interactive-research-brief/ 演示了多文件选择、失败重试、移除、重进恢复及按 ID 提交。
部署此能力时,需要先运行 pnpm db:migrate:deploy,扩展附件来源的数据库约束,再部署同版本的 API 和前端。迁移保留原有来源值、历史附件和字段结构。开始写入新的附件来源后,不应将服务回退到不识别该来源的旧版本;不要混用新旧 API 实例。
若各类文件上传都返回 INTERNAL_ERROR,且服务端日志出现 conversation_files_source_check 约束错误,请确认部署包包含并已执行 20260918210000_allow_interactive_application_uploads 迁移。修复后可直接重试上传,无需重新导入应用。pnpm test:interactive-files:postgres 使用临时 PostgreSQL 容器验证历史升级和全新安装,不连接应用数据库;该检查也会在 CI 中运行,需要 Docker。