@dcrays/dcgchat-test 0.5.0-alpha.1 → 0.5.0-alpha.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dcrays/dcgchat-test",
3
- "version": "0.5.0-alpha.1",
3
+ "version": "0.5.0-alpha.2",
4
4
  "type": "module",
5
5
  "description": "OpenClaw channel plugin for 书灵墨宝 (WebSocket)",
6
6
  "main": "index.ts",
@@ -18,7 +18,7 @@
18
18
  "ai"
19
19
  ],
20
20
  "peerDependencies": {
21
- "openclaw": ">=2026.4.11"
21
+ "openclaw": ">=2026.4.12"
22
22
  },
23
23
  "peerDependenciesMeta": {
24
24
  "openclaw": {
@@ -49,10 +49,10 @@
49
49
  "npmSpec": "@dcrays/dcgchat-test",
50
50
  "localPath": "extensions/dcgchat-test",
51
51
  "defaultChoice": "npm",
52
- "minHostVersion": ">=2026.4.11"
52
+ "minHostVersion": ">=2026.4.12"
53
53
  },
54
54
  "compat": {
55
- "pluginApi": ">=2026.4.11"
55
+ "pluginApi": ">=2026.4.12"
56
56
  }
57
57
  }
58
58
  }
package/src/bot.ts CHANGED
@@ -316,6 +316,7 @@ async function handleDcgchatMessageInboundTurn(msg: InboundMessage, accountId: s
316
316
  humanDelay: core.channel.reply.resolveHumanDelayConfig(config, route.agentId),
317
317
  onReplyStart: async () => {},
318
318
  deliver: async (payload: ReplyPayload, info) => {
319
+ if ((inboundGenerationBySessionKey.get(dcgSessionKey) ?? 0) !== inboundGenAtStart) return
319
320
  if (isSessionStreamSuppressed(dcgSessionKey)) return
320
321
  const mediaList = resolveReplyMediaList(payload)
321
322
  for (const mediaUrl of mediaList) {
@@ -332,6 +333,10 @@ async function handleDcgchatMessageInboundTurn(msg: InboundMessage, accountId: s
332
333
  )
333
334
  },
334
335
  onError: (err: unknown, info: { kind: string }) => {
336
+ if ((inboundGenerationBySessionKey.get(dcgSessionKey) ?? 0) !== inboundGenAtStart) {
337
+ dcgLogger(`${info.kind} reply failed (stale handler, ignored): ${String(err)}`, 'error')
338
+ return
339
+ }
335
340
  dispatchReplyErrorHandledByOnError = true
336
341
  setMsgStatus(dcgSessionKey, 'finished')
337
342
  const suppressed = isSessionStreamSuppressed(dcgSessionKey)
@@ -419,6 +424,7 @@ async function handleDcgchatMessageInboundTurn(msg: InboundMessage, accountId: s
419
424
  setActiveRunIdForSession(dcgSessionKey, runId)
420
425
  },
421
426
  onPartialReply: async (payload: ReplyPayload) => {
427
+ if ((inboundGenerationBySessionKey.get(dcgSessionKey) ?? 0) !== inboundGenAtStart) return
422
428
  if (isSessionStreamSuppressed(dcgSessionKey)) return
423
429
  // --- Streaming text chunks ---
424
430
  if (payload.text) {
@@ -59,6 +59,13 @@ export function preemptInboundQueueForStop(sessionKey: string): void {
59
59
  dispatchAbortBySessionKey.delete(sessionKey)
60
60
  }
61
61
  inboundTurnTailBySessionKey.set(sessionKey, Promise.resolve())
62
+ // 立即标记流抑制,防止 /stop 到 interruptLocalDispatchAndGateway 之间网关事件仍能推送
63
+ markSessionStreamSuppressed(sessionKey)
64
+ // 队尾被重置后旧 handler 会变成「僵尸」与后续 /stop 并发;仅靠后续 interrupt 晚一拍时网关 run 仍占位。
65
+ // 尽早对网关发 interrupt 级 abort(与 /stop 内 interrupt 重复无害),缩短僵尸窗口。
66
+ void abortGatewayRunsForSession(sessionKey, 'interrupt').catch((e) =>
67
+ dcgLogger(`preempt: gateway abort: ${String(e)}`, 'error')
68
+ )
62
69
  dcgLogger(`inbound queue: reset tail for /stop sessionKey=${sessionKey}`)
63
70
  }
64
71
 
@@ -123,6 +130,13 @@ export async function beginSupersedingUserTurn(sessionKey: string): Promise<Abor
123
130
  sessionStreamSuppressed.delete(sessionKey)
124
131
  dispatchAbortBySessionKey.get(sessionKey)?.abort()
125
132
  await abortGatewayRunsForSession(sessionKey, 'supersede')
133
+ // `/stop` 的 interrupt 会先清空 activeRunId;此时 supersede 可能整段跳过主会话 abort,网关仍跑着僵尸 run,
134
+ // 新一轮会「秒结束」且无回复。再发一次无 runId 的 main abort 作为兜底(幂等)。
135
+ try {
136
+ await sendGatewayRpc({ method: 'chat.abort', params: { sessionKey } })
137
+ } catch (e) {
138
+ dcgLogger(`supersede: best-effort main chat.abort ${sessionKey}: ${String(e)}`, 'error')
139
+ }
126
140
  const ac = new AbortController()
127
141
  dispatchAbortBySessionKey.set(sessionKey, ac)
128
142
  return ac
package/src/skill.ts CHANGED
@@ -19,14 +19,13 @@ type ISkillParams = {
19
19
  function sendEvent(msgContent: Record<string, any>) {
20
20
  const ws = getWsConnection()
21
21
  if (isWsOpen()) {
22
- ws?.send(
23
- JSON.stringify({
24
- messageType: 'openclaw_bot_event',
25
- source: 'client',
26
- content: msgContent
27
- })
28
- )
29
- dcgLogger(`技能安装: ${JSON.stringify(msgContent)}`)
22
+ const msg = JSON.stringify({
23
+ messageType: 'openclaw_bot_event',
24
+ source: 'client',
25
+ content: msgContent
26
+ });
27
+ ws?.send(msg);
28
+ dcgLogger(`[Send]技能安装: ${msg}`)
30
29
  }
31
30
  }
32
31
 
@@ -137,16 +136,11 @@ export function uninstallSkill(params: Omit<ISkillParams, 'path'>, msgContent: R
137
136
  const { code } = params
138
137
 
139
138
  const workspacePath = getWorkspaceDir()
140
- if (!workspacePath) {
141
- sendEvent({ ...msgContent, status: 'ok' })
142
- }
143
-
144
- const skillDir = path.join(workspacePath, 'skills', code)
145
-
146
- if (fs.existsSync(skillDir)) {
147
- fs.rmSync(skillDir, { recursive: true, force: true })
148
- sendEvent({ ...msgContent, status: 'ok' })
149
- } else {
150
- sendEvent({ ...msgContent, status: 'ok' })
139
+ if (workspacePath) {
140
+ const skillDir = path.join(workspacePath, 'skills', code)
141
+ if (fs.existsSync(skillDir)) {
142
+ fs.rmSync(skillDir, { recursive: true, force: true })
143
+ }
151
144
  }
145
+ sendEvent({ ...msgContent, status: 'ok' })
152
146
  }
package/src/transport.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { clearSentMediaKeys, getWsConnection } from './utils/global.js'
2
+ import { isSessionStreamSuppressed } from './sessionTermination.js'
2
3
  import { dcgLogger } from './utils/log.js'
3
4
  import type { IMsgParams } from './types.js'
4
5
  import { getEffectiveMsgParams, getParamsDefaults } from './utils/params.js'
@@ -139,6 +140,7 @@ export function isWsOpen(): boolean {
139
140
  * `ctx` 须由调用方用 getEffectiveMsgParams(sessionKey) 等解析好;`content` 为完整业务 payload。
140
141
  */
141
142
  export function wsSend(ctx: IMsgParams, content: Record<string, unknown>): boolean {
143
+ if (ctx.sessionKey && isSessionStreamSuppressed(ctx.sessionKey)) return false
142
144
  const ws = getWsConnection()
143
145
  if (ws?.readyState !== WebSocket.OPEN) return false
144
146
  const envelope = buildOpenclawBotChat(ctx, content)
@@ -151,6 +153,7 @@ export function wsSend(ctx: IMsgParams, content: Record<string, unknown>): boole
151
153
  * `ctx` 须由调用方解析(如需合并覆盖可先 mergeSessionParams)。
152
154
  */
153
155
  export function wsSendRaw(ctx: IMsgParams, content: Record<string, unknown>, isLog = true): boolean {
156
+ if (ctx.sessionKey && isSessionStreamSuppressed(ctx.sessionKey)) return false
154
157
  const ws = getWsConnection()
155
158
  if (ws?.readyState !== WebSocket.OPEN) {
156
159
  dcgLogger(`server socket not ready ${ws?.readyState}`, 'error')