@huai_xin/dsh-workbuddy 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -323,6 +323,35 @@ node test/live/policy-probe.mjs # 两版白名单与准入结果对照
323
323
  - **模型不可用**:`code 11102 model [...] service info not found`(例如目录里列了但 CLI 渠道不服务的 `glm-4.6` 等)→ `INVALID_REQUEST`,不重试。
324
324
  - **额度/限流**:其他 429 → `RATE_LIMIT`;401/403 → 先续期再重试一次;5xx → `SERVER`。
325
325
 
326
+ ### 流式回答怎么算"正常结束"(2026-10-22 起)
327
+
328
+ 判据是机械的:**只有带 `finish_reason`(且流走到 `data: [DONE]`)或交付了工具调用,才算正常结束**;其余一律视为异常,并按下表处理。这样处理的起因是一次真实故障排查:模型"话说一半就停"看起来像正常结束,实际可能是连接被掐断,而旧实现把它当成了 `stop` → 既不报错也不重试。
329
+
330
+ | 上游怎么结束 | 插件上报 | harness 会做什么 | 日志里能看到 |
331
+ | --- | --- | --- | --- |
332
+ | 没有 `finish_reason`、也没有 `[DONE]`(连接被掐断) | `TRANSPORT` 错误 | **自动退避重试**(默认策略含 `TRANSPORT`) | `… 的响应在 finish_reason/[DONE] 之前中断(收到 … kB,用时 …s,最后一段数据在 …s 前,中断时正在输出思考;可重试的传输错误)` |
333
+ | 同样的断流,但断在工具调用参数中间 | `TRANSPORT` 错误 | 自动重试 | `… 的工具调用参数在传输中被截断(…)` |
334
+ | `[DONE]` 但全程没有 `finish_reason` | `stop`(+ 记一笔) | 正常结束 | `… 的响应以 [DONE] 收尾但没有 finish_reason,按 stop 处理(输出 N tokens)` |
335
+ | 输出打满预算 / 工具参数被输出上限截断 | `max-tokens` | 由 harness 决定续跑 | `… 的输出达到上限,本步被截断(输出 N tokens)` |
336
+ | 正常 `stop` / `tool_calls` | 原样 | 正常 | 无 |
337
+
338
+ 重试本身由 harness 执行,插件只是把它记进日志:
339
+
340
+ ```
341
+ [workbuddy] intl: 第 2/5 次重试(TRANSPORT) — 上游没有给出结束原因就断开了响应…;2.1s 后重试
342
+ ```
343
+
344
+ 另外每个 finish 帧都带一个 `response` 诊断块(`provider` / `model` / `responseId` / `stopReason` / `outputTokens` / `blocks`),会话数据诊断里会原样显示,方便一眼看出"这一步是谁答的、上游叫它什么 id"。它不是 harness 的契约字段,harness 不会据此改变行为。
345
+
346
+ 排查用探针:
347
+
348
+ ```powershell
349
+ node test/live/stream-cut-probe.mjs cn --selftest # 离线验证"断流"分类
350
+ node test/live/stream-cut-probe.mjs intl 10 --long # 真实压测断流率(免费模型零消耗)
351
+ node test/live/find-message.mjs "关键词" <会话前缀> # 按文本定位某一步,并打出前后事件
352
+ node test/live/dump-stream.mjs <会话前缀> <seq,…> # 打印那一步存下来的原始流帧
353
+ ```
354
+
326
355
  ## 许可
327
356
 
328
357
  MIT,见 [`LICENSE`](LICENSE)。再次说明:本项目为第三方非官方插件,与腾讯 / WorkBuddy 无关联。
@@ -248,6 +248,11 @@ data: [DONE]
248
248
  频率实测工具:`node test/live/stream-cut-probe.mjs [cn|intl] [次数] [模型] [--long] [--selftest]`
249
249
  (2026-10-22 实测:大陆版短问答 4/4 正常、海外版 `deepseek-v4.1-flash` 长回答 10/10 正常,
250
250
  即断流是间歇性的,常规探测不易复现)
251
+ - **finish 帧带一个 `response` 诊断块**(`kind`/`version`/`api`/`provider`/`model`/`responseId`/
252
+ `stopReason`/`outputTokens`/`blocks`)。它不是 harness 的契约字段(`dsh-llm` 的类型里没有
253
+ `response` / `stopReason`),纯属遥测:会话诊断里会原样显示,用来一眼回答"这一步是谁答的、
254
+ 上游叫它什么 id",不必再从 `request/header` 反推。`responseId` 取自上游流里的 `id`
255
+ (形如 `chatcmpl-…`),可以直接和上游日志对账;harness 不会据此改变行为 ✅
251
256
  - `usage`(末帧携带):`prompt_tokens` / `completion_tokens` / `total_tokens` / `prompt_tokens_details.cached_tokens` / `completion_tokens_details.reasoning_tokens` / `prompt_cache_hit_tokens` / `prompt_cache_miss_tokens` / `credit` ✅
252
257
 
