LinkSense
开发者指南

开发交互式应用

按名称生成应用资源声明,完成 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,其余可选字段使用默认值:

manifest.json
{
  "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:

  1. 打开 我的应用 → 创建应用,点击 导入交互式应用 卡片右下角的 资源声明清单。
  2. 按名称搜索并勾选应用需要的资源。宽屏下每类资源按两列显示,插件、技能、MCP 服务和知识库各有独立的 全选,互不影响。搜索时,全选搜索结果 只操作该类的当前结果,不会清除其他已选资源。
  3. 点击 复制 dependencies JSON,将复制内容中的 dependencies 字段加入 manifest.json,与 name、version 等字段同级。如果已有该字段,请替换它,不要覆盖整个文件。

此工具自动填写资源名称及 UUID,只列出你拥有且可用的资源,不会创建应用、复制资源、导出凭证或授予权限。将包交给其他人或导入其他环境后,仍可能需要重新匹配资源。超出声明数量上限时,需减少选择后才能复制。

资源项只显示名称,点击带圆角边框的整项即可勾选或取消。每类标题右侧显示当前列表的已选数量,底部始终显示总选择数量。宽屏下,左栏选择资源,右栏始终显示“声明预览”,随选择实时更新;窄屏下自动上下排列。复制失败时,可以直接在预览中选中文本后手动复制。

没有找到资源时,先确认它属于当前账号且处于可用状态;仅能查看或由其他人共享给你的资源不会出现在此清单中。没有勾选任何资源时不能复制;应用不依赖这些资源时,可以直接省略 dependencies。

生成内容包含四类资源数组(未选择的类别为空数组)。以下 UUID 仅为示例:

dependencies.json
{
  "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,也不要删除应用原有的指令、权限和事件定义。例如,选择一个技能和一个知识库后,最小配置可以是:

manifest-with-dependencies.json
{
  "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 匹配。匹配不到时,可以在导入或更新应用包的窗口中选择自己的同类资源。平台保留原始声明及未匹配状态。所有声明都须完成匹配,才可运行草稿或发布;声明本身不会复制资源、携带凭证或授予访问权限。资源被停用或删除后,也需要重新处理。

  1. 在 导入交互式应用 中选择 ZIP,查看应用包已声明的资源。
  2. 名称左侧的 黄色圆形感叹号 表示尚未配置可用资源;在右侧选择器中搜索并选择同类资源。绿色圆形勾 表示已配置。
  3. 若选错资源,点击选择器右侧内部的 清除图标,再重新选择;清除选择不会删除资源或包内声明。
  4. 完成配置后,点击 导入 或 更新。资源未配置完整时,根据窗口提示补齐后重试。

更新、共享与上架

更新包时,相同类型和声明 UUID 保留原映射(包括手动留空);新增声明尝试自动匹配,移除的声明解除绑定。映射保存到应用草稿,不修改包内 UUID。已经发布的版本继续使用它自己的资源快照,不受未完成的新草稿影响;更新共享或上架版本后才采用新绑定。

开发界面的“发布更新”和应用菜单中的“更新应用包”只对创建者生效。包、资源绑定、发布记录与正式安装在同一事务中更新;失败不会留下部分配置。组织共享需重新保存,应用中心需另行上架,使用者手动安装后才采用新版。

交互式应用只允许作为在线服务共享或上架,不提供复制安装。使用者调用创建者绑定的资源,凭证留在服务端,不需要重复匹配。普通应用仍可按授权创建副本。升级前已有的交互式副本保留为独立应用,但不再通过原应用复制升级;历史仅允许复制的授权不会自动变为在线使用授权,创建者需要重新选择共享方式。没有 dependencies 的已有应用包仍可读取和使用。

权限的作用与选择

权限只开放对应 SDK 方法,不会安装插件或授予资源使用权。任务实际使用创建者为应用绑定的资源,而不是使用者在页面中传入的任意资源。没有声明所需权限时,SDK 调用会以 LINKSENSE_SDK_PERMISSION_DENIED 失败。

权限对应 SDK 方法返回内容或作用何时申请
user.profile:readcontext.getCurrentUser()当前用户的 id、name、avatar_url、language。需要显示用户信息或根据用户语言切换界面时;不需要时不申请。
capabilities:readresources.listCapabilities()技能和插件摘要:id、name、type、description、status、can_select、logo_url。需要展示当前用户的技能或插件摘要时;读取列表不会改变应用已配置的资源。
knowledge_bases:readresources.listKnowledgeBases()知识库摘要:id、name、description、lifecycle_status、availability_status。需要展示当前用户的知识库摘要时;应用任务使用创建者配置的知识库,不受此列表影响。
mcp_servers:readresources.listMcpServers()MCP 服务摘要:id、name、status、transport;不返回地址和凭据。需要展示连接信息时;此权限不提供直接调用 MCP 的接口,tasks.run() 也不接受 mcp_server_ids。
tasks:writetasks.run(input)、tasks.interrupt(turnId)、tasks.getState()、tasks.onStateChange(...)在当前应用任务中提交请求或请求中断指定轮次。页面有生成、提交或停止操作时;需要处理受理失败和运行中的冲突。
files:writefiles.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:

  1. 业务目标:任务要解决什么问题,以及哪些范围不包含在内。
  2. 交付要求:最终答复、章节或结论应包含什么,以及信息不足时如何向用户澄清。
  3. 事件触发规则:什么时机发送哪个事件,每次事件代表一条记录还是完整快照。
  4. 与聊天区的分工:在原生聊天区沟通、处理表单和审批;只将适合界面展示的业务数据发送给应用。

下面的完整示例使用“每个研究章节完成后发送一次事件”。不要只写“按需发事件”,也不要把密钥、固定用户身份或本次提交的全部表单值放入长期指令。

自定义事件字段参考

每个 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_schemaJSON 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。它不读取用户或资源列表;如果以后加入选择器,再增加对应权限。

manifest.json
{
  "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 示例:

research.section_ready.payload.json
{
  "section_id": "market-overview",
  "title": "市场概况",
  "summary": "本章节概述目标市场的主要参与者、需求变化与研究范围。"
}

这里的 section_id 是应用自己的业务标识,不是平台生成的事件 id。任务通过 LinkSense 的 emit_application_event 工具发送 { name, payload };页面通过 events.on(...) 接收,SDK 没有供 iframe 自行发送业务事件的 events.emit() 方法。

四种版本信息的区别

信息标记什么什么时候修改
顶层 schema_versionManifest 格式当前固定 1,由平台支持的格式决定。
sdk_versionSDK 协议当前固定 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。
nameManifest 中声明的事件名称。
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。查询只读取现有启动记录、轮次和待处理请求,不提交任务。

状态字段含义
statusidle 尚无任务;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。页面聚焦、恢复联网和任务生命周期事件会重新读取;忙碌时还会定期校准状态,不会重试执行任务。

task-state.js
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。导入成功后可以像普通应用一样启用、停用并共享给指定用户或用户组。应用菜单中的“更新应用包”用于导入新版本。

推荐按以下顺序完成首个版本:

  1. 用最小 Manifest、一个表单和 tasks.run() 跑通输入到聊天结果的流程。
  2. 定义一个有实际展示用途的业务事件,完成 Schema、发送规则和页面监听。
  3. 验证切换任务后能通过事件回放重建结果,再逐步增加资源选择器和可视化。
  4. 发布前检查加载、空结果、提交中、失败、权限不足和重复提交等状态;布局要适应聊天区宽度变化。
  5. 更新时一并调整包 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。