{"_id":"@0xtalk/channel-sdk","_rev":"2-c01281794b1fd19c46481c829b9cd1c5","name":"@0xtalk/channel-sdk","dist-tags":{"latest":"1.0.0"},"versions":{"0.1.0":{"name":"@0xtalk/channel-sdk","version":"0.1.0","keywords":["astradesk","customer-service","channel-adapter"],"license":"MIT","_id":"@0xtalk/channel-sdk@0.1.0","maintainers":[{"name":"0xtalk","email":"18516360572@163.com"}],"dist":{"shasum":"204a5abb65b3db3723f0a730d665573f56a48ee7","tarball":"https://registry.npmjs.org/@0xtalk/channel-sdk/-/channel-sdk-0.1.0.tgz","fileCount":4,"integrity":"sha512-PyWRR63oHA19N6KqGGsdVHhZRAXxaEiFP4MFMe+npbTleQBndcXUPhTHYnPWEH9fexwMWyItw3BvKjb4UED35Q==","signatures":[{"sig":"MEYCIQCRPf5YTWIVWpPAUamoEBw0BL1by7sU17pQbynrKAmF+QIhALSRENGPOg72V22qrMKJ5coDqwecKuQOFGIs6vfQa+wO","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":33527},"main":"./src/index.js","type":"module","types":"./src/index.d.ts","engines":{"node":">=18"},"exports":{".":{"types":"./src/index.d.ts","import":"./src/index.js"}},"gitHead":"22f709a024cbdb7be25ed17a9c134db8d69a55b5","scripts":{"test":"node --test"},"_npmUser":{"name":"0xtalk","email":"18516360572@163.com"},"_npmVersion":"11.17.0","description":"AstraDesk 服务端渠道适配器 SDK","directories":{},"_nodeVersion":"24.19.0","_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/channel-sdk_0.1.0_1787203389959_0.22073881769527715","host":"s3://npm-registry-packages-npm-production"}},"1.0.0":{"name":"@0xtalk/channel-sdk","version":"1.0.0","description":"AstraDesk 服务端渠道适配器 SDK","type":"module","main":"./src/index.js","types":"./src/index.d.ts","exports":{".":{"types":"./src/index.d.ts","import":"./src/index.js"}},"scripts":{"test":"node --test"},"engines":{"node":">=18"},"license":"MIT","keywords":["astradesk","customer-service","channel-adapter"],"gitHead":"8a208216383646bc4287b117c20ae2012236010c","_id":"@0xtalk/channel-sdk@1.0.0","_nodeVersion":"24.19.0","_npmVersion":"11.17.0","dist":{"integrity":"sha512-hWUwWW5odpg63hEBqgPK26Ib4a4ocglxF87a6myypflabZiYLSm36tv+KzTO5TEPNSO5X0/vekkkl3Vq+iac6Q==","shasum":"f3285f7f3bdd6e9ecfe27a9f91cd4c6cbe85e686","tarball":"https://registry.npmjs.org/@0xtalk/channel-sdk/-/channel-sdk-1.0.0.tgz","fileCount":4,"unpackedSize":28946,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQC9d17au+eAPPSdpB3WoiUghC546Xbl0W8MfmepZ8XeaAIgBG8ZpKfEwqtWojoP1hlXqNJCzouYKbLPhj51iWHrnvY="}]},"_npmUser":{"name":"0xtalk","email":"18516360572@163.com"},"directories":{},"maintainers":[{"name":"0xtalk","email":"18516360572@163.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/channel-sdk_1.0.0_1787540108448_0.05844244780423247"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-20T05:23:09.723Z","modified":"2026-08-24T02:55:08.732Z","0.1.0":"2026-08-20T05:23:10.107Z","1.0.0":"2026-08-24T02:55:08.587Z"},"license":"MIT","keywords":["astradesk","customer-service","channel-adapter"],"description":"AstraDesk 服务端渠道适配器 SDK","maintainers":[{"name":"0xtalk","email":"18516360572@163.com"}],"readme":"# AstraDesk 渠道 SDK\r\n\r\n`@0xtalk/channel-sdk` 1.0.0 是面向 Node.js 18+ 服务端的零依赖 ESM SDK。1.0 采用纯异步事件协议：渠道提交事件并立即确认，唯一的持久化批次任务负责等待结果和发送回复。\r\n\r\n> API Key 只能保存在服务端，不能写入网页、小程序、App 安装包或公开仓库。\r\n\r\n## 安装与初始化\r\n\r\n```bash\r\nnpm install @0xtalk/channel-sdk\r\n```\r\n\r\n```js\r\nimport { AstraDeskClient, createWebsiteAdapter } from \"@0xtalk/channel-sdk\";\r\n\r\nconst client = new AstraDeskClient({\r\n  baseUrl: process.env.ASTRADESK_BASE_URL,\r\n  apiKey: process.env.ASTRADESK_API_KEY,\r\n  assistantId: process.env.ASTRADESK_ASSISTANT_ID,\r\n  timeoutMs: 30_000,\r\n});\r\n\r\nconst website = createWebsiteAdapter(client);\r\n```\r\n\r\n默认 `baseUrl` 为 `http://localhost:8080`。SDK 使用原生 `fetch`，也可注入自定义 Fetch 实现。\r\n客户端 `timeoutMs` 控制单次 HTTP 请求，默认 30 秒；`waitForBatch()` 的整体等待上限独立计算，默认 5 分钟。\r\n\r\n## 正确的 Webhook 流程\r\n\r\n每个 Webhook 请求只提交事件、持久化唯一 `batchId` 并立即 ACK：\r\n\r\n```js\r\napp.post(\"/webhooks/chat\", async (req, res) => {\r\n  const event = verifyAndParseWebhook(req);\r\n  const conversationId = await loadConversation(event.senderId);\r\n\r\n  const acceptance = await website.submitEvent({\r\n    externalContactId: event.senderId,\r\n    externalMessageId: event.messageId,\r\n    conversationId: conversationId || undefined,\r\n    content: event.content,\r\n    signal: req.signal,\r\n  });\r\n\r\n  await jobs.upsertByBatchId({\r\n    batchId: acceptance.batchId,\r\n    externalContactId: event.senderId,\r\n  });\r\n\r\n  res.status(202).json({\r\n    batchId: acceptance.batchId,\r\n    duplicate: acceptance.duplicate,\r\n    conversationId: acceptance.conversationId,\r\n  });\r\n});\r\n```\r\n\r\n要求：\r\n\r\n- `externalMessageId` 使用来源渠道稳定且唯一的消息 ID，重投时不得生成新值。\r\n- 对 `batchId` 建唯一约束；重复事件返回同一批次时不能创建多个投递任务。\r\n- 不要让 Webhook 请求等待批次完成。\r\n- 消息聚合和去重由 AstraDesk 服务端完成，不要添加进程内计时器或去重 Map。\r\n\r\n## 唯一批次任务\r\n\r\n持久化 Worker 按唯一 `batchId` 等待完成，并执行一次渠道回复：\r\n\r\n```js\r\nasync function processBatch(job, signal) {\r\n  const result = await client.waitForBatch(job.batchId, {\r\n\t\ttimeoutMs: 300_000,\r\n    pollIntervalMs: 250,\r\n    maxPollIntervalMs: 2_000,\r\n    backoffFactor: 2,\r\n    signal,\r\n  });\r\n\r\n  await saveConversation(job.externalContactId, result.conversationId);\r\n  await deliverChannelReply(job.externalContactId, result.content);\r\n  await jobs.markDelivered(job.batchId);\r\n}\r\n```\r\n\r\n任务表应以 `batchId` 为唯一键，并用领取锁、状态机或队列的 exactly-once/at-least-once 防重机制保证同一批次不会并发发送多次答案。\r\n\r\n如果业务已有调度机制，也可先调用 `getBatch()`：\r\n\r\n```js\r\nconst batch = await client.getBatch(job.batchId);\r\n\r\nif (batch.status === \"completed\") {\r\n  const result = await client.waitForBatch(job.batchId);\r\n  await deliverChannelReply(job.externalContactId, result.content);\r\n} else if (batch.status === \"failed\") {\r\n  await jobs.markFailed(job.batchId, batch.error);\r\n} else {\r\n  await jobs.reschedule(job.batchId);\r\n}\r\n```\r\n\r\n批次状态为 `accumulating`、`processing`、`completed` 或 `failed`。`waitForBatch()` 使用有上限的指数退避；其 `timeoutMs` 覆盖整个等待过程，未传时为 300,000ms。\r\n\r\n## Client API\r\n\r\n### 提交事件\r\n\r\n```js\r\nconst acceptance = await client.submitEvent({\r\n  assistantId: \"assistant-id\", // 可省略，使用客户端默认值\r\n  channel: \"website\",\r\n  externalContactId: \"visitor_001\",\r\n  externalMessageId: \"website_message_123\",\r\n  content: \"请问什么时候发货？\",\r\n  conversationId: savedConversationId || undefined,\r\n  requestId: traceId,\r\n  signal,\r\n});\r\n```\r\n\r\n该调用发送 `POST /v1/chat/events`，成功时返回：\r\n\r\n```js\r\n{\r\n  batchId,\r\n  status: \"accumulating\",\r\n  duplicate,\r\n  conversationId,\r\n  raw,\r\n}\r\n```\r\n\r\n### 查询与等待\r\n\r\n```js\r\nconst batch = await client.getBatch(batchId, { signal, requestId });\r\n\r\nconst result = await client.waitForBatch(batchId, {\r\n\ttimeoutMs: 300_000,\r\n  pollIntervalMs: 250,\r\n  maxPollIntervalMs: 2_000,\r\n  backoffFactor: 2,\r\n  signal,\r\n  requestId,\r\n});\r\n```\r\n\r\n完成结果：\r\n\r\n```js\r\n{\r\n  id,\r\n  content,\r\n  conversationId,\r\n  messageId,\r\n  assistantName,\r\n  citations,\r\n  handoff,\r\n  usage,\r\n  raw, // completed 批次中的原始 ChatCompletion\r\n}\r\n```\r\n\r\n## 适配器与会话\r\n\r\n适配器只固定渠道并提交事件：\r\n\r\n```js\r\nimport {\r\n  createAppAdapter,\r\n  createWebsiteAdapter,\r\n  createWorkWechatAdapter,\r\n} from \"@0xtalk/channel-sdk\";\r\n\r\nconst app = createAppAdapter(client);\r\nconst website = createWebsiteAdapter(client);\r\nconst workWechat = createWorkWechatAdapter(client);\r\nconst custom = client.createAdapter({ channel: \"mini_program\" });\r\n\r\nconst acceptance = await workWechat.submitEvent({\r\n  externalContactId: inbound.FromUserName,\r\n  externalMessageId: inbound.MsgId,\r\n  conversationId: savedConversationId,\r\n  content: inbound.Content,\r\n});\r\n```\r\n\r\n长生命周期进程可用 session 自动带回 202 响应中的 `conversationId`：\r\n\r\n```js\r\nconst session = website.session(\"visitor_001\", {\r\n  conversationId: savedConversationId,\r\n});\r\n\r\nconst acceptance = await session.submitEvent(\"怎么申请退款？\", {\r\n  externalMessageId: \"website_message_124\",\r\n});\r\n\r\nawait jobs.upsertByBatchId({\r\n  batchId: acceptance.batchId,\r\n  externalContactId: \"visitor_001\",\r\n});\r\n\r\nconsole.log(session.conversationId);\r\nsession.reset();\r\n```\r\n\r\nsession 只保存内存中的会话 ID，不等待答案。生产环境仍应持久化 `acceptance.conversationId`。\r\n\r\n## 历史与会话\r\n\r\n```js\r\nconst conversation = await client.getConversation(conversationId);\r\nconst history = await client.listMessages(conversationId, { limit: 50 });\r\n```\r\n\r\n调用方必须验证该会话属于当前应用用户。续聊时只提交当前入站事件和 `conversationId`，不要重放完整 UI 历史。\r\n\r\n## 错误与取消\r\n\r\n```js\r\ntry {\r\n\tawait client.waitForBatch(batchId, { timeoutMs: 300_000, signal });\r\n} catch (error) {\r\n  if (error instanceof AstraDeskError) {\r\n    console.error(error.code, error.status, error.requestId, error.message);\r\n  }\r\n}\r\n```\r\n\r\n常见本地错误码：\r\n\r\n- `batch_timeout`：批次未在总超时内完成。\r\n- `request_aborted`：调用方取消，或单次 HTTP 请求超时。\r\n- `batch_failed`：服务端批次失败且未提供更具体的业务码。\r\n- `network_error`：无法连接 AstraDesk。\r\n- `invalid_response`：服务端响应不符合契约。\r\n\r\nHTTP 错误和失败批次会尽量保留服务端业务码、消息、请求 ID 与原始响应。\r\n\r\n## API 一览\r\n\r\n```js\r\nclient.submitEvent(options)\r\nclient.getBatch(batchId, options?)\r\nclient.waitForBatch(batchId, options?)\r\nclient.getConversation(conversationId, options?)\r\nclient.listMessages(conversationId, options?)\r\nclient.createAdapter({ channel, assistantId? })\r\n\r\nadapter.submitEvent(input)\r\nadapter.session(externalContactId, options?)\r\n\r\nsession.submitEvent(content, options)\r\nsession.messages(options?)\r\nsession.reset()\r\n```\r\n\r\n## 自测与打包预览\r\n\r\n```bash\r\ncd sdk/javascript\r\nnpm test\r\nnpm pack --dry-run\r\n```\r\n","readmeFilename":"README.md"}