lookio
的官方TypeScript SDK Lookio. 问助理 一个问题,继续一段对话,或者把Lookio变成一个产品 服务器、Worker或脚本。
- 零运行时依赖项。只有
fetch,Headers,AbortController,ReadableStream,TextDecoder和crypto.randomUUID. - 从API的OpenAPI文档键入。字段名是API自己的 snake_case,所以你在REST文档中读到的就是你得到的。
- 安全重试、自动等幂键、输入错误、自动分页、 流的答案。
安装
npm install @lookio/sdk
运行在Node 18+、Cloudflare Workers、Deno、Bun和现代浏览器上(参见 安全注意 在浏览器中使用之前)。ESM和CommonJS。
快速启动
创建一个API密钥 开发人员工具. 对于一个只会问问题的产品, 选择 问问题 预设和限制关键的助手,它需要。 将密钥保存在服务器上,然后:
import { Lookio } from "@lookio/sdk";
const lookio = new Lookio({ apiKey: process.env.LOOKIO_API_KEY });
// or just `new Lookio()`: the key is read from LOOKIO_API_KEY when `process` exists
问助理
const answer = await lookio.assistants.ask("assistant_id", {
question: "How much does the pro plan cost?",
});
console.log(answer.answer); // Markdown, citations appear as [1], [2]
for (const citation of answer.citations) {
console.log(citation.n, citation.title, citation.source);
}
console.log(answer.credit_cost, answer.credits_remaining);
流式传输答案
const stream = lookio.assistants.askStream("assistant_id", {
question: "Summarise our refund policy.",
});
for await (const text of stream.textStream()) {
process.stdout.write(text);
}
const final = await stream.finalAnswer(); // the same fields as `ask`
console.log(final.citations.length, "citations");
流也是类型化事件的异步可迭代对象
({ event: "status" | "delta" | "done" | "error", data }),事实也的确如此
abort(). 它可以读一次。 finalAnswer() 可以在之前,期间或
在读取后,单独调用它读取流到末尾。一个 error 事件
拒绝 finalAnswer() 然后从 textStream() 作为一个 LookioError
(一个 InsufficientCreditsError 当工作空间的积分用完时)。一个流
结束时没有。 done 或者 error 事件抛出 StreamInterruptedError.
请求在您第一次读取流时开始。流永远不会重试, 即使在网络错误之后也不会:服务器没有记录流,所以重复 将被收费两次。
上传一个文件并等待,直到它准备好
import { readFile } from "node:fs/promises";
// Node 20+: stream from disk without loading the file in memory:
// const file = await fs.openAsBlob("./handbook.pdf");
const file = new Blob([await readFile("./handbook.pdf")]);
const resource = await lookio.resources.uploadFile(file, "handbook.pdf", {
title: "Employee handbook",
tags: ["hr", "2026"],
source: "Employee handbook, 2026 edition",
});
// resource.status is "queued": it is read and indexed in the background
const ready = await lookio.resources.waitUntilReady(resource.id);
console.log(ready.status); // "ready"
uploadFile 接受一个 Blob, File, ArrayBuffer, Uint8Array (所以是节点)
Buffer)或网络 ReadableStream. API将文件作为原始字节,而不是
多部份,并要求 Content-Length: SDK会为所有事情计算它,但是
一条小溪,它需要 { contentLength: <bytes> } (一个Node流经过
Readable.toWeb). 流上传不能重新发送,因此不会重试。
waitUntilReady(id, { timeoutMs = 120000, intervalMs = 1500, signal }) 民意调查和
温柔地后退。它把 ResourceFailedError (与 statusDetail)如果……
导入失败 WaitTimeoutError 如果时间到了。 ready_simulated 计数
准备好了。留下的资源 stale 一直轮询直到超时:调用
resources.reindex(id) 为它。对于短文本、页面或小文件,请忽略
{ wait: true } 直接向 createText, createUrl, createSite, uploadFile
或者 get服务器最多等待15秒,以便大多数小导入返回 ready
在单个请求中。
迭代每个资源
for await (const resource of lookio.resources.iterate({ tag: "hr", status: "ready" })) {
console.log(resource.id, resource.title);
}
// One page at a time, with the cursor you control:
const page = await lookio.resources.list({ limit: 50 });
console.log(page.data.length, page.hasMore, page.nextCursor);
// Or page by page, automatically:
for await (const p of lookio.resources.iterate().pages()) {
console.log(p.data.length);
}
列表返回 { data, hasMore, nextCursor, limit?, requestId }. 相同的
iterate 存在于 conversations, publicAssistants, logs.*,
sites.iteratePages(resourceId) 浏览一个站点的页面。
设置站点的同步时间表
resources.createSite 和 sites.updateRules 接受 manual, daily, weekly,
monthly 或者一个区间,比如 every_3_days, every_2_weeks 或者
every_1_month. 限制是1-90天,1-52周和1-12个月。
重复的日程安排需要付费计划。如果 schedule 在创建时省略
一个站点,免费工作空间使用 manual 付费计划使用 weekly. 如果是的话
省略了 sites.updateRules,现有的时间表保持不变。
方法
所有东西都返回API data (列表返回一个页面)。每个方法都需要
{ idempotencyKey?, timeoutMs?, signal? } 作为它的最后一个参数。
| 名称空间 | 方法 |
|---|---|
account, credits, usage | account.get(), credits.get(), usage.get({ from?, to?, granularity? }) |
resources | list, iterate, get, createText, createUrl, createSite, uploadFile, update, delete, content, reindex, waitUntilReady, pages, iteratePages |
sites | preview, pages, iteratePages, updateRules, retryFailed |
tags | list, rename({ from, to }), delete(tag) |
assistants | list, get, create, update, delete, ask, askStream, search, getPublication, publish, unpublish |
publicAssistants | list, iterate, get |
accesses | list, get, create, update, delete, createKey, updateKey, deleteKey |
conversations | list, iterate, get, rename, delete, feedback |
logs | conversations.{list, iterate, get, stats}, answers.{list, iterate, stats}, indexing.{list, iterate, stats}, activity.{list, iterate} |
widgets | list, get, create, update, delete, duplicate, setImage, clearImage, conversations.{list, iterate, get} |
members | list, updatePermissions(userId, { permissions, assistant_ids }) |
feedback | send({ observed_problem, situation, expected_solution, ... }) |
client.request({ method, path, query?, body?, headers?, idempotencyKey? })
以相同的授权到达任何端点,包括在此版本之后添加的端点,
重试和幂等。路径包括 /v1 它和整体解决了
JSON主体(data, has_more, request_id……)。 client.lastRequestId 拥有
最后一个成功响应的Id。
构造函数选项: apiKey, baseUrl (默认 https://api-v2.lookio.app),
timeoutMs (60000), maxRetries (2), retryDelayMs (500)底
倒扣) fetch (你自己的实现), defaultHeaders,
dangerouslyAllowBrowser. 请求携带 Authorization: Bearer <key> 和
X-Lookio-SDK: lookio-js/<version> (浏览器不允许页面设置
User-Agent)。
幂等和重试
每一个 POST (除了流)携带一个 Idempotency-Key header:选择的UUID
每次调用一次,每次重试都不变地重复,因此API运行请求
不管它到达多少次,一次重试的问题不会被收取两次费用。
通过 { idempotencyKey } 要自己选择钥匙,比如要
让你自己的重试循环也是安全的。API会在24小时内记住一个密钥。
SDK重试到 maxRetries 随着时间的推移,伴随着指数级的退缩和不安,
兑现 Retry-After (秒或HTTP日期):
| 失败 | 获取,放置,删除 | 帖子 | 补丁 |
|---|---|---|---|
| 429 | 重试 | 重试 | 重试(在门口被拒绝) |
| 408,5xx,网络错误,超时 | 重试 | 重试(相同的键) | 不是重试 (变更可能已经生效) |
| 其他4xx吗? | 从来没有 | 从来没有 | 从来没有 |
流式传输和流式上传永远不会重试。每次尝试都有自己的超时
(timeoutMs,每个客户或每个电话,默认60秒;它覆盖了一条小溪
等待第一个字节)。通过一项 signal 取消呼叫及其重试。
错误处理
每一次失败都是一次 LookioError所以一 instanceof 包括API的错误,a
断开连接和滥用SDK。
import { InsufficientCreditsError, LookioError, RateLimitError } from "@lookio/sdk";
try {
await lookio.assistants.ask("assistant_id", { question: "..." });
} catch (e) {
if (e instanceof InsufficientCreditsError) {
console.log("Out of credits, top up here:", e.actionUrl);
} else if (e instanceof RateLimitError) {
console.log("Slow down, retry in", e.retryAfterSeconds, "seconds");
} else if (e instanceof LookioError) {
console.log(e.status, e.code, e.message, "request:", e.requestId);
} else {
throw e;
}
}
分支 e.code (例如 insufficient_credits, rate_limited,
resource_not_found),永远不要留言。字段: status (0时无回应
到达), code, type, param, requestId (引用它来支持), actionUrl,
docsUrl, retryAfterSeconds.
| 类 | 当 |
|---|---|
AuthenticationError | 401:没有钥匙,钥匙是错的,过期的或作废的 |
PermissionError | 403:范围缺失; feature_not_available,仅限管理员 |
NotFoundError | 404和410 |
ConflictError | 409 |
RateLimitError | 429,重试次数用完后 |
InsufficientCreditsError | 代码 insufficient_credits,已经 actionUrl |
ConnectionError | 网络故障,请求超时(code: "request_timeout"), abort (code: "aborted") |
ResourceFailedError, WaitTimeoutError | waitUntilReady |
StreamInterruptedError | 流在其最终事件之前结束 |
LookioUsageError | SDK被滥用了(没有密钥,流上传没有大小)。没有发送任何东西 |
不是API的错误信封(代理页面,空正文)的正文变成
一个通用的 LookioError 使用代码 http_error 以及消息中的状态文本。
学分
进口和其他一切都是免费的。 只有 assistants.ask 和
assistants.askStream 消费信用,对每一个问题,即使资源没有答案
(没有读取资源的助手被拒绝 assistant_has_no_resources
在任何信用被持有之前)。价格取决于 mode (flash 是
默认情况下, eco, deep),不管答案的长度是多少。这个
价格上涨 account.get():
const account = await lookio.account.get();
console.log(account.credit_balance);
console.log(account.credential.scopes); // a key without the `query` scope cannot ask
for (const mode of account.capabilities.query_modes) {
console.log(mode.id, mode.credits, mode.available);
}
小部件
小部件是一个聊天框,它向匿名访问者显示一个助手
的网站。每个小部件都带有“准备粘贴”功能 snippets (bubble, embed,
hosted, iframe),加上 settings, limits 和 stats.
const widget = await lookio.widgets.create({
assistant_id: "assistant_id",
name: "Docs help",
allowed_domains: ["docs.acme.com"],
settings: { accent: "#4F46E5", greeting: "Ask me anything about the docs." },
limits: { monthly_credits: 5000 },
});
console.log(widget.snippets.bubble); // paste it in the page
await lookio.widgets.update(widget.id, { status: "paused" }); // shows nothing, answers nothing
for await (const chat of lookio.widgets.conversations.iterate(widget.id)) {
console.log(chat.end_user, chat.message_count, chat.credits);
}
const full = await lookio.widgets.conversations.get(widget.id, "conversation_id");
update 是部分PATCH: settings 和 limits 逐字段更改,而
allowed_domains 作为一个整体被替换。范围: assistants:read 阅读,
assistants:write 为了创建和更新, assistants:delete 删除。
访问者的回答将被计入工作区(插件的价格) mode,
受其限制 limits),所以 limits.monthly_credits 在你发布
片段。
对话和最终用户
每个答案都会保存在一次对话中(90天)。 assistants.ask 返回它
conversation_id 答案是 message_id;后续操作只发送新的
问题和 conversation_id, Lookio阅读历史本身。API密钥
继续它开始的对话:
const first = await lookio.assistants.ask("assistant_id", {
question: "How much does the pro plan cost?",
end_user: "user_42", // your own id for the person asking (optional)
});
const followUp = await lookio.assistants.ask("assistant_id", {
question: "And for the annual plan?",
conversationId: first.conversation_id!,
end_user: "user_42", // a conversation can only be continued for the same end user
});
end_user 对用户的对话(以及工作空间的日志显示)进行分组
它); store: false 什么都不保留(两个id都返回) null,你保留
与自己对话 history). 列出、读取、重命名和删除键
与 lookio.conversations.* (list({ end_user }),
get(id, { end_user }), delete(id): API密钥的会话也离开
日志),并转发用户的大拇指:
await lookio.conversations.feedback(first.conversation_id!, first.message_id!, {
rating: "down", // "up", "down" or null to take it back
comment: "The price is out of date.",
});
end_user 是标识符,而不是身份证明。从你的产品设置它
服务器上经过身份验证的会话。检查对话是否属于这个
人,然后再继续。不要信任由浏览器代码发送的id
检查它。
一个人自己的聊天记录是私人的:一个人的令牌列出了这些记录,并接受no
大拇指还在(feature_not_available,一个 PermissionError)。
日志
工作空间有四个日志,都是空闲的。每个列表都有 list (一页),
iterate (每一页)和,除了 activity, stats 上面的数字
相同的过滤器)。的游标 answers, indexing 和 activity 不透明的:
迭代器通过 next_cursor 回来找你。
| 日志 | 读取 | 需要 | 保持 |
|---|---|---|---|
logs.conversations | 工作区外的人问的是:小部件访问者、通过API密钥或MCP访问产品的用户。成员自己的聊天记录不在其中。 | assistants:write,而一个人的代币则是编辑助手的权利 | 最后一条信息发出后的90天 |
logs.answers | 任何渠道的每个答案(chat, lookio_ai, api, mcp, widget),仅提供元数据,不提供问答文本 | usage:read | 使用历史 |
logs.indexing | 读取和索引的每个文件、页面或文本,以及其状态和失败原因 | resources:read | 有了资源 |
logs.activity | 谁改变了什么,从哪扇门app, api, cli, mcp, agent, system) | admin,通过个人连接(绝不是API密钥) | 一年 |
// Conversations
for await (const chat of lookio.logs.conversations.iterate({ rating: "down", channel: "widget,api" })) {
console.log(chat.channel, chat.end_user, chat.title, chat.thumbs_down);
}
const full = await lookio.logs.conversations.get("conversation_id"); // messages, citations, cost, thumbs, comments
const stats = await lookio.logs.conversations.stats({ from: "2026-10-01" }); // conversations, questions, people, credits, thumbs, by_channel
// Answers: the questions the resources could not answer, and the conversation of a kept one
for await (const answer of lookio.logs.answers.iterate({ channel: "widget,api" })) {
console.log(answer.channel, answer.mode, answer.credits, answer.conversation_id);
}
const answerStats = await lookio.logs.answers.stats({ from: "2026-10-01" }); // answers, credits, tokens, by_channel
// Indexing: what failed, and why
const failed = await lookio.logs.indexing.list({ status: "failed", limit: 20 });
for (const line of failed.data) console.log(line.resource.title, line.name, line.error);
const indexingStats = await lookio.logs.indexing.stats({ resource_id: "resource_id" }); // total, indexed, failed, in_progress, words_indexed
// Activity (admins)
for await (const change of lookio.logs.activity.iterate({ origin: "cli,mcp", action: "assistant" })) {
console.log(change.created_at, change.actor.name, change.action, change.summary);
}
过滤器(全部可选):
conversations:channel(逗号分隔widget,api,mcp),widget_id,api_key_id,assistant_id,rating(up,down,rated,unrated),end_user,q,from,to.answers:channel(逗号分隔chat,lookio_ai,api,mcp,widget),assistant_id,widget_id,api_key_id,member_id(管理员)mode,from,to.indexing:resource_id,status(逗号分隔queued,processing,indexed,replaced,unchanged,skipped,failed),source_type(file,url,site,text),q,include_unchanged("true"或者"false"(默认情况下,网站同步未更改的页面会被忽略),from,to.activity:actor_id,origin(逗号分隔)target_type(resource,assistant,widget,tag,member),target_id,action(assistant.updated,或者assistant对于一种类型上的每一个动作,q,from,to.
类型
公共类型(Resource, Assistant, Answer, Citation, Conversation,
ConversationSummary, LogConversation, LogStats, LogAnswer, LogAnswerStats, LogIndexing, LogIndexingStats, LogActivity, Widget, WidgetSettings, WidgetLimits,
WidgetSnippets, WidgetConversation, CreateWidgetParams, Member, Usage, Account, Credits, TagSummary,
SitePages, SitePreview, StreamEvent)都是从生成的
OpenAPI类型和导出 lookio,就像 components, operations 和
paths 他们自己。它们是由 ../api-worker/openapi.json:
npm run generate # rewrites src/generated/openapi.ts; never edit that file by hand
npm test, npm run typecheck 和 npm run build 先运行它,这样就提交了
文件不能过期。
开发
npm install
npm test # vitest, with a mocked fetch: no test calls a live API
npm run typecheck
npm run build # dist/index.js (ESM), dist/index.cjs (CJS), dist/index.d.ts
src/coverage.test.ts 读取OpenAPI文档,当一个操作有
没有SDK方法,当一个方法发送一个规范没有描述的请求时,或者当
这个README提到了API没有的端点、范围或错误代码。
安全
API密钥永远不能发送给浏览器。 任何打开这一页的人都能读到
然后花掉你的信用。从服务器(Worker, API路由)调用Lookio
将结果发送到页面。在浏览器中,客户端记录一次警告;通过
dangerouslyAllowBrowser: true 让它安静下来,如果你知道你在做什么
在内部工具上实例一个限制为只读范围的键)。永远不要犯错误。
关键:读它 LOOKIO_API_KEY.
许可证
麻省理工学院Duv公司
暂停一个小部件的每月上限 client.widgets.update(id, { limits: { monthly_credits_enabled: false } }). 将其设置为 true 恢复储蓄金额;当月早些时候的支出仍有影响。