呼叫介面
本頁說明 Go SDK 呼叫 OpenAPI 的通用規則:REST 介面怎樣對應到 SDK 的方法和引數,可選引數怎樣表示,怎樣翻頁、讀取批次介面的結果、判斷錯誤,SDK 會自動重試哪些請求、被限流時怎樣等待,以及檔案上傳、應用狀態的影響和排查問題的方法。
各介面的引數和返回欄位的含義以服務端 REST API 為準,本頁不再重複;每個方法對應的介面見服務與方法。
| 要做什麼 | 看哪裡 |
|---|---|
| 把操作開放給使用者之前要檢查什麼 | 應用許可權與使用者可見性 |
找到方法,寫引數、可選引數和 null | 命名規則、可選引數與零值 |
| 翻頁、遍歷全部資料、增量匯出訊息 | 分頁 |
| 讀取批次介面的逐項結果、批次匯入 | 批次介面的結果 |
| 判斷錯誤 | 錯誤型別與判斷 |
| 瞭解哪些請求會自動重試、避免重複傳送 | 重試與冪等 |
| 處理限流、給後臺任務限速 | 限流 |
| 上傳、下載檔案 | 檔案上傳與下載 |
| 日誌、指標和單元測試 | 日誌與排查 |
| AI 機器人的流式回覆 | AI 流式回覆 |
應用許可權與使用者可見性
服務端 SDK 以應用的身份呼叫介面,不知道你的業務系統中當前登入的是誰,也不會替你檢查許可權。把操作開放給瀏覽器之前,你的後端要檢查使用者的登入狀態、會話是否屬於他、他是否是群成員和管理員。
Messages.Recall是管理員撤回,不受使用者撤回時限和角色的限制,不能直接當作“使用者撤回”的介面。Conversations.Messages、AllMessages讀到的是管理視角的全部記錄,不會按使用者過濾(各自刪除、清空的訊息,入群前、退群后的訊息)。使用者的聊天介面應通過客戶端 SDK,或以使用者的登入令牌(User Token)讀取他能看到的記錄。- Client Secret、App Token 和使用者的登入令牌都只儲存在後端,不發給瀏覽器。
服務端 Demo 演示了服務端 SDK 和以使用者身份呼叫的介面怎樣配合;需要使用者長連線的,使用 Go 客戶端 SDK。
命名規則
SDK 的方法與 REST 介面一一對應。客戶端按模組分為 13 個服務:
| 服務 | 內容 | REST 文件 |
|---|---|---|
client.Users | 使用者、登入憑證、登入裝置、全域性禁言 | 使用者管理 |
client.Friends | 好友、好友申請、加好友方式 | 好友 |
client.Blacklist | 使用者的黑名單 | 黑名單 |
client.Groups | 群、群成員、群黑名單、入群申請、使用者的入群設定 | 群組管理 |
client.Messages | 傳送、批次傳送、查詢、匯出、撤回、編輯、置頂訊息 | 傳送訊息 |
client.Conversations | 會話資訊、會話訊息、使用者的會話狀態和未讀數 | 查詢會話 |
client.Media | 上傳、下載、查詢和刪除檔案 | 檔案 |
client.Push | 推送設定、會話免打擾、推送裝置 | 推送 |
client.Chatrooms | 聊天室、成員、禁言、封禁、白名單、屬性、訊息 | 聊天室管理 |
client.Moderation | 內容安全的配置、詞庫、稽核記錄、違規、舉報 | 內容安全 |
client.Calls | 音視訊通話的查詢、結束和統計 | 音影片 |
client.Channels | 獨立 RTC 頻道、票據、成員、統計與更正 | 頻道 |
client.Presence | 線上狀態 | 線上狀態 |
方法和引數的對應方式:
- 路徑引數是方法的位置引數,按路徑中的順序排列,在
ctx之後。例如PATCH /users/{username}為client.Users.Update(ctx, username, params),PUT /groups/{group_id}/members/{username}/role為client.Groups.SetRole(ctx, groupID, username, role)。 - 查詢引數和請求體欄位在引數結構體中,型別名為“動作 + 物件 +
Params”,如CreateUserParams、SendMessageParams、ListUsersParams。只有一兩個引數的介面直接作為位置引數,如client.Groups.Delete(ctx, groupID, reason)。 - 欄位名由 JSON 欄位名轉為 Go 的寫法:
snake_case轉為CamelCase,縮寫全大寫,如client_msg_id為ClientMsgID,avatar_url為AvatarURL,to_user為ToUser。 - 結果是對應的型別,欄位帶 JSON 標籤,如
*drim.User、*drim.SendResult。沒有響應體的介面(204 No Content)只返回error。 - 每個結果型別都有
Raw欄位:服務端返回的原始 JSON。服務端以後新增的欄位,不必等 SDK 升級就能從中讀取;不認識的欄位和列舉值不會報錯。 - 自由內容是
json.RawMessage:訊息的Body、Ext等原樣傳送和返回,寫法逐位元組保留(傳送時只去掉空白)。用drim.JSON(s)把 JSON 文本轉為它,或用drim.Marshal(v)編碼 Go 的值(不把<、>、&轉義)。 - 時間是
time.Time:請求中按 RFC 3339 傳送,響應解析為time.Time。 - 使用者名稱不區分大小寫:服務端把使用者名稱轉為小寫,響應中的使用者名稱都是小寫。SDK 不改動你傳入的使用者名稱;用響應中的使用者名稱作為鍵(如批次結果的失敗項)時,按小寫比較。
可選引數與零值
引數結構體中,零值的欄位不出現在請求中(不傳)。有三種寫法:
| 欄位型別 | 含義 | 寫法 |
|---|---|---|
普通型別(string、bool、int 等) | 零值即“不傳”,零值本身沒有特別含義 | Nickname: "张三" |
指標(*string、*int64、*bool 等) | nil 為“不傳”,零值有意義(如空字串表示清空、false 表示關閉) | drim.String("")、drim.Int64(3600)、drim.Bool(false) |
drim.Field[T] | 區分“不傳”、“傳 null”和“傳值”三種狀態 | 零值不傳,drim.Null[T]() 傳 null,drim.Set(v) 傳值 |
drim.String、drim.Int、drim.Int64、drim.Bool、drim.Float64 返回值的指標。
Field 用於 null 另有含義的欄位,如修改使用者資料時 attributes 為 null 清空全部自定義屬性、某一項為 null 刪除這一項:
// 修改昵称,设置 vip 属性、删除 level 属性,其他属性保持不变
user, err := client.Users.Update(ctx, "zhangsan", drim.UpdateUserParams{
Nickname: drim.String("张三"),
Attributes: drim.Set(map[string]drim.Field[string]{
"vip": drim.Set("1"),
"level": drim.Null[string](),
}),
})
if err != nil {
return err
}
fmt.Println(user.Nickname, user.Version)
// 清空全部自定义属性
_, err = client.Users.Update(ctx, "zhangsan", drim.UpdateUserParams{
Attributes: drim.Null[map[string]drim.Field[string]](),
})
return err併發修改
可以修改的物件帶版本號(使用者的 Version、群的 InfoVersion 等),修改類引數中有可選的 Version:給出時只有版本一致才修改,否則返回 version_conflict;不給時直接覆蓋。見併發修改與 version。SDK 不自動重試 version_conflict,也不自動讀取最新版本。
本地檢查
SDK 在發出請求之前檢查一部分引數,不合規的不傳送,返回錯誤碼為 invalid_params 的 *drim.Error,Details["field"] 指出是哪個引數:
- 缺少必填的引數(如發訊息時
ToUser和ToGroup必須恰好給一個、Type和Body必填); - 路徑引數為空字串、
.或..; - 批次介面的項數超過上限(如每次最多 100 個使用者名稱);
ClientMsgID、WithRequestID的格式不對;- 請求體超過 1 MiB(錯誤碼為
payload_too_large,StatusCode為 0)。
與應用設定有關的規則(如訊息大小上限、人數上限)由服務端檢查。
分頁
列表介面有兩個方法:逐頁的方法返回一頁(如 List),逐項遍歷的方法自動翻頁(如 All),返回 Go 1.23 的迭代器 iter.Seq2[T, error]。
自動翻頁
// 遍历全部正常状态的用户
for user, err := range client.Users.All(ctx, drim.ListUsersParams{Status: "active"}) {
if err != nil {
return err // 某一页最终失败:之前产出的项有效
}
fmt.Println(user.Username, user.Nickname)
}
return nil- 每一頁的請求照常經過重試和限流等待;某一頁最終失敗時,迭代器產出這個錯誤後結束。
- 是否結束只看服務端給出的下一頁標記,不看條數:一頁的條數可能少於
Limit甚至為 0,而後面還有資料。 break跳出迴圈即停止,不再請求下一頁。- 服務端返回的下一頁游標與這一頁相同時,迭代器產出
invalid_response錯誤後結束,避免死迴圈。
逐頁讀取
需要自己控制翻頁(如分批處理、中斷後從上次的位置繼續)時,用逐頁的方法。游標分頁的引數嵌入了 drim.ListOptions(Cursor、Limit),結果是 *drim.Page[T]:
// 从上次保存的游标继续,每页 100 个
cursor := loadCursor()
for {
page, err := client.Users.List(ctx, drim.ListUsersParams{
Status: "active",
ListOptions: drim.ListOptions{Cursor: cursor, Limit: 100},
})
if err != nil {
return err
}
for _, user := range page.Items {
fmt.Println(user.Username)
}
if page.NextCursor == "" {
return nil // 没有下一页
}
cursor = page.NextCursor
saveCursor(cursor)
}Limit為 0 時用服務端的預設值(大多數列表預設 20、最多 100),見分頁。NextCursor為空表示沒有下一頁。游標原樣傳回,不要解析;游標只在同樣的篩選條件下有效,改變條件要從第一頁開始。
其他翻頁方式
| 方式 | 方法 | 結果型別 | 說明 |
|---|---|---|---|
| 游標 | 大多數列表,如 Users.List、Groups.List | *Page[T] | 遍歷方法為 All、AllMembers 等,見服務與方法 |
| 帶版本號的列表 | Friends.List、Blacklist.List、Groups.Members、Push.Mutes | *VersionedPage[T] | 引數中的 KnownVersion 只在第一頁有效:與當前版本相同時 NotModified 為 true,Items 為空。另有 Version、Count(好友數、黑名單人數、群成員數)、MaxCount(好友和黑名單的上限)。遍歷方法不使用 KnownVersion |
| 按序號 | Conversations.Messages | *SeqPage | HasMore、NextAfterSeq、NextBeforeSeq;按 AroundSeq 取時另有 HasMoreBefore、HasMoreAfter。遍歷方法為 Conversations.AllMessages(不支援 AroundSeq) |
| 按訊息 ID 匯出 | Messages.Export | *ExportPage | HasMore、NextAfterMessageID。遍歷方法為 Messages.ExportAll |
| 迴圈刪除 | Friends.DeleteAll | 刪除的總數 | SDK 迴圈呼叫到 has_more 為 false,出錯時返回已刪除的數和錯誤,再次呼叫即可繼續 |
| 不分頁 | 如 Users.Sessions、Push.Devices、Moderation.WordLists、Chatrooms.RecentMessages | 切片 | 沒有遍歷方法 |
只在資料有變化時才讀取好友列表:
// 第一页带上本地保存的版本号;没有变化时只返回版本号
page, err := client.Friends.List(ctx, "zhangsan", drim.ListFriendsParams{KnownVersion: drim.Int64(localVersion)})
if err != nil {
return err
}
if page.NotModified {
return nil
}
var friends []drim.Friend
for friend, err := range client.Friends.All(ctx, "zhangsan", drim.ListFriendsParams{}) {
if err != nil {
return err
}
friends = append(friends, friend)
}
saveFriends(page.Version, friends)
return nil遍歷期間資料有變化時,SDK 不重新開始;需要時以新的版本號再取一次。
匯出應用的全部訊息(按訊息 ID 從舊到新,只匯出 30 秒之前傳送的),中斷後從最後一條繼續;遍歷結束後儲存 lastMessageID,下次從它繼續即為增量匯出:
for msg, err := range client.Messages.ExportAll(ctx, drim.ExportParams{AfterMessageID: lastMessageID, Limit: 1000}) {
if err != nil {
return err // 下次从 lastMessageID 之后继续
}
if err := archive(msg); err != nil {
return err
}
lastMessageID = msg.MessageID
}
return nilConversations.AllMessages 給出 AfterSeq(或只給 StartTime)時向新的方向遍歷(序號從小到大),否則從 BeforeSeq(不給為最新)向舊的方向遍歷(序號從大到小)。
批次介面的結果
批次介面整體返回 200,逐項給出結果,見批次介面。引數不合規、額度不足等整體的錯誤仍以 error 返回;單項的失敗在結果中,型別為 drim.ItemError(不是 *drim.Error):
| 欄位 | 說明 |
|---|---|
Key | 項的標識:使用者名稱(小寫)、訊息 ID、記錄 ID、屬性的鍵等 |
Code | 錯誤碼,取值與 *drim.Error 的相同 |
Message | 說明,只用於日誌;有的介面沒有 |
Details | 附加資訊,可能為 nil;Reason() 返回 details.reason |
每個批次介面有自己的結果型別,都有三個方法:FailedItems() 返回失敗的項,OK() 表示是否全部成功,Err() 在有失敗時返回彙總的錯誤(*drim.BatchError,其中 Failed 為失敗的項),否則返回 nil。
| 方法 | 結果型別 | 成功的項 | 失敗的項 |
|---|---|---|---|
Users.BatchCreate | BatchCreateUsersResult | Created | Failed(沒有 Details) |
Friends.BatchAdd | BatchAddFriendsResult | Added(含已是好友的) | Failed |
Friends.BatchDelete | BatchDeleteFriendsResult | Deleted | Failed |
Blacklist.BatchAdd、BatchRemove | BatchBlacklistResult | Added 或 Removed | Failed |
Groups.Create、AddMembers、RemoveMembers | CreateGroupResult、GroupMembersResult | Results 中 Error 為 nil 的,Status 為 joined、already_member、removed、not_member 等 | Results 中 Error 不為 nil 的 |
Messages.BatchSend | BatchSendResult | Results 中 Status 為 sent、duplicate 的 | Results 中 Error 不為 nil 的 |
Messages.BatchRecall | BatchRecallResult | Results 中 Error 為 nil 的 | 同左,Error 不為 nil 的 |
Chatrooms.Kick、Mute、Unmute、Ban、Unban、AddToAllowlist、RemoveFromAllowlist | RoomBatchResult | Results 中 Error 為 nil 的,Result 為結果 | Results 中 Error 不為 nil 的 |
Chatrooms.SetAttributes、RemoveAttributes | RoomAttributesChange | 不在 Failed 中的鍵 | Failed(鍵為屬性名) |
Moderation.AddWords | AddWordsResult | Added(個數)、Duplicated | Invalid(Key 為詞條) |
Moderation.BatchDecide | BatchDecideResult | Results 中 Item 不為 nil 的 | Results 中 Error 不為 nil 的 |
Media.DownloadURLs | []DownloadURLItem | DownloadURL 不為空的 | Error 不為 nil 的 |
result, err := client.Groups.AddMembers(ctx, groupID, []string{"lisi", "wangwu", "zhaoliu"})
if err != nil {
return err // 整个请求失败,没有任何一项执行
}
for _, item := range result.FailedItems() {
if item.Code == drim.CodePermissionDenied && item.Reason() == "group_blacklisted" {
log.Printf("%s 在群黑名单中,没有加入", item.Key)
continue
}
log.Printf("%s 加入失败:%s", item.Key, item.Code)
}
return nil自動拆分
Users.BatchCreateAll、Friends.BatchAddAll、Messages.BatchSendAll 按介面的上限拆分、依次提交、合併結果,用於匯入使用者、群發等場景:
- 服務端以
too_many_items給出更小的上限時(如應用的max_recipients_per_message),按它重新拆分。 BatchSendAll的每一批用同一個ClientMsgID,服務端按接收人去重,重複提交不會重複傳送。- 某一批整體失敗時停止,返回已完成部分的合併結果和這一批的錯誤。沒有出現在結果中的項可以再提交一次。
- 合併後的
Raw為{"batches":[各批的原始 JSON]}。 - 每一批按項數計入 OpenAPI 額度,與本地限速配合使用,把額度留給線上請求。
// 从旧系统迁移用户:每批 100 个,本地限速每秒 200 项
client, err := drim.New(drim.Config{
BaseURL: "https://im.example.com",
OrgName: "1100250925",
AppName: "demo",
ClientID: os.Getenv("IM_CLIENT_ID"),
ClientSecret: os.Getenv("IM_CLIENT_SECRET"),
RateLimit: &drim.RateLimit{PerSecond: 200},
})
if err != nil {
return err
}
defer client.Close()
var users []drim.CreateUserParams
for i := 0; i < 1000; i++ {
users = append(users, drim.CreateUserParams{Username: fmt.Sprintf("legacy-%04d", i), Nickname: fmt.Sprintf("用户 %d", i)})
}
result, err := client.Users.BatchCreateAll(ctx, users)
if result != nil {
log.Printf("创建了 %d 个", len(result.Created))
for _, item := range result.FailedItems() {
if item.Code != drim.CodeAlreadyExists { // 已存在的视为上次已导入
log.Printf("%s 失败:%s", item.Key, item.Code)
}
}
}
return err // 不为 nil 时,某一批整体失败,之后的没有提交錯誤型別與判斷
SDK 返回的錯誤都是 *drim.Error(批次介面的單項失敗和回撥處理的錯誤除外),用 errors.As 取得:
| 欄位 | 說明 |
|---|---|
StatusCode | HTTP 狀態碼;SDK 自己的錯誤為 0(invalid_response 為實際的狀態碼) |
Code | 服務端的錯誤碼,或 SDK 自己的錯誤碼 |
Message | 服務端的中文說明,只用於日誌,不要用它判斷 |
Details | details 原樣,沒有時為 nil;Reason() 返回 details.reason |
RequestID | 這次請求的 ID,排查問題時提供它 |
RetryAfter | 響應頭 Retry-After |
Op | 介面名,如 users.create;換取令牌失敗時為 app.token |
Attempts | 這次呼叫一共嘗試了幾次 |
Retried | 這個錯誤來自自動重試,而之前的嘗試結果不明,見重試與冪等 |
Err | 底層錯誤(網路錯誤、context.Canceled、context.DeadlineExceeded 等),Unwrap 返回它 |
err.Error() 形如 users.create: 409 already_exists: 用户名已被占用 (request_id=...),不含請求體和令牌,可以直接寫入日誌。
服務端的錯誤碼
服務端的錯誤碼和 details 的取值見錯誤碼,SDK 為每個錯誤碼定義了常量,如 drim.CodeNotFound、drim.CodeRateLimited;服務端以後新增的錯誤碼照常放在 Code 中。按 Code 判斷,需要區分原因時再看 Reason() 和 Details:
_, err := client.Groups.AddToBlacklist(ctx, groupID, "lisi", "广告")
var e *drim.Error
switch {
case err == nil:
return nil
case drim.IsCode(err, drim.CodeNotFound):
return fmt.Errorf("群不存在或已解散")
case drim.IsReason(err, drim.CodeLimitExceeded, "group_blacklist_limit"):
return fmt.Errorf("群黑名单已满")
case drim.IsCode(err, drim.CodePermissionDenied):
return fmt.Errorf("不能把群主加入群黑名单")
case errors.As(err, &e) && e.Code == drim.CodeInvalidArgument:
log.Printf("参数错误:%v,字段 %v", e.Reason(), e.Details["field"])
return err
default:
return err
}判斷函式:
| 函式 | 為真的情況 |
|---|---|
drim.IsCode(err, code) | err 是錯誤碼為 code 的 *drim.Error |
drim.IsReason(err, code, reason) | 錯誤碼為 code 且 details.reason 為 reason |
drim.IsTemporary(err) | 網路錯誤、超時、internal、5xx、rate_limited:稍後可以再試 |
drim.IsServiceUnavailable(err) | 服務暫停或只讀:tenant_unavailable、app_unavailable,以及因此進入冷卻期的 client_unavailable |
drim.IsPermanentlyUnavailable(err) | 應用已刪除、租戶已登出(unauthenticated 的 app_deleted、tenant_closed),以及因此永久失效的客戶端返回的 client_unavailable:應清除儲存的憑據 |
SDK 自己的錯誤碼
| 錯誤碼 | 常量 | 含義 |
|---|---|---|
network_error | CodeNetworkError | 連線失敗、連線中斷、TLS 錯誤(如證書校驗失敗);Err 為底層錯誤 |
timeout | CodeTimeout | 某次嘗試超時,或 ctx 的截止時間已到;errors.Is(err, context.DeadlineExceeded) 為真 |
canceled | CodeCanceled | ctx 被取消;errors.Is(err, context.Canceled) 為真 |
invalid_params | CodeInvalidParams | 本地檢查不通過,Details["field"] 指出引數 |
payload_too_large | CodePayloadTooLarge | 請求體超過 1 MiB,沒有傳送(與服務端的錯誤碼同名,StatusCode 為 0) |
invalid_response | CodeInvalidResponse | 響應不是預期的 JSON(如閘道器返回的 HTML 頁面),Details["body_prefix"] 為響應體的前 512 位元組 |
client_unavailable | CodeClientUnavailable | 客戶端已關閉、已永久失效,或處於換取令牌失敗後的冷卻期;Details["cause"] 為原因(closed 或最初的錯誤碼) |
重試與冪等
SDK 為每個介面標明能否安全地重試,按它決定網路錯誤、超時、5xx 時是否自動重試(每個方法的類別見服務與方法):
| 類別 | 介面 | 網路錯誤、超時、5xx |
|---|---|---|
| 查詢 | 所有 GET,以及查詢性質的 POST(按使用者名稱批次查詢、檢查好友關係、換取下載地址、查詢線上狀態、檢查文本等) | 自動重試 |
| 帶去重 ID 的寫操作 | 傳送訊息、批次傳送單聊、傳送聊天室訊息(client_msg_id) | 用同一個去重 ID 自動重試 |
| 以唯一標識建立 | 建立使用者、批次建立使用者(使用者名稱) | 自動重試 |
| 本身冪等的寫操作 | 重複呼叫結果相同或無害的:修改、刪除、封禁、拉黑、加人和移人、禁言、撤回、置頂、標記已讀、完成上傳、簽發登入憑證、代使用者舉報、不帶處置或帶 Version 的稽核結論等 | 自動重試 |
| 不冪等的寫操作 | 建群、建立聊天室、建立上傳、建立詞庫、轉交平臺、設定密碼、踢掉全部裝置、只推線上的訊息、低優先順序的聊天室訊息、帶處置而不帶 Version 的稽核結論 | 請求可能已經發出的不重試,直接返回錯誤 |
- 重試按 0.5、1、2 秒(各乘以 0.5 到 1.5 的隨機數)退避,預設最多重試 3 次;服務端給了
Retry-After的不短於它。次數用Config.Retry或drim.WithMaxAttempts修改。 - 連線沒有建立成功(DNS 解析失敗、連線被拒絕)的請求一定沒有發出,所有類別都會重試。證書校驗失敗不重試。
- 被限流的請求沒有執行,所有類別都會在等待後重發,見限流。
- App Token 失效(
unauthenticated)時換取新令牌後重發一次,見App Token 的獲取與快取。
重試之後的結果
前一次嘗試的結果不明(如連線中斷)而重試的,前一次可能已經執行了。重試後得到以下錯誤時,錯誤的 Retried 為 true,可以按“前一次已經執行”處理:
- 建立使用者得到
already_exists:使用者是前一次嘗試建立的; - 刪除得到
not_found:前一次已經刪除; - 帶
Version的修改得到version_conflict:前一次已經修改,重新讀取看到的是修改後的資料。
_, err := client.Users.Create(ctx, drim.CreateUserParams{Username: "zhangsan", Nickname: "张三"})
var e *drim.Error
if errors.As(err, &e) && e.Code == drim.CodeAlreadyExists && e.Retried {
err = nil // 前一次尝试已经创建
}
return err傳送訊息的去重 ID
傳送訊息、傳送聊天室訊息時,SDK 在你沒有給出 ClientMsgID 時自動生成一個(32 個十六進位制字元),使自動重試不會重複傳送。建議用業務單號作為 ClientMsgID(1 到 64 個可見 ASCII 字元,不能以 evt_、sys_ 開頭),這樣業務層自己的重試也不會重複傳送:
- 同一會話中重複的
ClientMsgID不再發送,結果的Duplicate為true,返回的是第一次傳送的訊息; - 批次傳送單聊的
ClientMsgID必填,服務端按接收人去重,結果中重複的項Status為duplicate; - 只推線上的訊息(
OnlineOnly)和低優先順序的聊天室訊息不去重,SDK 不生成去重 ID,也不自動重試。
代使用者舉報的結果同樣有 Duplicate(同一舉報人對同一物件只記一次)。
不冪等的操作超時以後
不冪等的寫操作超時或連線中斷後,先查詢結果再決定是否重試:
- 建群:按群主查詢使用者加入的群(建群時把業務 ID 寫進群的
Attributes,便於確認); - 建立聊天室:按名稱或所有者查詢聊天室列表;
- 建立詞庫:用
Moderation.WordLists按名稱查詢; - 簽發登入憑證可以直接再簽發一個,只把最後拿到的交給 App。
限流
OpenAPI 按應用限流,超出時返回 429 rate_limited 和 Retry-After,見限流與配額。SDK 收到 rate_limited 時按 Retry-After 等待(加 0 到 20% 的隨機量)後重發,預設最多 3 次。等待的範圍按 details.reason 決定:
details.reason | 限制 | 一起等待的請求 |
|---|---|---|
| 沒有 | 應用的 OpenAPI 額度 | 整個客戶端:之後發出的請求都先等到同一時刻,而不是各自重試 |
app_message_rate | 應用每分鐘的訊息數 | 這個客戶端中傳送訊息的請求(Messages.Send、BatchSend);等待時長在 Retry-After 到它的 2 倍之間隨機 |
request_rate | 代使用者舉報的頻率 | 這個客戶端中代使用者舉報的請求 |
其他,如 group_send_rate、room_send_rate、image_busy | 某個群、聊天室、檔案的限制 | 只有這個請求 |
Retry-After超過MaxRetryAfter(預設 60 秒)的不等待,直接返回rate_limited:通常是額度已用完,應由你的任務佇列決定何時再試。- 等待算在
ctx的時限內:等待會超過截止時間時不再等待,直接返回rate_limited。 - 等待重發
MaxRateLimited次(預設 3 次)後仍被限流的,返回rate_limited。錯誤的RetryAfter為服務端給出的等待時間。 - 不想在線上請求中等待的,用
drim.WithMaxRetryAfter(ctx, 0),被限流時立即返回。
本地限速
批次匯入、群發等後臺任務可以在本地限速,把應用的額度留給線上請求:
// 后台任务专用的客户端:每秒最多 200 次,批量接口按项数计
client, err := drim.New(drim.Config{
BaseURL: "https://im.example.com",
OrgName: "1100250925",
AppName: "demo",
ClientID: os.Getenv("IM_CLIENT_ID"),
ClientSecret: os.Getenv("IM_CLIENT_SECRET"),
RateLimit: &drim.RateLimit{PerSecond: 200, Burst: 200},
})
if err != nil {
return err
}
defer client.Close()- 本地限速是令牌桶:
PerSecond為每秒的請求數,Burst為桶的容量(預設為PerSecond向上取整,至少 1)。 - 批次介面按項數計(與服務端的額度計算一致),一個請求最多扣到桶的容量。
- 本地限速的等待也算在
ctx的時限內,會超過截止時間時返回timeout。 - 線上請求和後臺任務分別建立客戶端,只給後臺任務的客戶端設定
RateLimit。
檔案上傳與下載
上傳檔案要經過建立上傳、上傳內容、完成上傳三步(見上傳檔案)。client.Media.Upload 一次完成:
f, err := os.Open("photo.jpg")
if err != nil {
return err
}
defer f.Close()
st, err := f.Stat()
if err != nil {
return err
}
file, err := client.Media.Upload(ctx, f, st.Size(), drim.UploadParams{Purpose: "attachment", Kind: "image"})
if err != nil {
return err
}
// 把文件地址(不是下载地址)写入图片消息
body, err := drim.Marshal(map[string]any{"url": file.URL, "size": file.Size, "width": file.Width, "height": file.Height})
if err != nil {
return err
}
_, err = client.Messages.Send(ctx, drim.SendMessageParams{From: "notice", ToUser: "zhangsan", Type: "image", Body: body})
return err- 內容的來源是
io.ReaderAt(*os.File、*bytes.Reader、*strings.Reader都實現了它),用於分片的併發讀取和重傳;只有io.Reader的(如網路流),先寫入臨時檔案。size為內容的位元組數。 - 不超過 32 MB 的一次上傳;更大的分片上傳,每片 16 MB,默認同時上傳 4 片(
Concurrency)。 - 上傳內容的請求直接發到物件儲存,不帶
Authorization;失敗時每片單獨重試 3 次,地址過期時重新索取。 - 完成上傳時服務端核對內容、生成縮圖,最長約 2 分鐘;SDK 按服務端的結果等待、重試或補傳缺少的分片,最終返回檔案資訊。
ctx被取消或到期時停止上傳,並盡力取消這次上傳(獨立的 5 秒時限,失敗不報錯)。
UploadParams 的欄位:
| 欄位 | 說明 |
|---|---|
Purpose | 用途:attachment(訊息附件)、group_file(群檔案)、user_avatar、group_avatar、chatroom_avatar |
Kind | 型別:image、voice、video、file;頭像只能是 image,群檔案不能是 voice |
Name | 檔名,型別為 file 和群檔案必填;最長 255 個字元,不能含 /、\ 和控制字元 |
ContentType | 只作提示,檔案的格式由服務端按內容識別 |
GroupID | 群檔案所在的群,群檔案必填 |
Owner | 所屬使用者,使用者頭像必填 |
Concurrency | 分片上傳的併發數,預設 4 |
OnProgress | 每個分片(一次上傳為整個檔案)完成後呼叫,引數為已上傳和總的位元組數;不能阻塞 |
SDK 在本地檢查用途與型別的組合、頭像不超過 5 MB、檔名的規則,以及群檔案的 GroupID 和使用者頭像的 Owner,不合規的返回 invalid_params。與應用設定有關的上限由服務端檢查。上傳到物件儲存失敗的錯誤 Op 為 media.upload 或 media.upload_part。
為使用者設定頭像
f, err := os.Open("avatar.png")
if err != nil {
return err
}
defer f.Close()
st, err := f.Stat()
if err != nil {
return err
}
file, err := client.Media.Upload(ctx, f, st.Size(), drim.UploadParams{Purpose: "user_avatar", Kind: "image", Owner: "zhangsan"})
if err != nil {
return err
}
_, err = client.Users.Update(ctx, "zhangsan", drim.UpdateUserParams{AvatarURL: drim.String(file.URL)})
return err續傳
分片上傳(超過 32 MB 的檔案)在 24 小時內(從建立上傳時算起)中斷的,可以用檔案 ID 和同一份內容續傳:ResumeUpload 查詢已上傳的分片,補傳缺少的和大小不對的,再完成上傳。續傳時 ctx 被取消只停止,不取消上傳,之後還可以再續傳。
Upload 失敗時不返回檔案 ID(ctx 被取消時還會取消這次上傳),所以需要續傳的大檔案,先用 CreateUpload 建立上傳並儲存檔案 ID,再用 ResumeUpload 上傳內容:第一次呼叫時沒有已上傳的分片,全部上傳;中斷後用同一個檔案 ID 再呼叫即可。
// 第一步:创建上传,把文件 ID 保存到你的数据库
func startUpload(ctx context.Context, client *drim.Client, size int64) (string, error) {
session, err := client.Media.CreateUpload(ctx, drim.CreateUploadParams{Purpose: "attachment", Kind: "video", Size: size})
if err != nil {
return "", err
}
return session.FileID, nil
}
// 第二步:上传内容并完成;中断后用同一个文件 ID 再调用
func uploadContent(ctx context.Context, client *drim.Client, fileID, path string) (*drim.File, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
st, err := f.Stat()
if err != nil {
return nil, err
}
return client.Media.ResumeUpload(ctx, fileID, f, st.Size(), drim.UploadParams{
OnProgress: func(uploaded, total int64) { log.Printf("已上传 %d / %d", uploaded, total) },
})
}下載
out, err := os.Create("download.jpg")
if err != nil {
return err
}
defer out.Close()
n, err := client.Media.Download(ctx, fileURL, out)
if err != nil {
return err
}
log.Printf("下载了 %d 字节", n)
return nilDownload 先換取下載地址,再直接請求它(不帶 Authorization,不經過 OpenAPI 的重試和限流),把內容寫入 io.Writer;下載地址過期時重新換取一次。縮圖的地址(檔案地址加 /thumb)同樣可以下載。下載地址只有 60 到 90 分鐘有效,需要下載地址本身時用 client.Media.DownloadURLs,不要儲存或寫進訊息。
Media.Download 只取回檔案內容,不替你設定業務 HTTP 響應的內容型別、快取和下載頭。把附件轉發給瀏覽器時,不要相信瀏覽器傳來的 MIME 型別;HTML、SVG 等可以執行指令碼的內容要強制下載,或放到單獨的域名下。服務端 Demo 的做法是:只有按內容識別為 PNG、JPEG、GIF、WebP 的直接顯示,其他都作為下載,並設定 nosniff、沙箱 CSP 和禁止快取。
處理待稽核的內容
client.Moderation.ProcessPending 遍歷租戶稽核佇列中待處理的記錄(pending、auto_violation),讀取內容後交給你的函式,按返回的結論處理:
decided, err := client.Moderation.ProcessPending(ctx, drim.ProcessOptions{Scene: "message"},
func(ctx context.Context, item drim.ModerationItem, content drim.ItemContent) (*drim.Decision, error) {
if needHumanReview(item) {
return nil, nil // 返回 nil 跳过这条
}
return &drim.Decision{Decision: "no_violation", Note: "自动复核通过"}, nil
})
log.Printf("处理了 %d 条", decided)
return err- 作出結論時帶上讀到的
Version,所以可以安全重試;記錄已被別人處理(version_conflict)的跳過。 - 你的函式返回錯誤或作出結論失敗時,預設記警告日誌後繼續下一條;
StopOnError為true時停止並返回錯誤。 - 返回作出結論的條數;取下一頁失敗時返回已作出的條數和這個錯誤。
引數和結論的取值見稽核記錄。
應用和租戶的狀態
應用或租戶的狀態變化時(見應用和租戶的狀態),SDK 的表現:
| 狀態 | 呼叫 OpenAPI 得到的結果 | 判斷 |
|---|---|---|
| 只讀(平臺設定或欠費) | 查詢和處置類介面正常;新增資料、修改資料返回 app_unavailable | drim.IsServiceUnavailable(err) |
| 應用停用、租戶暫停、登出冷靜期 | 已有的令牌立即失效;SDK 重新換取時得到 app_unavailable 或 tenant_unavailable,之後 2 到 5 分鐘內直接返回 client_unavailable,不再換取 | drim.IsServiceUnavailable(err) |
| 應用刪除、租戶登出 | unauthenticated(app_deleted、tenant_closed),客戶端永久失效 | drim.IsPermanentlyUnavailable(err) |
狀態恢復後,SDK 在冷卻期結束後的下一次請求自動換取新令牌,不需要重新建立客戶端。暫停期間發生的事件不回撥、恢復後也不補發,需要的資料用 OpenAPI 查詢或匯出補齊。
日誌與排查
日誌
SDK 預設不輸出日誌。把你的 *slog.Logger 交給 Config.Logger:
logger := slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelInfo}))
client, err := drim.New(drim.Config{
BaseURL: "https://im.example.com",
OrgName: "1100250925",
AppName: "demo",
ClientID: os.Getenv("IM_CLIENT_ID"),
ClientSecret: os.Getenv("IM_CLIENT_SECRET"),
Logger: logger,
})
if err != nil {
return err
}
defer client.Close()| 級別 | 內容 |
|---|---|
debug | 業務結果類的失敗(如 not_found、already_exists)、限流等待、從令牌儲存取得令牌 |
info | 換取到新令牌(含有效期,不含令牌)、更新憑據 |
warn | 請求失敗後將要重試、限流等待超過 5 秒、令牌儲存出錯 |
error | 最終失敗的網路錯誤、超時、5xx、unauthenticated、ip_not_allowed、method_not_allowed、invalid_response、client_unavailable,換取令牌失敗、客戶端永久失效 |
日誌帶 op(介面名)、status、code、reason、request_id、attempts 等欄位,不記錄 Client Secret、App Token、登入憑證、密碼、訊息內容和上傳、下載地址。其他 4xx 是你的業務結果,SDK 只記 debug,由你決定是否記錄。
請求 ID
每個請求都帶 X-Request-ID。沒有用 drim.WithRequestID 指定時由 SDK 生成,同一次呼叫的重試沿用同一個。出錯時 e.RequestID 為服務端返回的請求 ID,聯絡技術支援時提供它。
鉤子與統計
Config.Hooks 在每次嘗試的前後呼叫,用於注入追蹤頭、記錄指標(換取令牌的請求同樣呼叫,介面名為 app.token):
requests := map[string]int{} // 示例:按接口名和结果计数(实际请换成你的指标库)
var mu sync.Mutex
client, err := drim.New(drim.Config{
BaseURL: "https://im.example.com",
OrgName: "1100250925",
AppName: "demo",
ClientID: os.Getenv("IM_CLIENT_ID"),
ClientSecret: os.Getenv("IM_CLIENT_SECRET"),
Hooks: drim.Hooks{
BeforeRequest: func(ctx context.Context, req *http.Request) {
req.Header.Set("traceparent", traceparentFrom(ctx)) // 注入追踪头
},
AfterResponse: func(ctx context.Context, info drim.RequestInfo) {
mu.Lock()
requests[info.Op+" "+info.Code]++
mu.Unlock()
if info.Duration > time.Second {
log.Printf("慢请求 %s %s 第 %d 次尝试 %v request_id=%s", info.Method, info.URL, info.Attempt, info.Duration, info.RequestID)
}
},
},
})
if err != nil {
return err
}
defer client.Close()- 鉤子在發出請求的協程中同步呼叫,不能阻塞太久;鉤子 panic 時 SDK 不恢復。
BeforeRequest看到的請求中有Authorization和請求體,不要記錄它們。RequestInfo含Op、Method、URL(不含查詢引數)、Attempt、StatusCode(沒有收到響應時為 0)、Code(成功時為空)、Reason、RequestID、Duration和RateLimitWait(這次嘗試之前的限流等待)。
client.Stats() 返回按介面名統計的快照:Requests(嘗試的次數,含重試)、Failures(最終失敗的呼叫數)、Retries、RateLimitWait(限流等待的總時長),可以定期讀取後上報:
for op, s := range client.Stats().Ops {
log.Printf("%s 请求 %d 次,失败 %d 次,重试 %d 次,限流等待 %v", op, s.Requests, s.Failures, s.Retries, s.RateLimitWait)
}用測試服務模擬各種結果
drimtest.NewServer() 啟動一個假的 OpenAPI 服務,可以按介面名設定返回值或錯誤,並記錄收到的請求,用來測試你的程式碼在限流、出錯時的行為:
package notify_test
import (
"context"
"testing"
drim "github.com/DeepRespond-AI/deeprespond-im-server-go-sdk"
"github.com/DeepRespond-AI/deeprespond-im-server-go-sdk/drimtest"
)
func TestSendRetriesAfterRateLimit(t *testing.T) {
srv := drimtest.NewServer()
defer srv.Close()
// 后设置的规则先用:第一次发送被限流(只生效一次),SDK 等待 1 秒后重发,得到 201
srv.Respond("messages.send", 201, `{"message_id":"1840000000000000001","seq":7}`)
srv.Fail("messages.send", drim.CodeRateLimited, drimtest.FailOptions{RetryAfter: "1"})
client, err := drim.New(srv.Config())
if err != nil {
t.Fatal(err)
}
defer client.Close()
result, err := client.Messages.Send(context.Background(), drim.SendMessageParams{
From: "notice", ToUser: "zhangsan", ClientMsgID: "order-1001-paid", Type: "text", Body: drim.JSON(`{"text":"hi"}`),
})
if err != nil {
t.Fatal(err)
}
if result.Seq != 7 || result.Duplicate {
t.Fatalf("unexpected result: %+v", result)
}
calls := srv.Calls("messages.send")
if len(calls) != 2 || calls[1].JSON()["client_msg_id"] != "order-1001-paid" {
t.Fatalf("expected 2 sends with the same client_msg_id, got %d", len(calls))
}
}| 方法 | 說明 |
|---|---|
srv.Config() | 連到這個服務的 drim.Config(不使用代理) |
srv.Respond(op, status, body, opts...) | 設定介面的返回值;body 為 string、[]byte 時原樣傳送,其他值編碼為 JSON |
srv.Fail(op, code, drimtest.FailOptions{...}) | 讓介面返回錯誤,預設一次;Status 預設按錯誤碼,RetryAfter 為響應頭,Times 為 -1 時一直有效 |
srv.Handle(op, handler, opts...) | 用自己的 http.HandlerFunc 處理 |
drimtest.Times(n) | Respond、Handle 的選項:只生效 n 次。後設置的規則先用 |
srv.RevokeTokens() | 讓已簽發的 App Token 全部失效 |
srv.Calls(op)、srv.Requests() | 收到的請求:Op、Method、Path、Query、Header、Body,JSON() 解析請求體 |
op 是介面名,如 users.create、messages.send,換取令牌為 app.token;每個方法的介面名見服務與方法。沒有設定返回值的介面,有響應體的返回 200 和 {},沒有響應體的返回 204。
使用獨立頻道
下面建立一個影片頻道,併為一個使用者簽發加入頻道的票據(client 和 ctx 同前文):
func issueMeetingTickets(ctx context.Context, client *drim.Client) (*drim.RTCChannelTicketsResult, error) {
created, err := client.Channels.Create(ctx, "meeting-demo", drim.RTCChannelCreateParams{
Name: "项目会议", Media: "video", AccessMode: "ticket",
})
if err != nil { return nil, err }
return client.Channels.IssueTickets(ctx, created.Channel.ChannelID, drim.RTCChannelIssueTicketsParams{
Users: []drim.ChannelsIssueTicketsUsers{
{Username: "alice", Role: "publisher"},
},
})
}
// 调用方经已鉴权业务接口交付返回的票据,勿打印原文。- 引數型別為
RTCChannel…Params,結果為對應的型別。到期時間等欄位用Field[time.Time],區分不傳、null(清除)和設值。 - 用量更正用
Channels.UsageAdjustments。 - 頻道的列表沒有自動翻頁的方法,用返回的
NextCursor自己翻頁。 - 簽發票據不自動重試;移出成員、撤銷票據重試時,同一個操作要用同一個
OperationID。 - 頻道的方法只在較新的 SDK 中,見頻道版本;每個方法對應的介面見頻道方法。
遊戲房間
遊戲房間給多人線上遊戲提供即時對戰通道。你的服務端用 client.Game 建房、給玩家簽發加入票據、對局結束時結束房間;玩家加入房間和對戰由客戶端 SDK 完成。下面是一局對戰在服務端要做的事(client 和 ctx 同前文):
// startMatch 匹配成功后建房,给每个玩家签一张加入票据。
func startMatch(ctx context.Context, client *drim.Client, matchID string, players []string) (map[string]string, error) {
// 房间 ID 用你的对局 ID:重试或重复调用都只有一个房间
_, err := client.Game.CreateRoom(ctx, matchID, drim.CreateGameRoomParams{
MaxPlayers: drim.Int64(int64(len(players))),
StartMode: "all_ready",
Ext: json.RawMessage(`{"mode":"ranked"}`),
})
switch {
case drim.IsReason(err, drim.CodeUnavailable, "game_capacity_unavailable"):
return nil, errors.New("游戏服务繁忙,请稍后再试") // 大区暂时没有空闲的游戏节点
case drim.IsReason(err, drim.CodeLimitExceeded, "open_rooms"):
return nil, errors.New("同时进行的对局太多")
case err != nil:
return nil, err
}
users := make([]drim.GameTicketUser, len(players))
for i, name := range players {
users[i] = drim.GameTicketUser{Username: name, Seat: drim.Int64(int64(i))}
}
tickets, err := client.Game.IssueTickets(ctx, matchID, drim.IssueGameTicketsParams{Users: users})
if err != nil {
return nil, err // 不会自动重试;超时后直接再签一次,旧票据随之作废
}
byUser := make(map[string]string, len(tickets))
for _, t := range tickets {
byUser[t.Username] = t.Ticket // 经你的业务接口分别交给每个玩家,不要写进日志
}
return byUser, nil
}
// finishMatch 对局结束时带上结算结果结束房间。
func finishMatch(ctx context.Context, client *drim.Client, matchID, winner string) error {
result, _ := json.Marshal(map[string]string{"winner": winner})
res, err := client.Game.CloseRoom(ctx, matchID, drim.CloseGameRoomParams{Reason: "对局结束", Result: result})
if err != nil {
return err
}
// res.Accepted 为 false:房间之前已在结束(例如超时自动结束),这次的原因和结果不生效
_ = res.Accepted
return nil
}
// kickCheater 把作弊的玩家移出房间。
func kickCheater(ctx context.Context, client *drim.Client, matchID, username string) error {
res, err := client.Game.RemoveMember(ctx, matchID, username, drim.RemoveGameMemberParams{Reason: "作弊"})
if err != nil {
return err
}
_ = res.Removed // false:他已经不在房间里
return nil
}| 要做什麼 | 方法 | 注意 |
|---|---|---|
| 建房 | CreateRoom | 房間 ID 用你的對局 ID。同樣的引數重複呼叫返回同一個房間(Created 為假),網路錯誤時自動重試 |
| 簽發加入票據 | IssueTickets | 每次 1–10 人。不自動重試:超時後直接再籤一次,同一使用者之前沒用的票據隨之作廢。票據原文只返回這一次,不要寫進日誌 |
| 手動開局 | StartRoom | 只用於開局方式為 manual 的房間 |
| 結束房間 | CloseRoom | 結算結果原樣發給玩家和 game_room.closed 回撥。房間之前已在結束時 Accepted 為假,這次的原因和結果不生效 |
| 移出玩家 | RemoveMember | SDK 自動生成 OperationID,重試時不變。玩家不在房間裡時 Removed 為假 |
| 查詢 | GetRoom、ListRooms / AllRooms、ListMembers、ListRegions | 列表按建立時間從新到舊;狀態可以寫多個,用逗號分隔 |
- 可選引數用指標或空字串表示不傳,
drim.Int64(4)取得指標,見可選引數與零值。Ext和Result是json.RawMessage。 Created、Accepted、Removed由 HTTP 狀態碼得出,不是響應體中的欄位。- 常見錯誤:大區暫時沒有空閒的遊戲節點為
unavailable(game_capacity_unavailable),稍後重試,或在建房時給出候選大區;同時進行的房間數達到上限為limit_exceeded(open_rooms)。全部錯誤見遊戲房間的錯誤。 - 擴充套件欄位
ext不超過 2KB,結算結果result不超過 4KB;上限和計費見控制台、計費與限制。 - 對局結束後的結算回撥(
game_room.closed)見遊戲房間回撥。 - 每個方法對應的介面見遊戲房間方法,請求和返回的欄位見遊戲房間:服務端介面。遊戲房間的方法從 v1.0.3 起提供。
AI 流式回覆
AI 對話中,使用者給機器人發訊息後你會收到 ai.message_received 回撥,然後用 client.AI.StartStream 開始一條回覆,邊生成邊寫入。返回的 *drim.Stream 替你處理增量的編號與合批(滿 20 毫秒或 4 KB 發一段)、持續寫入與按批寫入的切換、失敗重發;連續三次寫入失敗時以 Fail("transport") 結束並返回錯誤。機器人管理用 client.AI.CreateBot 等方法,見服務與方法。
收到訊息後開始回覆
回撥處理函式應儘快返回,模型呼叫放到另一個協程中。ClientStreamID 用事件 ID,回撥重新投遞時不會開始第二條回覆:
handler, err := callback.NewHandler(callback.Config{
Secrets: callback.StaticSecrets{endpointID: {secret}},
Nonces: callback.NewMemoryNonceStore(),
Dedup: callback.NewMemoryDedupStore(0),
Events: callback.EventHandlers{
AIMessageReceived: func(ctx context.Context, ev *callback.Event, d *callback.AIMessageReceived) error {
var msg struct {
Seq int64 `json:"seq"`
Body struct {
Text string `json:"text"`
} `json:"body"`
}
if err := json.Unmarshal(d.Message, &msg); err != nil {
return err
}
stream, err := client.AI.StartStream(ctx, drim.StartStreamInput{
Bot: d.Bot, To: d.From, ReplyToSeq: msg.Seq, ClientStreamID: ev.ID,
})
if err != nil {
return err
}
go reply(stream, msg.Body.Text)
return nil
},
},
})StartStreamInput 的 Replace 為真時先結束會話中正在生成的回覆;BatchOnly 為真時只用按批寫入(執行環境不支援流式請求體時,如部分雲函式)。
對接模型
PipeOpenAI、PipeOpenAIResponses、PipeAnthropic、PipeGemini 讀完模型的流後自動 Finish(帶上結束原因、模型名和用量);模型返回錯誤時以 Fail("model_error", 说明) 結束並返回這個錯誤。引數可以是官方 SDK 的流(有 Next、Current、Err 方法,如 openai-go、anthropic-sdk-go 的 *ssestream.Stream),也可以是 SSE 格式的 HTTP 響應體(io.Reader)。相容 OpenAI 格式的模型(DeepSeek、通義千問、Kimi、豆包、vLLM、Ollama 等)都用 PipeOpenAI,reasoning_content 寫為思考過程:
func reply(stream *drim.Stream, question string) {
defer stream.Close() // 没有结束时以 Fail("canceled") 结束
resp := openaiClient.Chat.Completions.NewStreaming(stream.Context(), openai.ChatCompletionNewParams{
Model: "gpt-4.1",
Messages: []openai.ChatCompletionMessageParamUnion{openai.UserMessage(question)},
})
if _, err := stream.PipeOpenAI(resp); err != nil {
log.Printf("回复 %s 没有正常结束:%v", stream.ID, err)
}
}| 方法 | 轉換 |
|---|---|
PipeOpenAI | Chat Completions:delta.content → 正文,reasoning_content / reasoning → 思考過程,tool_calls → 工具呼叫(流結束時改為 done) |
PipeOpenAIResponses | Responses API:response.output_text.delta → 正文,reasoning_summary_text.delta → 思考過程,*_call 輸出項 → 工具呼叫 |
PipeAnthropic | Messages:text_delta → 正文,thinking_delta → 思考過程,tool_use 塊 → 工具呼叫 |
PipeGemini | generateContent:thought 為真的部分 → 思考過程,其餘 → 正文 |
手動寫入
_ = stream.WriteReasoning("先查询天气。")
_ = stream.WriteTool("call_1", "weather", "查询天气", drim.ToolRunning)
// … 调用工具 …
_ = stream.WriteTool("call_1", "weather", "查询天气", drim.ToolDone)
_ = stream.WriteText("明天上海**多云**,")
_ = stream.WriteText("最高 23℃。")
_ = stream.WriteImage(drim.ImagePart{URL: "https://cdn.example.com/a.png", Width: 800, Height: 600})
result, err := stream.Finish(ctx, drim.FinishOptions{Model: "gpt-4.1", Usage: &drim.AIUsage{InputTokens: 120, OutputTokens: 380}})Write*只放入記憶體,不等待網路,可以在多個協程中呼叫;與上一段同類型的文字追加到同一個片段。WriteTool用同一個 id 再次寫入時更新同一個片段。Flush(ctx)立即發出並等待確認;Finish先確認全部內容再結束。- 模型呼叫失敗時呼叫
stream.Fail(ctx, "model_error", "模型暂时不可用"),已生成的內容保留,使用者看到“生成中斷”和這句說明。 - 結束後再寫入返回錯誤。
停止生成
使用者點停止生成、回覆超時或被新的回覆替換時,stream.Context() 被取消,context.Cause 為 drim.ErrStreamStopped;之後的寫入返回 drim.CodeStreamClosed 的錯誤。把 stream.Context() 傳給模型請求,模型呼叫隨之中止;Pipe* 也會停止讀取並關閉模型的流。
callback.Handler 收到 ai.stream_stopped 時自動取消本程序中對應流的 Context,不需要註冊處理函式。流在另一個程序中時,那邊下一次傳送會收到 stream_closed,效果相同;自己處理回撥(如從佇列中取出事件)時呼叫 client.AI.StreamStopped(streamID)。
