@dcrays/dcgchat-test 0.6.2 → 0.6.3
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/index.js +304 -0
- package/openclaw.plugin.json +41 -12
- package/package.json +9 -36
- package/README.md +0 -167
- package/index.ts +0 -31
- package/src/agent.ts +0 -592
- package/src/bot.ts +0 -676
- package/src/channel/index.ts +0 -329
- package/src/channel/outboundMedia.ts +0 -144
- package/src/channel/outboundTarget.ts +0 -24
- package/src/channel/uploadMediaUrl.ts +0 -76
- package/src/cron/message.ts +0 -109
- package/src/cron/params.ts +0 -166
- package/src/cron/toolGuard.ts +0 -285
- package/src/cron/types.ts +0 -15
- package/src/gateway/index.ts +0 -430
- package/src/gateway/security.ts +0 -95
- package/src/gateway/socket.ts +0 -256
- package/src/libs/ali-oss-6.23.0.tgz +0 -0
- package/src/libs/axios-1.13.6.tgz +0 -0
- package/src/libs/md5-2.3.0.tgz +0 -0
- package/src/libs/mime-types-3.0.2.tgz +0 -0
- package/src/libs/unzipper-0.12.3.tgz +0 -0
- package/src/libs/ws-8.19.0.tgz +0 -0
- package/src/monitor.ts +0 -269
- package/src/request/api.ts +0 -80
- package/src/request/oss.ts +0 -200
- package/src/request/request.ts +0 -191
- package/src/request/userInfo.ts +0 -44
- package/src/session.ts +0 -28
- package/src/skill.ts +0 -114
- package/src/tool.ts +0 -625
- package/src/tools/messageTool.ts +0 -334
- package/src/tools/toolCallGuard.ts +0 -323
- package/src/transport.ts +0 -217
- package/src/types.ts +0 -139
- package/src/utils/agentErrors.ts +0 -23
- package/src/utils/constant.ts +0 -60
- package/src/utils/env-config.ts +0 -19
- package/src/utils/formatLlmInputEvent.ts +0 -48
- package/src/utils/gatewayMsgHandler.ts +0 -89
- package/src/utils/global.ts +0 -306
- package/src/utils/inboundTurnState.ts +0 -66
- package/src/utils/log.ts +0 -77
- package/src/utils/mediaAttached.ts +0 -244
- package/src/utils/mediaEmitter.ts +0 -54
- package/src/utils/outboundAssistantText.ts +0 -110
- package/src/utils/params.ts +0 -104
- package/src/utils/passTxt.ts +0 -4
- package/src/utils/resolveRegisterConfig.ts +0 -33
- package/src/utils/searchFile.ts +0 -228
- package/src/utils/sessionState.ts +0 -137
- package/src/utils/sessionTermination.ts +0 -228
- package/src/utils/streamMerge.ts +0 -150
- package/src/utils/subagentRunMap.ts +0 -71
- package/src/utils/workspaceFilePaths.ts +0 -18
- package/src/utils/wsMessageHandler.ts +0 -124
- package/src/utils/zipExtract.ts +0 -97
- package/src/utils/zipPath.ts +0 -24
|
@@ -1,228 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 会话终止 / 抢占 / 网关 abort 的集中实现,便于单独调整策略与观测。
|
|
3
|
-
* 与 inboundTurnState(generation / streamChunkIdx)、入站业务上下文分离,仅负责:本地 AbortController、流抑制标记、
|
|
4
|
-
* 入站串行队尾、activeRunId、以及 /stop control barrier 时的 chat.abort(子会话→主会话)。
|
|
5
|
-
*/
|
|
6
|
-
import { sendGatewayRpc } from '../gateway/socket.js'
|
|
7
|
-
import { dcgLogger } from './log.js'
|
|
8
|
-
import { bumpInboundGeneration, bumpStopSeq, clearInboundGenerationForSession, getStopSeq } from './inboundTurnState.js'
|
|
9
|
-
import { getDescendantSessionKeysForRequester, resetSubagentStateForRequesterSession, cleanupSubagentStateForSession } from '../tool.js'
|
|
10
|
-
import { clearParamsMessage } from './params.js'
|
|
11
|
-
import { setMsgStatus } from './global.js'
|
|
12
|
-
import { clearOutboundTextState } from './outboundAssistantText.js'
|
|
13
|
-
|
|
14
|
-
// 缩短超时时间,避免长时间阻塞
|
|
15
|
-
const GATEWAY_ABORT_TIMEOUT_MS = 5_000
|
|
16
|
-
|
|
17
|
-
// --- 状态(仅本模块内修改,供 bot 通过下方 API 使用)---
|
|
18
|
-
|
|
19
|
-
/** 当前会话最近一次 agent run 的 runId(网关 chat.abort 主会话时携带) */
|
|
20
|
-
const activeRunIdBySessionKey = new Map<string, string>()
|
|
21
|
-
|
|
22
|
-
/** dispatchReplyFromConfig 使用的 AbortSignal,用于真正掐断工具与模型 */
|
|
23
|
-
const dispatchAbortBySessionKey = new Map<string, AbortController>()
|
|
24
|
-
|
|
25
|
-
/** 打断后抑制 deliver / onPartialReply 继续下发 */
|
|
26
|
-
const sessionStreamSuppressed = new Set<string>()
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* 同 sessionKey 入站串行队尾;与 Core session lane 对齐,避免并发 dispatch 过早返回。
|
|
30
|
-
*/
|
|
31
|
-
const inboundTurnTailBySessionKey = new Map<string, Promise<void>>()
|
|
32
|
-
|
|
33
|
-
/** 当前 session 正在执行的 /stop 终止屏障;普通消息必须等它结束后再进入新 turn。 */
|
|
34
|
-
const stopBarrierBySessionKey = new Map<string, Promise<void>>()
|
|
35
|
-
|
|
36
|
-
// --- activeRunId(供 bot 在 onAgentRunStart / 错误收尾时同步)---
|
|
37
|
-
|
|
38
|
-
export function setActiveRunIdForSession(sessionKey: string, runId: string): void {
|
|
39
|
-
activeRunIdBySessionKey.set(sessionKey, runId)
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function clearActiveRunIdForSession(sessionKey: string): void {
|
|
43
|
-
activeRunIdBySessionKey.delete(sessionKey)
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// --- 流抑制 ---
|
|
47
|
-
|
|
48
|
-
export function isSessionStreamSuppressed(sessionKey: string): boolean {
|
|
49
|
-
return sessionStreamSuppressed.has(sessionKey)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export function clearSessionStreamSuppression(sessionKey: string): void {
|
|
53
|
-
sessionStreamSuppressed.delete(sessionKey)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function markSessionStreamSuppressed(sessionKey: string): void {
|
|
57
|
-
sessionStreamSuppressed.add(sessionKey)
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// --- 入站串行队列 ---
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* /stop control path:掐断当前 in-process,等待网关 chat.abort(子会话→主会话)完成后再释放普通消息。
|
|
64
|
-
*
|
|
65
|
-
* 返回 started=false 表示已有同 session 的 stop barrier 在跑;调用方仍应给当前 /stop 消息立即 ack。
|
|
66
|
-
*/
|
|
67
|
-
export function requestStopInterruption(sessionKey: string): { started: boolean; barrier: Promise<void> } {
|
|
68
|
-
const key = sessionKey.trim()
|
|
69
|
-
if (!key) return { started: false, barrier: Promise.resolve() }
|
|
70
|
-
|
|
71
|
-
const existing = stopBarrierBySessionKey.get(key)
|
|
72
|
-
if (existing) {
|
|
73
|
-
dcgLogger(`stop barrier already running sessionKey=${key}`)
|
|
74
|
-
return { started: false, barrier: existing }
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
let barrier!: Promise<void>
|
|
78
|
-
barrier = Promise.resolve().then(async () => {
|
|
79
|
-
try {
|
|
80
|
-
const c = dispatchAbortBySessionKey.get(key)
|
|
81
|
-
if (c) {
|
|
82
|
-
c.abort()
|
|
83
|
-
dispatchAbortBySessionKey.delete(key)
|
|
84
|
-
dcgLogger(`stop barrier: aborted local controller sessionKey=${key}`)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
markSessionStreamSuppressed(key)
|
|
88
|
-
const inboundGenAfterBump = bumpInboundGeneration(key)
|
|
89
|
-
const stopSeqAfterBump = bumpStopSeq(key)
|
|
90
|
-
dcgLogger(`stop barrier: bump inboundGen=${inboundGenAfterBump}, stopSeq=${stopSeqAfterBump}, sessionKey=${key}`)
|
|
91
|
-
|
|
92
|
-
const oldTail = inboundTurnTailBySessionKey.get(key)
|
|
93
|
-
if (oldTail) {
|
|
94
|
-
await Promise.race([oldTail.catch(() => {}), new Promise<void>((r) => setTimeout(r, 100))])
|
|
95
|
-
dcgLogger(`stop barrier: old tail done/timeout sessionKey=${key}`)
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
try {
|
|
99
|
-
await abortGatewayRunsForSession(key)
|
|
100
|
-
} catch (e) {
|
|
101
|
-
dcgLogger(`stop barrier: gateway abort: ${String(e)}`, 'error')
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
inboundTurnTailBySessionKey.set(key, Promise.resolve())
|
|
105
|
-
dcgLogger(`stop barrier: released inbound queue sessionKey=${key}`)
|
|
106
|
-
} finally {
|
|
107
|
-
if (stopBarrierBySessionKey.get(key) === barrier) {
|
|
108
|
-
stopBarrierBySessionKey.delete(key)
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
stopBarrierBySessionKey.set(key, barrier)
|
|
114
|
-
return { started: true, barrier }
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/** 普通入站消息进入队列前调用,保证 /stop 的 gateway abort 已完成或超时。 */
|
|
118
|
-
export async function waitForStopInterruption(sessionKey: string): Promise<void> {
|
|
119
|
-
const key = sessionKey.trim()
|
|
120
|
-
if (!key) return
|
|
121
|
-
for (;;) {
|
|
122
|
-
const barrier = stopBarrierBySessionKey.get(key)
|
|
123
|
-
if (!barrier) return
|
|
124
|
-
dcgLogger(`waiting stop barrier before inbound turn sessionKey=${key}`)
|
|
125
|
-
await barrier.catch((e) => dcgLogger(`stop barrier wait failed: ${String(e)}`, 'error'))
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/** 将本轮入站处理挂到 sessionKey 队尾,保证同会话顺序执行 */
|
|
130
|
-
export async function runInboundTurnSequenced(sessionKey: string, run: () => Promise<void>): Promise<void> {
|
|
131
|
-
const prev = inboundTurnTailBySessionKey.get(sessionKey) ?? Promise.resolve()
|
|
132
|
-
|
|
133
|
-
const next = prev.catch(() => {}).then(() => run())
|
|
134
|
-
|
|
135
|
-
inboundTurnTailBySessionKey.set(
|
|
136
|
-
sessionKey,
|
|
137
|
-
next.catch((e) => dcgLogger(`session turn failed: ${String(e)}`, 'error'))
|
|
138
|
-
)
|
|
139
|
-
await next
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// --- 网关 abort ---
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* 终止网关上仍可能活跃的 run(子会话自深到浅,再主会话)。
|
|
146
|
-
* 主会话始终 chat.abort;有 mainRunId 则按 run 掐断,无则仅用 sessionKey 扫尾。
|
|
147
|
-
*/
|
|
148
|
-
export async function abortGatewayRunsForSession(sessionKey: string): Promise<void> {
|
|
149
|
-
const prefix = 'interrupt'
|
|
150
|
-
const descendantKeys = getDescendantSessionKeysForRequester(sessionKey)
|
|
151
|
-
const abortSubKeys = [...descendantKeys].reverse()
|
|
152
|
-
const mainRunId = activeRunIdBySessionKey.get(sessionKey)
|
|
153
|
-
|
|
154
|
-
if (abortSubKeys.length > 0) {
|
|
155
|
-
dcgLogger(`${prefix}: chat.abort ${abortSubKeys.length} subagent session(s) (nested incl.)`)
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const abortAllSequentially = async () => {
|
|
159
|
-
// 按“先子后主”顺序终止,降低主会话先结束导致子会话残留事件的概率。
|
|
160
|
-
for (const subKey of abortSubKeys) {
|
|
161
|
-
try {
|
|
162
|
-
await sendGatewayRpc({ method: 'chat.abort', params: { sessionKey: subKey } })
|
|
163
|
-
} catch (e: unknown) {
|
|
164
|
-
dcgLogger(`${prefix}: chat.abort subagent ${subKey}: ${String(e)}`, 'error')
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
try {
|
|
168
|
-
await sendGatewayRpc({
|
|
169
|
-
method: 'chat.abort',
|
|
170
|
-
params: mainRunId ? { sessionKey, runId: mainRunId } : { sessionKey }
|
|
171
|
-
})
|
|
172
|
-
} catch (e: unknown) {
|
|
173
|
-
dcgLogger(`${prefix}: chat.abort main ${sessionKey}: ${String(e)}`, 'error')
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
try {
|
|
178
|
-
await Promise.race([
|
|
179
|
-
abortAllSequentially(),
|
|
180
|
-
new Promise<void>((_, reject) => setTimeout(() => reject(new Error('batch abort timeout')), GATEWAY_ABORT_TIMEOUT_MS))
|
|
181
|
-
])
|
|
182
|
-
} catch (e) {
|
|
183
|
-
dcgLogger(`${prefix}: batch abort timeout: ${String(e)}`, 'error')
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
activeRunIdBySessionKey.delete(sessionKey)
|
|
187
|
-
resetSubagentStateForRequesterSession(sessionKey)
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// --- 本地 dispatch AbortController ---
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* 新一轮非 /stop 用户消息:本地中止上一轮 AbortController(若有)、清除流抑制,并安装本轮 AbortController。
|
|
194
|
-
* 不在此处发往网关 chat.abort;每轮流式由入站串行保证先后,网关 run 收尾由上一轮正常完结或用户 /stop 处理。
|
|
195
|
-
* 调用方须在之前或之后自行 `resetStreamChunkIdxForSession(sessionKey, 0)`(见 inboundTurnState)。
|
|
196
|
-
*/
|
|
197
|
-
export function beginDispatchAbortForInboundTurn(sessionKey: string): AbortController {
|
|
198
|
-
dispatchAbortBySessionKey.get(sessionKey)?.abort()
|
|
199
|
-
sessionStreamSuppressed.delete(sessionKey)
|
|
200
|
-
const ac = new AbortController()
|
|
201
|
-
dispatchAbortBySessionKey.set(sessionKey, ac)
|
|
202
|
-
return ac
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/** try/finally 中:仅当仍是当前 controller 时从 map 移除 */
|
|
206
|
-
export function releaseDispatchAbortIfCurrent(sessionKey: string, controller: AbortController | undefined): void {
|
|
207
|
-
if (controller && dispatchAbortBySessionKey.get(sessionKey) === controller) {
|
|
208
|
-
dispatchAbortBySessionKey.delete(sessionKey)
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* 会话完全终止时的资源清理:移除本模块及 tool 模块中与该会话相关的所有状态,
|
|
214
|
-
* 防止 Map/Set 内存泄漏。
|
|
215
|
-
*/
|
|
216
|
-
export function cleanupTerminatedSession(sessionKey: string): void {
|
|
217
|
-
if (!sessionKey) return
|
|
218
|
-
activeRunIdBySessionKey.delete(sessionKey)
|
|
219
|
-
dispatchAbortBySessionKey.delete(sessionKey)
|
|
220
|
-
sessionStreamSuppressed.delete(sessionKey)
|
|
221
|
-
inboundTurnTailBySessionKey.delete(sessionKey)
|
|
222
|
-
stopBarrierBySessionKey.delete(sessionKey)
|
|
223
|
-
clearInboundGenerationForSession(sessionKey)
|
|
224
|
-
clearParamsMessage(sessionKey)
|
|
225
|
-
setMsgStatus(sessionKey, '')
|
|
226
|
-
clearOutboundTextState(sessionKey)
|
|
227
|
-
cleanupSubagentStateForSession(sessionKey)
|
|
228
|
-
}
|
package/src/utils/streamMerge.ts
DELETED
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
import type { IMsgParams } from '../types.js'
|
|
2
|
-
import { sendChunk } from '../transport.js'
|
|
3
|
-
import { getWsConnection } from './global.js'
|
|
4
|
-
import { isSessionStreamSuppressed } from './sessionTermination.js'
|
|
5
|
-
import { getInboundGeneration, takeStreamChunkIdxAndAdvance } from './inboundTurnState.js'
|
|
6
|
-
|
|
7
|
-
export const STREAM_MERGE_PROFILES = [
|
|
8
|
-
{ maxAvgDeltaGapMs: 20, flushIntervalMs: 50, deltaCount: 20, charCount: 100 },
|
|
9
|
-
{ maxAvgDeltaGapMs: 60, flushIntervalMs: 30, deltaCount: 15, charCount: 60 },
|
|
10
|
-
{ maxAvgDeltaGapMs: Number.POSITIVE_INFINITY, flushIntervalMs: 20, deltaCount: 6, charCount: 30 }
|
|
11
|
-
] as const
|
|
12
|
-
|
|
13
|
-
export const STREAM_MERGE_ENABLE_WINDOW_MS = 1000
|
|
14
|
-
export const STREAM_MERGE_ENABLE_DELTA_COUNT = 50
|
|
15
|
-
export const STREAM_MERGE_IDLE_RESET_MS = 300
|
|
16
|
-
export const STREAM_BACKPRESSURE_HIGH_WATERMARK = 512 * 1024
|
|
17
|
-
export const STREAM_BACKPRESSURE_RETRY_MS = 50
|
|
18
|
-
|
|
19
|
-
export interface StreamMergeController {
|
|
20
|
-
queueDelta(delta: string): boolean
|
|
21
|
-
flush(force?: boolean): boolean
|
|
22
|
-
dispose(): void
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export interface CreateStreamMergeControllerOptions {
|
|
26
|
-
sessionKey: string
|
|
27
|
-
inboundGenAtStart: number
|
|
28
|
-
outboundCtx: IMsgParams
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function createStreamMergeController(
|
|
32
|
-
options: CreateStreamMergeControllerOptions
|
|
33
|
-
): StreamMergeController {
|
|
34
|
-
const { sessionKey, inboundGenAtStart, outboundCtx } = options
|
|
35
|
-
|
|
36
|
-
let pendingText = ''
|
|
37
|
-
let pendingDeltaCount = 0
|
|
38
|
-
let timer: ReturnType<typeof setTimeout> | undefined
|
|
39
|
-
let mergeMode = false
|
|
40
|
-
let burstWindowStart = 0
|
|
41
|
-
let burstDeltaCount = 0
|
|
42
|
-
let lastDeltaAt = 0
|
|
43
|
-
let deltaGapAvgMs = 0
|
|
44
|
-
|
|
45
|
-
const clearTimer = () => {
|
|
46
|
-
if (timer) {
|
|
47
|
-
clearTimeout(timer)
|
|
48
|
-
timer = undefined
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const isBackpressured = () => (getWsConnection()?.bufferedAmount ?? 0) > STREAM_BACKPRESSURE_HIGH_WATERMARK
|
|
53
|
-
|
|
54
|
-
const currentProfile = () =>
|
|
55
|
-
STREAM_MERGE_PROFILES.find((profile) => deltaGapAvgMs <= profile.maxAvgDeltaGapMs) ?? STREAM_MERGE_PROFILES[1]
|
|
56
|
-
|
|
57
|
-
const isStale = () =>
|
|
58
|
-
getInboundGeneration(sessionKey) !== inboundGenAtStart || isSessionStreamSuppressed(sessionKey)
|
|
59
|
-
|
|
60
|
-
const emit = (text: string) => sendChunk(text, outboundCtx, takeStreamChunkIdxAndAdvance(sessionKey))
|
|
61
|
-
|
|
62
|
-
const resetMerge = () => {
|
|
63
|
-
mergeMode = false
|
|
64
|
-
burstWindowStart = 0
|
|
65
|
-
burstDeltaCount = 0
|
|
66
|
-
deltaGapAvgMs = 0
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const flush = (force = false): boolean => {
|
|
70
|
-
clearTimer()
|
|
71
|
-
if (!pendingText.trim()) {
|
|
72
|
-
pendingText = ''
|
|
73
|
-
pendingDeltaCount = 0
|
|
74
|
-
return false
|
|
75
|
-
}
|
|
76
|
-
if (isStale()) {
|
|
77
|
-
pendingText = ''
|
|
78
|
-
pendingDeltaCount = 0
|
|
79
|
-
return false
|
|
80
|
-
}
|
|
81
|
-
if (!force && isBackpressured()) {
|
|
82
|
-
scheduleFlush(STREAM_BACKPRESSURE_RETRY_MS)
|
|
83
|
-
return false
|
|
84
|
-
}
|
|
85
|
-
const textToSend = pendingText
|
|
86
|
-
pendingText = ''
|
|
87
|
-
pendingDeltaCount = 0
|
|
88
|
-
return emit(textToSend)
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const scheduleFlush = (delayMs = currentProfile().flushIntervalMs) => {
|
|
92
|
-
if (timer) return
|
|
93
|
-
timer = setTimeout(() => flush(), delayMs)
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const queueDelta = (delta: string): boolean => {
|
|
97
|
-
const now = Date.now()
|
|
98
|
-
const deltaGapMs = lastDeltaAt > 0 ? now - lastDeltaAt : 0
|
|
99
|
-
|
|
100
|
-
if (lastDeltaAt > 0 && deltaGapMs >= STREAM_MERGE_IDLE_RESET_MS) {
|
|
101
|
-
flush(true)
|
|
102
|
-
resetMerge()
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (deltaGapMs > 0) {
|
|
106
|
-
deltaGapAvgMs = deltaGapAvgMs === 0 ? deltaGapMs : deltaGapAvgMs * 0.7 + deltaGapMs * 0.3
|
|
107
|
-
}
|
|
108
|
-
lastDeltaAt = now
|
|
109
|
-
|
|
110
|
-
if (!mergeMode) {
|
|
111
|
-
if (burstWindowStart === 0 || now - burstWindowStart > STREAM_MERGE_ENABLE_WINDOW_MS) {
|
|
112
|
-
burstWindowStart = now
|
|
113
|
-
burstDeltaCount = 1
|
|
114
|
-
} else {
|
|
115
|
-
burstDeltaCount += 1
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
pendingText += delta
|
|
119
|
-
pendingDeltaCount += 1
|
|
120
|
-
flush()
|
|
121
|
-
|
|
122
|
-
if (burstDeltaCount >= STREAM_MERGE_ENABLE_DELTA_COUNT) {
|
|
123
|
-
mergeMode = true
|
|
124
|
-
}
|
|
125
|
-
return true
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
pendingText += delta
|
|
129
|
-
pendingDeltaCount += 1
|
|
130
|
-
const profile = currentProfile()
|
|
131
|
-
if (pendingText.length >= profile.charCount || pendingDeltaCount >= profile.deltaCount) {
|
|
132
|
-
flush()
|
|
133
|
-
return true
|
|
134
|
-
}
|
|
135
|
-
scheduleFlush()
|
|
136
|
-
return true
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const dispose = () => {
|
|
140
|
-
clearTimer()
|
|
141
|
-
pendingText = ''
|
|
142
|
-
pendingDeltaCount = 0
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
return {
|
|
146
|
-
queueDelta,
|
|
147
|
-
flush,
|
|
148
|
-
dispose
|
|
149
|
-
}
|
|
150
|
-
}
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
const requesterSessionKeyByRunId = new Map<string, string>()
|
|
2
|
-
const childSessionKeyByRunId = new Map<string, string>()
|
|
3
|
-
|
|
4
|
-
export type SubagentRunMapSnapshot = {
|
|
5
|
-
requesterSessionKeyByRunId?: Array<[string, string]>
|
|
6
|
-
childSessionKeyByRunId?: Array<[string, string]>
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function setSubagentRunRequester(runId: string | undefined, requesterSessionKey: string | undefined): void {
|
|
10
|
-
const rid = runId?.trim()
|
|
11
|
-
const req = requesterSessionKey?.trim()
|
|
12
|
-
if (!rid || !req) return
|
|
13
|
-
requesterSessionKeyByRunId.set(rid, req)
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function setSubagentRunChild(runId: string | undefined, childSessionKey: string | undefined): void {
|
|
17
|
-
const rid = runId?.trim()
|
|
18
|
-
const child = childSessionKey?.trim()
|
|
19
|
-
if (!rid || !child) return
|
|
20
|
-
childSessionKeyByRunId.set(rid, child)
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function deleteSubagentRunMappings(runId: string | undefined): void {
|
|
24
|
-
const rid = runId?.trim()
|
|
25
|
-
if (!rid) return
|
|
26
|
-
requesterSessionKeyByRunId.delete(rid)
|
|
27
|
-
childSessionKeyByRunId.delete(rid)
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function clearSubagentRunMappings(): void {
|
|
31
|
-
requesterSessionKeyByRunId.clear()
|
|
32
|
-
childSessionKeyByRunId.clear()
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function resolveRequesterSessionKeyBySubagentRunId(runId: string | undefined): string | undefined {
|
|
36
|
-
const rid = runId?.trim()
|
|
37
|
-
if (!rid) return undefined
|
|
38
|
-
return requesterSessionKeyByRunId.get(rid)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function resolveChildSessionKeyBySubagentRunId(runId: string | undefined): string | undefined {
|
|
42
|
-
const rid = runId?.trim()
|
|
43
|
-
if (!rid) return undefined
|
|
44
|
-
return childSessionKeyByRunId.get(rid)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function cleanupSubagentRunMappingsForSessionKeys(sessionKeys: Set<string>): void {
|
|
48
|
-
for (const [rid, child] of childSessionKeyByRunId.entries()) {
|
|
49
|
-
if (sessionKeys.has(child)) deleteSubagentRunMappings(rid)
|
|
50
|
-
}
|
|
51
|
-
for (const [rid, requester] of requesterSessionKeyByRunId.entries()) {
|
|
52
|
-
if (sessionKeys.has(requester)) deleteSubagentRunMappings(rid)
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function snapshotSubagentRunMappings(): SubagentRunMapSnapshot {
|
|
57
|
-
return {
|
|
58
|
-
requesterSessionKeyByRunId: [...requesterSessionKeyByRunId.entries()],
|
|
59
|
-
childSessionKeyByRunId: [...childSessionKeyByRunId.entries()]
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function restoreSubagentRunMappings(snap: SubagentRunMapSnapshot | null | undefined): void {
|
|
64
|
-
clearSubagentRunMappings()
|
|
65
|
-
for (const [k, v] of snap?.requesterSessionKeyByRunId ?? []) {
|
|
66
|
-
if (typeof k === 'string' && typeof v === 'string' && k && v) requesterSessionKeyByRunId.set(k, v)
|
|
67
|
-
}
|
|
68
|
-
for (const [k, v] of snap?.childSessionKeyByRunId ?? []) {
|
|
69
|
-
if (typeof k === 'string' && typeof v === 'string' && k && v) childSessionKeyByRunId.set(k, v)
|
|
70
|
-
}
|
|
71
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import os from 'node:os'
|
|
2
|
-
import path from 'node:path'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* 将正文里的 `~/...` / `~\...` 展开为绝对路径(`path.resolve` 不会处理 `~`)。
|
|
6
|
-
*/
|
|
7
|
-
export function expandTildePath(input: string): string {
|
|
8
|
-
const s = input.trim()
|
|
9
|
-
if (!s) return s
|
|
10
|
-
if (s === '~') return os.homedir()
|
|
11
|
-
if (s.startsWith('~/')) {
|
|
12
|
-
return path.resolve(os.homedir(), s.slice(2))
|
|
13
|
-
}
|
|
14
|
-
if (s.startsWith('~\\')) {
|
|
15
|
-
return path.resolve(os.homedir(), s.slice(2))
|
|
16
|
-
}
|
|
17
|
-
return s
|
|
18
|
-
}
|
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
import { handleDcgchatMessage } from '../bot.js'
|
|
2
|
-
import { setMsgStatus, getSessionKey } from './global.js'
|
|
3
|
-
import type { InboundMessage } from '../types.js'
|
|
4
|
-
import { installSkill, uninstallSkill } from '../skill.js'
|
|
5
|
-
import { dcgLogger } from './log.js'
|
|
6
|
-
import { onDisabledCronJob, onEnabledCronJob, onRemoveCronJob, onRunCronJob } from '../cron/message.js'
|
|
7
|
-
import { ignoreToolCommand } from './constant.js'
|
|
8
|
-
import { createAgent, onRemoveAgent, onUpdateAgent } from '../agent.js'
|
|
9
|
-
import { onRemoveSession } from '../session.js'
|
|
10
|
-
|
|
11
|
-
interface WsEventContent {
|
|
12
|
-
event_type?: string
|
|
13
|
-
operation_type?: string
|
|
14
|
-
skill_url?: string
|
|
15
|
-
skill_code?: string
|
|
16
|
-
skill_id?: string
|
|
17
|
-
bot_token?: string
|
|
18
|
-
websocket_trace_id?: string
|
|
19
|
-
job_id?: string
|
|
20
|
-
message_id?: string
|
|
21
|
-
agent_id?: string
|
|
22
|
-
session_id?: string
|
|
23
|
-
agent_clone_code?: string
|
|
24
|
-
url?: string
|
|
25
|
-
clone_code?: string
|
|
26
|
-
name?: string
|
|
27
|
-
description?: string
|
|
28
|
-
[key: string]: unknown
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export type ParsedWsPayload = {
|
|
32
|
-
messageType?: string
|
|
33
|
-
content: WsEventContent | null
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function logUnsupportedOpenclawBotEvent(
|
|
37
|
-
branch: 'skill' | 'agent',
|
|
38
|
-
eventType: string,
|
|
39
|
-
operationType: unknown,
|
|
40
|
-
rawPayload: string
|
|
41
|
-
): void {
|
|
42
|
-
dcgLogger(
|
|
43
|
-
`openclaw_bot_event ${branch}: unsupported operation_type=${String(operationType)} (event_type=${eventType}), ${rawPayload}`,
|
|
44
|
-
'error'
|
|
45
|
-
)
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* 处理 WebSocket 已解析 JSON 且 content 已二次 parse 后的业务消息(openclaw_bot_chat / openclaw_bot_event)。
|
|
50
|
-
*/
|
|
51
|
-
export async function handleParsedWsMessage(parsed: ParsedWsPayload, rawPayload: string, accountId: string): Promise<void> {
|
|
52
|
-
if (parsed.messageType === 'openclaw_bot_chat') {
|
|
53
|
-
const msg = parsed as unknown as InboundMessage
|
|
54
|
-
// 与 monitor 原逻辑一致:工具类指令不进入 running,避免误触工具链监控
|
|
55
|
-
const effectiveSessionKey = getSessionKey(msg.content, accountId)
|
|
56
|
-
const isToolOnly = ignoreToolCommand.includes(msg.content.text?.trim() ?? '')
|
|
57
|
-
if (!isToolOnly) {
|
|
58
|
-
setMsgStatus(effectiveSessionKey, 'running')
|
|
59
|
-
} else {
|
|
60
|
-
setMsgStatus(effectiveSessionKey, 'finished')
|
|
61
|
-
}
|
|
62
|
-
try {
|
|
63
|
-
await handleDcgchatMessage(msg, accountId)
|
|
64
|
-
} catch (err) {
|
|
65
|
-
dcgLogger(`handleDcgchatMessage failed: ${String(err)}`, 'error')
|
|
66
|
-
if (!isToolOnly) {
|
|
67
|
-
setMsgStatus(effectiveSessionKey, '')
|
|
68
|
-
}
|
|
69
|
-
// 不再向上 throw:否则 async onMessage 未接住的 rejection 会触发 Unhandled promise rejection
|
|
70
|
-
}
|
|
71
|
-
} else if (parsed.messageType === 'openclaw_bot_event') {
|
|
72
|
-
if (!parsed.content) {
|
|
73
|
-
dcgLogger(`openclaw_bot_event: content is null, ${rawPayload}`, 'error')
|
|
74
|
-
return
|
|
75
|
-
}
|
|
76
|
-
const { event_type, operation_type } = parsed.content
|
|
77
|
-
if (event_type === 'skill') {
|
|
78
|
-
const { skill_url = '', skill_code = '', skill_id, bot_token, websocket_trace_id } = parsed.content
|
|
79
|
-
const content = { event_type, operation_type, skill_url, skill_code, skill_id, bot_token, websocket_trace_id }
|
|
80
|
-
if (operation_type === 'install' || operation_type === 'enable' || operation_type === 'update') {
|
|
81
|
-
installSkill({ path: skill_url, code: skill_code }, content)
|
|
82
|
-
} else if (operation_type === 'remove' || operation_type === 'disable') {
|
|
83
|
-
uninstallSkill({ code: skill_code }, content)
|
|
84
|
-
} else {
|
|
85
|
-
logUnsupportedOpenclawBotEvent('skill', event_type, operation_type, rawPayload)
|
|
86
|
-
}
|
|
87
|
-
} else if (event_type === 'agent') {
|
|
88
|
-
if (operation_type === 'create') {
|
|
89
|
-
await createAgent(parsed.content)
|
|
90
|
-
} else if (operation_type === 'remove') {
|
|
91
|
-
await onRemoveAgent(parsed.content)
|
|
92
|
-
} else if (operation_type === 'update') {
|
|
93
|
-
await onUpdateAgent(parsed.content)
|
|
94
|
-
} else {
|
|
95
|
-
logUnsupportedOpenclawBotEvent('agent', event_type, operation_type, rawPayload)
|
|
96
|
-
}
|
|
97
|
-
} else if (event_type === 'cron') {
|
|
98
|
-
const { job_id = '', message_id = '' } = parsed.content
|
|
99
|
-
if (operation_type === 'remove') {
|
|
100
|
-
await onRemoveCronJob(job_id)
|
|
101
|
-
} else if (operation_type === 'enable') {
|
|
102
|
-
await onEnabledCronJob(job_id)
|
|
103
|
-
} else if (operation_type === 'disable') {
|
|
104
|
-
await onDisabledCronJob(job_id)
|
|
105
|
-
} else if (operation_type === 'run') {
|
|
106
|
-
await onRunCronJob(job_id, message_id)
|
|
107
|
-
} else {
|
|
108
|
-
dcgLogger(`openclaw_bot_event cron: unsupported operation_type=${String(operation_type)}, ${rawPayload}`, 'error')
|
|
109
|
-
}
|
|
110
|
-
} else if (event_type === 'session') {
|
|
111
|
-
const { agent_id = '', session_id = '', agent_clone_code } = parsed.content
|
|
112
|
-
if (operation_type === 'remove') {
|
|
113
|
-
await onRemoveSession({ agent_id, session_id, agent_clone_code, account_id: accountId })
|
|
114
|
-
}
|
|
115
|
-
} else {
|
|
116
|
-
dcgLogger(
|
|
117
|
-
`openclaw_bot_event: unknown event_type=${String(event_type)}, operation_type=${String(operation_type)}, ${rawPayload}`,
|
|
118
|
-
'error'
|
|
119
|
-
)
|
|
120
|
-
}
|
|
121
|
-
} else {
|
|
122
|
-
dcgLogger(`ignoring unknown messageType: ${parsed.messageType}`, 'error')
|
|
123
|
-
}
|
|
124
|
-
}
|
package/src/utils/zipExtract.ts
DELETED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import path from 'path'
|
|
2
|
-
import fs from 'fs'
|
|
3
|
-
/** @ts-ignore */
|
|
4
|
-
import unzipper from 'unzipper'
|
|
5
|
-
import { pipeline } from 'stream/promises'
|
|
6
|
-
import { decodeZipEntryPath } from './zipPath.js'
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* 若且唯若所有条目都在同一顶层目录下(如 GitHub 下载的 repo-name/...),返回该目录名;否则返回 null。
|
|
10
|
-
* 不能再用「第一个多段路径的第一段」推断,否则 ZIP 条目顺序变化时会误判(例如先出现 .github/ 或 src/)。
|
|
11
|
-
*/
|
|
12
|
-
export function computeSharedZipRootPrefix(decodedPaths: string[]): string | null {
|
|
13
|
-
const normalized = decodedPaths
|
|
14
|
-
.map((p) => p.replace(/\\/g, '/').replace(/\/+$/, ''))
|
|
15
|
-
.filter((p) => p.length > 0)
|
|
16
|
-
|
|
17
|
-
if (normalized.length === 0) return null
|
|
18
|
-
|
|
19
|
-
const firstSegs = new Set<string>()
|
|
20
|
-
for (const p of normalized) {
|
|
21
|
-
const seg = p.split('/').filter(Boolean)[0]
|
|
22
|
-
if (seg) firstSegs.add(seg)
|
|
23
|
-
}
|
|
24
|
-
if (firstSegs.size !== 1) return null
|
|
25
|
-
|
|
26
|
-
const root = [...firstSegs][0]!
|
|
27
|
-
const prefix = `${root}/`
|
|
28
|
-
for (const p of normalized) {
|
|
29
|
-
if (p !== root && !p.startsWith(prefix)) return null
|
|
30
|
-
}
|
|
31
|
-
return root
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function assertSafeZipTarget(destDir: string, relativePath: string): string {
|
|
35
|
-
const resolvedPath = path.resolve(destDir, relativePath)
|
|
36
|
-
const resolvedDest = path.resolve(destDir)
|
|
37
|
-
const rel = path.relative(resolvedDest, resolvedPath)
|
|
38
|
-
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
39
|
-
throw new Error(`zip 路径越界: ${relativePath}`)
|
|
40
|
-
}
|
|
41
|
-
return resolvedPath
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
type ZipEntry = {
|
|
45
|
-
path: string
|
|
46
|
-
pathBuffer: Buffer
|
|
47
|
-
flags: number
|
|
48
|
-
type: string
|
|
49
|
-
stream: (password?: string) => NodeJS.ReadableStream
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* 将已下载的 zip 解压到 destDir;顶层单根目录(若存在)会被剥掉,与原先流式 Parse 行为一致,但根目录由全量路径计算,与条目顺序无关。
|
|
54
|
-
*/
|
|
55
|
-
export async function extractZipBufferToDirectory(buf: Buffer, destDir: string): Promise<void> {
|
|
56
|
-
const directory = await unzipper.Open.buffer(buf)
|
|
57
|
-
const files = (await directory.files) as ZipEntry[]
|
|
58
|
-
|
|
59
|
-
const decodedPaths = files.map((entry) =>
|
|
60
|
-
decodeZipEntryPath(entry.pathBuffer, entry.flags ?? 0, entry.path)
|
|
61
|
-
)
|
|
62
|
-
const rootDir = computeSharedZipRootPrefix(decodedPaths)
|
|
63
|
-
|
|
64
|
-
// 与 unzipper 默认 extract 一致:串行读各 entry,避免同一 buffer 上多路解压竞争
|
|
65
|
-
for (let i = 0; i < files.length; i++) {
|
|
66
|
-
const entry = files[i]!
|
|
67
|
-
const entryPath = decodedPaths[i]!
|
|
68
|
-
let newPath = entryPath.replace(/\\/g, '/')
|
|
69
|
-
if (rootDir) {
|
|
70
|
-
if (newPath === rootDir || newPath === `${rootDir}/`) {
|
|
71
|
-
continue
|
|
72
|
-
}
|
|
73
|
-
if (newPath.startsWith(`${rootDir}/`)) {
|
|
74
|
-
newPath = newPath.slice(rootDir.length + 1)
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
newPath = newPath.replace(/\/+$/, '')
|
|
78
|
-
if (!newPath) continue
|
|
79
|
-
|
|
80
|
-
const targetPath = assertSafeZipTarget(destDir, newPath)
|
|
81
|
-
|
|
82
|
-
if (entry.type === 'Directory') {
|
|
83
|
-
fs.mkdirSync(targetPath, { recursive: true })
|
|
84
|
-
continue
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const parentDir = path.dirname(targetPath)
|
|
88
|
-
fs.mkdirSync(parentDir, { recursive: true })
|
|
89
|
-
const writeStream = fs.createWriteStream(targetPath)
|
|
90
|
-
try {
|
|
91
|
-
await pipeline(entry.stream(), writeStream)
|
|
92
|
-
} catch (err) {
|
|
93
|
-
const message = err instanceof Error ? err.message : String(err)
|
|
94
|
-
throw new Error(`解压文件失败 ${entryPath}: ${message}`)
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|