253
258
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@huai_xin/dsh-workbuddy",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "WorkBuddy model provider for DeepSeek Harness (international + China deployments)",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
package/src/adapter.js CHANGED
@@ -789,6 +789,31 @@ export class WorkBuddyAdapter {
789
789
  let lastDelta = '无内容'
790
790
  /** Output tokens the last usage frame reported, for the truncation log. */
791
791
  let outputTokens
792
+ /** The gateway's own response id, for correlating with upstream logs. */
793
+ let responseId
794
+ /**
795
+ * The finish chunk's diagnostics block.
796
+ *
797
+ * Not part of the harness contract — the other adapters attach their own and
798
+ * the session diagnostics show it verbatim, which is how a "who answered this,
799
+ * and what did upstream call it" question gets answered without reconstructing
800
+ * it from the request headers. Carries nothing the harness acts on.
801
+ */
802
+ const finishChunk = (reason) => ({
803
+ type: 'finish',
804
+ reason,
805
+ response: {
806
+ kind: 'workbuddy',
807
+ version: 1,
808
+ api: 'openai-completions',
809
+ provider: this.region.provider,
810
+ model: options.model,
811
+ ...(responseId === undefined ? {} : { responseId }),
812
+ stopReason: finishReason ?? reason.kind,
813
+ ...(outputTokens === undefined ? {} : { outputTokens }),
814
+ blocks: blocks.map((block) => ({ type: block.kind }))
815
+ }
816
+ })
792
817
 
793
818
  const textBlock = () => {
794
819
  let block = blocks.find((candidate) => candidate.kind === 'text')
@@ -838,6 +863,7 @@ export class WorkBuddyAdapter {
838
863
  }
839
864
  const choice = data.choices?.[0]
840
865
  const delta = choice?.delta
866
+ if (responseId === undefined && typeof data.id === 'string' && data.id.length > 0) responseId = data.id
841
867
  if (typeof choice?.finish_reason === 'string' && choice.finish_reason.length > 0) finishReason = choice.finish_reason
842
868
 
843
869
  if (typeof delta?.content === 'string' && delta.content.length > 0) {
@@ -946,31 +972,25 @@ export class WorkBuddyAdapter {
946
972
  if (!streamEnded && finishReason === undefined) {
947
973
  const detail = describeCut({ receivedBytes, startedAt, lastDataAt, lastDelta })
948
974
  this.ctx.logger.warn(`[workbuddy] ${this.region.id}: ${options.model} 的响应在 finish_reason/[DONE] 之前中断(${detail}),按传输错误上报,可自动重试`)
949
- yield {
950
- type: 'finish',
951
- reason: {
952
- kind: 'error',
953
- failure: {
954
- message: `${this.region.provider}: 上游没有给出结束原因就断开了响应(${detail};可重试的传输错误)`,
955
- code: 'TRANSPORT'
956
- }
975
+ yield finishChunk({
976
+ kind: 'error',
977
+ failure: {
978
+ message: `${this.region.provider}: 上游没有给出结束原因就断开了响应(${detail};可重试的传输错误)`,
979
+ code: 'TRANSPORT'
957
980
  }
958
- }
981
+ })
959
982
  return
960
983
  }
961
984
  if (argsTruncated && !streamEnded) {
962
985
  const detail = describeCut({ receivedBytes, startedAt, lastDataAt, lastDelta })
963
986
  this.ctx.logger.warn(`[workbuddy] ${this.region.id}: ${options.model} 的工具调用参数在传输中被截断(${detail}),按传输错误上报,可自动重试`)
964
- yield {
965
- type: 'finish',
966
- reason: {
967
- kind: 'error',
968
- failure: {
969
- message: `${this.region.provider}: 工具调用的参数在传输中被截断(${detail};可重试的传输错误)`,
970
- code: 'TRANSPORT'
971
- }
987
+ yield finishChunk({
988
+ kind: 'error',
989
+ failure: {
990
+ message: `${this.region.provider}: 工具调用的参数在传输中被截断(${detail};可重试的传输错误)`,
991
+ code: 'TRANSPORT'
972
992
  }
973
- }
993
+ })
974
994
  return
975
995
  }
976
996
  // Truncated arguments are checked before the empty-response rule: a step whose
@@ -978,11 +998,11 @@ export class WorkBuddyAdapter {
978
998
  // that "empty" would hide what happened.
979
999
  if (argsTruncated) {
980
1000
  this.ctx.logger.warn(`[workbuddy] ${this.region.id}: ${options.model} 的工具调用参数被输出上限截断,本步按 max-tokens 上报(harness 可续跑)`)
981
- yield { type: 'finish', reason: { kind: 'max-tokens' } }
1001
+ yield finishChunk({ kind: 'max-tokens' })
982
1002
  return
983
1003
  }
984
1004
  if (blockCount === 0) {
985
- yield { type: 'finish', reason: { kind: 'error', failure: { message: '模型返回了空响应', code: 'EMPTY_RESPONSE' } } }
1005
+ yield finishChunk({ kind: 'error', failure: { message: '模型返回了空响应', code: 'EMPTY_RESPONSE' } })
986
1006
  return
987
1007
  }
988
1008
  const reason = finishReason === 'tool_calls' || toolOrder.length > 0
@@ -999,7 +1019,7 @@ export class WorkBuddyAdapter {
999
1019
  } else if (reason.kind === 'stop' && finishReason === undefined) {
1000
1020
  this.ctx.logger.warn(`[workbuddy] ${this.region.id}: ${options.model} 的响应以 [DONE] 收尾但没有 finish_reason,按 stop 处理(输出 ${outputTokens ?? '未知'} tokens)`)
1001
1021
  }
1002
- yield { type: 'finish', reason }
1022
+ yield finishChunk(reason)
1003
1023
  }
1004
1024
  }
1005
1025