JavaScript SDK
@xiangxinai/sdk 把 POST /v1/systemone 包装成一个带类型的方法:用 noul()、choice()、score() 写问题,答案的类型从问题里自动推断出来;出错时抛出按 HTTP 状态码细分的错误类,限流和过载时自动退避重试。
- 零运行时依赖,基于全局
fetch:支持 Node.js 18 及以上、Deno、Bun,以及 Cloudflare Workers 等边缘运行时;也可以传入自定义fetch。 - 同时发布 ESM 与 CommonJS 两种格式,附带完整的类型声明(需要 TypeScript 5.0 及以上才能获得字面量推断)。
- 与 Python SDK 的默认值、错误类和重试策略一致,见 SDK 总览。
每个导出符号的完整签名见 API 参考,版本变化见更新日志。
快速上手
安装 SDK:
npm install @xiangxinai/sdkpnpm add @xiangxinai/sdkyarn add @xiangxinai/sdkbun add @xiangxinai/sdkDeno 可以直接用 npm 说明符导入:import { XiangxinClient } from 'npm:@xiangxinai/sdk'(运行时需 --allow-env --allow-net)。
在控制台 → API 密钥创建一个密钥(形如 sk-xx-...),放进环境变量 XIANGXIN_API_KEY:
export XIANGXIN_API_KEY="sk-xx-你的密钥"然后创建客户端并提问。下面的例子把一条电商售后消息作为 state,一次调用同时问三个问题:该分给哪个组、客户有多着急、是不是在要求退款。
import { XiangxinClient, choice, noul, score } from '@xiangxinai/sdk'
const client = new XiangxinClient() // 读取 XIANGXIN_API_KEY
const ticket = '我 3 号买的空气炸锅,今天刚到就发现内胆有裂痕,618 活动价买的,现在要退款还是换货?急用!'
const { model, answers, usage } = await client.systemOne({
state: ticket,
questions: {
team: choice('这条消息应该由哪个组处理?', {
after_sales: '退换货、质量问题、售后维修',
logistics: '物流延迟、快递丢件、改地址',
pre_sales: '下单前的商品咨询、价格与优惠',
}),
urgency: score('客户表达的紧迫程度有多高?', ['不着急', '希望尽快处理', '非常着急,明确催促']),
wants_refund: noul('客户是否明确要求退款?'),
},
})
console.log(model) // xiangxin-1.0.0
console.log(answers.team.choice) // 'after_sales'
console.log(answers.team.confidence) // 例如 0.94
console.log(answers.urgency.score) // 例如 1.72
console.log(answers.wants_refund.noul) // 例如 0.41
console.log(usage.input_tokens)const { XiangxinClient, choice, noul, score } = require('@xiangxinai/sdk')
const client = new XiangxinClient()
async function main() {
const { answers } = await client.systemOne({
state: '我 3 号买的空气炸锅,今天刚到就发现内胆有裂痕,现在要退款还是换货?急用!',
questions: {
team: choice('这条消息应该由哪个组处理?', {
after_sales: '退换货、质量问题、售后维修',
logistics: '物流延迟、快递丢件、改地址',
pre_sales: '下单前的商品咨询、价格与优惠',
}),
urgency: score('客户表达的紧迫程度有多高?', ['不着急', '希望尽快处理', '非常着急,明确催促']),
wants_refund: noul('客户是否明确要求退款?'),
},
})
console.log(answers.team.choice, answers.urgency.score, answers.wants_refund.noul)
}
main()注释里的数值只是示意。在 TypeScript 里,answers.team.choice 的类型是 'after_sales' | 'logistics' | 'pre_sales',拼错选项名会在编译期报错,见类型推断。
一个客户端可以在整个进程里复用,它不持有连接池、定时器或其他需要关闭的资源。建议在模块级创建一个实例;不同的超时或重试需求用单次调用的 options 表达,而不是创建很多客户端。
不要把密钥发到浏览器
API 密钥属于组织,任何拿到它的人都能以你的组织身份调用并产生费用。请只在服务端使用本 SDK(Node.js 服务、Serverless / 边缘函数等),浏览器里的页面去请求你自己的服务端接口,由服务端代为调用象信。
SDK 检测到浏览器环境时会拒绝创建客户端。dangerouslyAllowBrowser: true 可以跳过这项检查,但只应用于密钥不会外泄的内部工具。
配置
new XiangxinClient(config?) 的配置项见 XiangxinClientConfig。显式传入的值优先于环境变量,其次是 SDK 默认值;只含空白的环境变量会被忽略。
| 配置项 | 环境变量 | 默认值 | 说明 |
|---|---|---|---|
apiKey | XIANGXIN_API_KEY | — | API 密钥,必填。 |
baseURL | XIANGXIN_BASE_URL | https://api.xiangxinai.cn | API 根地址,末尾斜杠会被去掉。 |
defaultModel | XIANGXIN_DEFAULT_MODEL | xiangxin-latest | 请求省略 model 时使用的模型。 |
logLevel | XIANGXIN_LOG | warn | 日志级别:debug、info、warn、error、off。 |
timeout | — | 30000 | 每次尝试的超时(毫秒),包括读取响应体。 |
retry | — | 见重试 | 重试策略,只需写要改的字段。{ maxRetries: 0 } 关闭重试。 |
defaultHeaders | — | {} | 附加到每个请求的请求头。Authorization 与 Accept 由 SDK 设置,不能被覆盖。 |
fetch | — | 全局 fetch | 自定义 HTTP 实现,用于代理、埋点或测试。 |
logger | — | 带 [xiangxin] 前缀的 console | 日志输出目标,兼容 console、pino、winston 等。 |
dangerouslyAllowBrowser | — | false | 允许在浏览器中运行,这会把密钥暴露给页面访问者。 |
构造时会校验配置,以下情况直接抛出 XiangxinError:没有 API 密钥、密钥含空白或非 ASCII 字符、baseURL 不是 http(s):// 开头、timeout 不是正数、logLevel 无效、运行时没有全局 fetch 且未传入 fetch,以及在浏览器中运行而没有设置 dangerouslyAllowBrowser。
API 密钥保存在私有字段中,不会出现在 console.log(client)、JSON.stringify(client)、String(client) 或任何日志里。
systemOne() 和 models.list() 的最后一个参数是 RequestOptions,可以覆盖本次调用的 timeout、retry、headers,并传入取消用的 signal。
写问题
辅助函数 noul()、choice()、score() 和普通对象完全等价,可以在同一个请求里混用:
const result = await client.systemOne({
state: { 标题: '无法登录', 正文: '输入验证码后一直转圈,换了浏览器也一样' },
questions: {
// 辅助函数:参数少、推断好
is_bug: noul('这是产品缺陷,而不是使用问题吗?'),
area: choice('问题出在哪个模块?', { login: '登录与验证码', payment: '支付', other: null }),
// 普通对象:与 HTTP 请求体一一对应,适合从配置文件读取
severity: { type: 'score', instructions: '严重程度', criteria: ['轻微', '一般', '严重'] },
},
})state、instructions 和各项描述都可以是字符串、JSON 对象或数组。请求对象里除 state、questions、model 之外的字段会原样合并到请求体顶层,用于调用 SDK 尚未封装的新参数。三种问题的含义见原语,请求与答案的字段见 HTTP API。
本地参数不合法时(questions 为空、type 不是 noul / choice / score、Choice 的 criteria 为空、Score 的 criteria 少于两项、state 缺失),systemOne() 直接以 XiangxinError 拒绝,不会发出请求。
类型推断
systemOne 的返回类型由你传入的问题推断:
| 问题 | 答案类型 | 推断出的内容 |
|---|---|---|
noul(...) / { type: 'noul' } | NoulAnswer | noul: number |
choice(i, { a: …, b: … }) | ChoiceAnswer | choice: 'a' | 'b',probabilities: { a: number; b: number } |
score(i, ['低', '中', '高']) | ScoreAnswer | legend: { '0': '低'; '1': '中'; '2': '高' },probabilities 的键为 '0' | '1' | '2' |
const { answers } = await client.systemOne({
state: ticket,
questions: {
dept: choice('分派到哪个部门?', { billing: '扣费、退款', technical: '报错、故障' }),
anger: score('用户有多生气?', ['平静', '不满', '愤怒']),
},
})
switch (answers.dept.choice) {
case 'billing': // ✓
case 'technical': // ✓
break
case 'sales': // ✗ 编译错误:'sales' 不在选项里
}
answers.anger.legend['2'] // 类型为 '愤怒'
answers.anger.legend['3'] // ✗ 编译错误:量表只有 0–2 档
answers.missing // ✗ 编译错误:没有这个问题推断依赖 TypeScript 5.0 的 const 类型参数,所以问题要直接写在调用处或辅助函数里。如果选项来自运行时数据(例如从数据库读出的 Record<string, string>),类型会退化为 string,这是预期行为。
需要在函数之间传递问题时,先把问题集合声明为常量,再用 SystemOneResult 得到结果类型:
import type { SystemOneResult } from '@xiangxinai/sdk'
const ticketQuestions = {
dept: choice('分派到哪个部门?', { billing: null, technical: null }),
urgent: noul('是否需要当天处理?'),
}
type TicketResult = SystemOneResult<typeof ticketQuestions>
function route(r: TicketResult) {
if (r.answers.urgent.noul > 0.8) return 'oncall'
return r.answers.dept.choice // 'billing' | 'technical'
}
route(await client.systemOne({ state: ticket, questions: ticketQuestions }))单个问题的答案类型可以用 ResultFor<typeof question> 得到。
读取答案与用量
const r = await client.systemOne({ state, questions })
r.model // 实际作答的版本,如 'xiangxin-1.0.0',建议写进日志
r.usage.input_tokens // 计费 token 数
r.answers.dept.probabilities // 每个选项的概率,总和为 1读取响应头
systemOne() 和 models.list() 返回的是 APIPromise,它是 Promise 的子类。直接 await 得到解析后的数据;调用 .withResponse() 还能拿到 HTTP 响应和请求 ID:
const { data, response, requestId } = await client.systemOne({ state, questions }).withResponse()
requestId // x-request-id,联系技术支持时提供
response.headers.get('x-xiangxin-model-ms') // 模型耗时(毫秒)
response.headers.get('x-xiangxin-total-ms') // 网关总耗时(毫秒)
data.answers.dept.choice // 与直接 await 的结果相同.asResponse() 返回一个未解析的 Response 副本,可以自己读取原始 JSON;.map(fn) 在不重复解析的前提下变换结果。请求在调用方法时立即发出,与是否 await 无关;对同一个 APIPromise 多次 await 不会重复请求。
错误处理
所有错误都继承自 XiangxinError;服务端返回非 2xx 时抛出 APIError 的子类,可以读取 status、detail 和 requestId:
import {
APIConnectionError,
APIError,
InsufficientBalanceError,
RateLimitError,
UnprocessableEntityError,
} from '@xiangxinai/sdk'
try {
const r = await client.systemOne({ state, questions })
} catch (err) {
if (err instanceof InsufficientBalanceError) {
// 402:余额不足,提示管理员去控制台充值;不会自动重试
} else if (err instanceof UnprocessableEntityError) {
// 422:请求不合法,detail 是字段错误列表或说明字符串
console.error(err.detail)
} else if (err instanceof RateLimitError) {
// 429:自动重试之后仍然超限
console.warn(`稍后再试,建议等待 ${err.retryAfter ?? '?'} 秒`)
} else if (err instanceof APIError) {
console.error(err.status, err.detail, err.requestId)
} else if (err instanceof APIConnectionError) {
// 网络错误或超时(APITimeoutError 是它的子类)
} else {
throw err
}
}| 状态码 | 错误类 | 默认自动重试 |
|---|---|---|
| — | XiangxinError(本地配置或参数错误) | 否 |
| 400 | BadRequestError | 否 |
| 401 | AuthenticationError | 否 |
| 402 | InsufficientBalanceError | 否 |
| 403 | PermissionDeniedError | 否 |
| 404 | NotFoundError | 否 |
| 422 | UnprocessableEntityError | 否 |
| 429 | RateLimitError | 是 |
| 529 | OverloadedError | 是 |
| 500 / 502 / 503 / 504 | InternalServerError | 是 |
| 2xx,响应体结构不对 | APIResponseValidationError | 否 |
| — | APIConnectionError(没拿到 HTTP 响应) | 是 |
| — | APITimeoutError(单次尝试超时) | 是 |
| — | APIUserAbortError(调用方取消) | 否 |
HTTP 层面每个状态码的含义与典型 detail 见 HTTP 错误码。
重试
默认策略(DEFAULT_RETRY_POLICY):
- 对
429、500、502、503、504、529、连接错误和超时最多重试 2 次(共 3 次尝试); - 等待时间指数增长:约 0.5 秒、1 秒、2 秒……单次不超过 8 秒,并随机扣减最多 25% 作为抖动;
- 服务端返回
retry-after-ms或retry-after时按它等待,最长 60 秒; 400、401、402、403、404、422和调用方取消都不会重试。
客户端级与单次调用级都可以只覆盖部分字段,未写的字段沿用上一层:
// 离线批处理:多等一会儿
const batchClient = new XiangxinClient({ retry: { maxRetries: 5, backoffMaxMs: 20_000 } })
// 在线接口:快速失败
await client.systemOne({ state, questions }, { retry: { maxRetries: 0 } })
// 只对过载重试,不对 429 重试
await client.systemOne({ state, questions }, { retry: { httpStatuses: new Set([529]) } })全部字段见 RetryPolicy。象信只在请求成功后扣费,失败的尝试不收费,因此重试不会重复扣费。不要在 SDK 外面再包一层重试循环。
超时与取消
timeout 是每次尝试的上限(毫秒,默认 30000),包括等待响应头和读取响应体;重试时重新计时。
const client = new XiangxinClient({ timeout: 10_000 })
await client.systemOne({ state, questions }, { timeout: 3_000 }) // 单次覆盖超时抛出 APITimeoutError(默认会重试)。要限制一次调用的总时长(含所有重试和等待),传入 AbortSignal:
// 整个调用最多 15 秒
await client.systemOne({ state, questions }, { signal: AbortSignal.timeout(15_000) })
// 用户离开页面、请求被取消时一并取消
const controller = new AbortController()
req.on('close', () => controller.abort())
await client.systemOne({ state, questions }, { signal: controller.signal })取消会立即中止进行中的请求和等待中的重试,并抛出 APIUserAbortError,不会再重试。
批量处理
每个请求本身就能问很多问题,优先把同一个 state 的问题放进一次请求。需要处理很多条 state 时,用固定数量的“工人”控制并发,避免触发速率限制:
async function mapWithConcurrency<T, R>(items: T[], limit: number, fn: (item: T) => Promise<R>): Promise<R[]> {
const results: R[] = new Array(items.length)
let next = 0
const workers = Array.from({ length: limit }, async () => {
while (next < items.length) {
const i = next++
results[i] = await fn(items[i]!)
}
})
await Promise.all(workers)
return results
}
const reviews = ['物流太慢了', '质量不错,下次还买', '客服态度很差']
const results = await mapWithConcurrency(reviews, 8, (text) =>
client.systemOne({ state: text, questions: { negative: noul('这是负面评价吗?') } }),
)指定模型
// 整个客户端固定版本,适合已经按某个版本调好阈值的生产环境
const pinned = new XiangxinClient({ defaultModel: 'xiangxin-1.0.0' })
// 单次调用覆盖
await client.systemOne({ state, questions, model: 'xiangxin-preview' })
// 列出可用模型
for (const m of await client.models.list()) console.log(m.name, m.release_date)默认使用别名 xiangxin-latest,它始终指向最新的象信一号版本。别名与版本的区别见模型。
日志
const client = new XiangxinClient({ logLevel: 'info' }) // 每个请求一行摘要
const verbose = new XiangxinClient({ logLevel: 'debug', logger: pino() }) // 兼容 console 的任意 logger也可以设置环境变量 XIANGXIN_LOG=debug|info|warn|error|off,默认 warn(只输出重试提示)。debug 会输出请求头和正文,Authorization 等鉴权头会被替换为 <redacted>;SDK 在任何级别都不会输出 API 密钥,但会输出请求正文,生产环境请谨慎开启。日志器接口见 Logger。
自定义 fetch
fetch 选项接收任何与全局 fetch 兼容的函数(Fetch),可用于埋点、代理或测试:
const client = new XiangxinClient({
fetch: async (url, init) => {
const started = performance.now()
const res = await fetch(url, init)
metrics.observe('xiangxin_http_ms', performance.now() - started)
return res
},
})在 Node.js 中走 HTTP 代理,可以用 undici 的 setGlobalDispatcher(new ProxyAgent(...)) 设置全局代理,SDK 使用的全局 fetch 会随之生效。
在测试中替身客户端
传入一个返回固定响应的 fetch,就能在不联网、不花钱的情况下测试你的业务逻辑:
import { XiangxinClient } from '@xiangxinai/sdk'
const fakeFetch = async () =>
new Response(
JSON.stringify({
model: 'xiangxin-1.0.0',
answers: { negative: { type: 'noul', noul: 0.92 } },
usage: { input_tokens: 12, output_tokens: 1 },
}),
{ status: 200, headers: { 'content-type': 'application/json' } },
)
const client = new XiangxinClient({ apiKey: 'sk-xx-test', fetch: fakeFetch })同样可以返回 new Response('{"detail":"overloaded"}', { status: 529 }) 来测试错误分支(测试时建议传 retry: { maxRetries: 0 })。
文档
- 象信能做什么:介绍、快速开始、原语总览。
- 每个导出符号的签名与说明:API 参考,例如
XiangxinClient。 - 直接调用 HTTP 接口:HTTP API。
- 用 Python:Python SDK。

