@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 +4 -4
- package/src/bot.ts +6 -0
- package/src/sessionTermination.ts +14 -0
- package/src/skill.ts +13 -19
- package/src/transport.ts +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dcrays/dcgchat-test",
|
|
3
|
-
"version": "0.5.0-alpha.
|
|
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.
|
|
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.
|
|
52
|
+
"minHostVersion": ">=2026.4.12"
|
|
53
53
|
},
|
|
54
54
|
"compat": {
|
|
55
|
-
"pluginApi": ">=2026.4.
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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 (
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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')
|