@dcrays/dcgchat-test 0.6.10 → 0.6.12
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 +167 -0
- package/index.ts +35 -0
- package/openclaw.plugin.json +12 -41
- package/package.json +35 -8
- package/src/agent.ts +568 -0
- package/src/bot.ts +646 -0
- package/src/channel/index.ts +329 -0
- package/src/channel/outboundMedia.ts +144 -0
- package/src/channel/outboundTarget.ts +62 -0
- package/src/channel/uploadMediaUrl.ts +76 -0
- package/src/cron/message.ts +114 -0
- package/src/cron/params.ts +164 -0
- package/src/cron/toolGuard.ts +466 -0
- package/src/cron/types.ts +15 -0
- package/src/gateway/index.ts +430 -0
- package/src/gateway/security.ts +95 -0
- package/src/gateway/socket.ts +256 -0
- 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 +269 -0
- package/src/request/api.ts +80 -0
- package/src/request/oss.ts +200 -0
- package/src/request/request.ts +191 -0
- package/src/request/userInfo.ts +44 -0
- package/src/session.ts +28 -0
- package/src/skill.ts +114 -0
- package/src/tool.ts +603 -0
- package/src/tools/messageTool.ts +334 -0
- package/src/tools/toolCallGuard.ts +327 -0
- package/src/transport.ts +217 -0
- package/src/types.ts +139 -0
- package/src/utils/agentErrors.ts +116 -0
- package/src/utils/constant.ts +60 -0
- package/src/utils/env-config.ts +19 -0
- package/src/utils/formatLlmInputEvent.ts +48 -0
- package/src/utils/gatewayMsgHandler.ts +147 -0
- package/src/utils/global.ts +309 -0
- package/src/utils/inboundTurnState.ts +66 -0
- package/src/utils/log.ts +77 -0
- package/src/utils/mediaAttached.ts +244 -0
- package/src/utils/mediaEmitter.ts +54 -0
- package/src/utils/outboundAssistantText.ts +117 -0
- package/src/utils/params.ts +104 -0
- package/src/utils/passTxt.ts +4 -0
- package/src/utils/resolveRegisterConfig.ts +33 -0
- package/src/utils/searchFile.ts +228 -0
- package/src/utils/sessionState.ts +137 -0
- package/src/utils/sessionTermination.ts +228 -0
- package/src/utils/streamMerge.ts +150 -0
- package/src/utils/subagentRunMap.ts +71 -0
- package/src/utils/undiciFetchInterceptor.ts +346 -0
- package/src/utils/workspaceFilePaths.ts +18 -0
- package/src/utils/wsMessageHandler.ts +124 -0
- package/src/utils/zipExtract.ts +97 -0
- package/src/utils/zipPath.ts +24 -0
- package/index.js +0 -304
package/src/transport.ts
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { WebSocket } from 'ws'
|
|
2
|
+
import { clearSentMediaKeys, getWsConnection, resolveOutboundMediaDedupeKey } from './utils/global.js'
|
|
3
|
+
import { dcgLogger } from './utils/log.js'
|
|
4
|
+
import type { IMsgParams } from './types.js'
|
|
5
|
+
import { getEffectiveMsgParams, getParamsDefaults } from './utils/params.js'
|
|
6
|
+
|
|
7
|
+
/** 用 sessionKey 从 map 取参,再合并 overrides(channel 出站、媒体等) */
|
|
8
|
+
export function mergeSessionParams(sessionKey: string, overrides?: Partial<IMsgParams>): IMsgParams {
|
|
9
|
+
const base = getEffectiveMsgParams(sessionKey)
|
|
10
|
+
if (!overrides) return base
|
|
11
|
+
return { ...base, ...overrides }
|
|
12
|
+
}
|
|
13
|
+
export function mergeDefaultParams(overrides?: Partial<IMsgParams>): IMsgParams {
|
|
14
|
+
const base = getParamsDefaults()
|
|
15
|
+
if (!overrides) return base
|
|
16
|
+
return { ...base, ...overrides }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type InboundMsgForContext = {
|
|
20
|
+
_userId: number | string
|
|
21
|
+
content: {
|
|
22
|
+
bot_token: string
|
|
23
|
+
domain_id?: string
|
|
24
|
+
app_id?: string
|
|
25
|
+
bot_id?: string
|
|
26
|
+
agent_id?: string
|
|
27
|
+
session_id: string
|
|
28
|
+
message_id: string
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type OpenclawBotChatEnvelope = {
|
|
33
|
+
messageType: 'openclaw_bot_chat'
|
|
34
|
+
_userId: number | undefined
|
|
35
|
+
source: 'client'
|
|
36
|
+
content: Record<string, unknown>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function formatWsEnvelopeForLog(envelope: OpenclawBotChatEnvelope): string {
|
|
40
|
+
const c = envelope.content
|
|
41
|
+
const sid = c.session_id
|
|
42
|
+
const mid = c.message_id
|
|
43
|
+
const state = c.state
|
|
44
|
+
const r = c.response
|
|
45
|
+
const files = c.files
|
|
46
|
+
const respLen = typeof r === 'string' ? r.length : 0
|
|
47
|
+
const fileHint = Array.isArray(files) ? ` files=${files.length}` : ''
|
|
48
|
+
return `[ws] ${envelope.messageType} session_id=${sid} message_id=${mid} state=${String(state)} responseLen=${respLen}${fileHint}`
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isInboundWire(arg: unknown): arg is InboundMsgForContext {
|
|
52
|
+
return Boolean(arg && typeof arg === 'object' && '_userId' in arg && 'content' in arg)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** 下行 WebSocket 帧 → 内部上下文(字段缺省用 channel 配置补) */
|
|
56
|
+
function inboundToCtx(msg: InboundMsgForContext, d: IMsgParams): IMsgParams {
|
|
57
|
+
const c = msg.content
|
|
58
|
+
return {
|
|
59
|
+
userId: Number(msg._userId ?? d.userId),
|
|
60
|
+
botToken: c.bot_token ?? d.botToken,
|
|
61
|
+
domainId: String(c.domain_id ?? d.domainId),
|
|
62
|
+
appId: String(c.app_id ?? d.appId),
|
|
63
|
+
botId: c.bot_id,
|
|
64
|
+
agentId: c.agent_id,
|
|
65
|
+
sessionId: c.session_id,
|
|
66
|
+
messageId: c.message_id
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** 上行:与配置合并缺省后再 `...ctx` 覆盖(原 wsSendRaw) */
|
|
71
|
+
function mergeOutboundWithDefaults(ctx: IMsgParams, d: IMsgParams): IMsgParams {
|
|
72
|
+
return {
|
|
73
|
+
userId: Number(ctx.userId ?? d.userId),
|
|
74
|
+
botToken: ctx.botToken ?? d.botToken,
|
|
75
|
+
domainId: String(ctx.domainId ?? d.domainId),
|
|
76
|
+
appId: String(ctx.appId ?? d.appId),
|
|
77
|
+
...ctx
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 组装完整 wire `content` 对象:先写会话/机器人基础字段(回落到 d),再合并调用方传入的 payload。
|
|
83
|
+
* `content` 在使用处构造(如 response、state、files),同名键可覆盖基础字段。
|
|
84
|
+
*/
|
|
85
|
+
export function buildWireContent(base: IMsgParams, d: IMsgParams, content: Record<string, unknown>): Record<string, unknown> {
|
|
86
|
+
return {
|
|
87
|
+
bot_token: base.botToken ?? d.botToken,
|
|
88
|
+
domain_id: base.domainId ?? d.domainId,
|
|
89
|
+
app_id: base.appId ?? d.appId,
|
|
90
|
+
bot_id: base.botId,
|
|
91
|
+
agent_id: base.agentId,
|
|
92
|
+
session_id: base.sessionId,
|
|
93
|
+
message_id: base.messageId || Date.now().toString(),
|
|
94
|
+
...content
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** 上行:在已合并的 ctx 上套 openclaw_bot_chat 信封(messageType / _userId / source + content) */
|
|
99
|
+
function buildOutboundOpenclawBotChatEnvelope(
|
|
100
|
+
ctx: IMsgParams,
|
|
101
|
+
content: Record<string, unknown>,
|
|
102
|
+
opts?: { mergeChannelDefaults?: boolean }
|
|
103
|
+
): OpenclawBotChatEnvelope {
|
|
104
|
+
const d = getParamsDefaults()
|
|
105
|
+
const base = opts?.mergeChannelDefaults ? mergeOutboundWithDefaults(ctx, d) : ctx
|
|
106
|
+
return {
|
|
107
|
+
messageType: 'openclaw_bot_chat',
|
|
108
|
+
_userId: base.userId,
|
|
109
|
+
source: 'client',
|
|
110
|
+
content: buildWireContent(base, d, content)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* 下行解析为 DcgchatMsgContext,或上行组装 openclaw_bot_chat 信封。
|
|
116
|
+
* 上行时 `content` 由调用方传入;基础参数来自 `ctx` 与 `getParamsDefaults()`(可选 mergeChannelDefaults,同原 wsSendRaw)。
|
|
117
|
+
*/
|
|
118
|
+
export function buildOpenclawBotChat(msg: InboundMsgForContext): IMsgParams
|
|
119
|
+
export function buildOpenclawBotChat(
|
|
120
|
+
ctx: IMsgParams,
|
|
121
|
+
content: Record<string, unknown>,
|
|
122
|
+
opts?: { mergeChannelDefaults?: boolean }
|
|
123
|
+
): OpenclawBotChatEnvelope
|
|
124
|
+
export function buildOpenclawBotChat(
|
|
125
|
+
arg1: InboundMsgForContext | IMsgParams,
|
|
126
|
+
arg2?: Record<string, unknown>,
|
|
127
|
+
opts?: { mergeChannelDefaults?: boolean }
|
|
128
|
+
): IMsgParams | OpenclawBotChatEnvelope {
|
|
129
|
+
const d = getParamsDefaults()
|
|
130
|
+
|
|
131
|
+
if (arg2 === undefined && isInboundWire(arg1)) {
|
|
132
|
+
return inboundToCtx(arg1, d)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const ctx = arg1 as IMsgParams
|
|
136
|
+
return buildOutboundOpenclawBotChatEnvelope(ctx, arg2 ?? {}, opts)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function isWsOpen(): boolean {
|
|
140
|
+
const isOpen = getWsConnection()?.readyState === WebSocket.OPEN
|
|
141
|
+
if (!isOpen) {
|
|
142
|
+
dcgLogger(`server socket not ready ${getWsConnection()?.readyState}`, 'error')
|
|
143
|
+
}
|
|
144
|
+
return isOpen
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* 聊天流路径:content 单独 JSON.stringify(双重编码),符合 dcgchat 协议。
|
|
149
|
+
* `ctx` 须由调用方用 getEffectiveMsgParams(sessionKey) 等解析好;`content` 为完整业务 payload。
|
|
150
|
+
*/
|
|
151
|
+
export function wsSend(ctx: IMsgParams, content: Record<string, unknown>, isLog = true): boolean {
|
|
152
|
+
const ws = getWsConnection()
|
|
153
|
+
if (ws?.readyState !== WebSocket.OPEN) return false
|
|
154
|
+
const envelope = buildOpenclawBotChat(ctx, content)
|
|
155
|
+
ws.send(JSON.stringify({ ...envelope, content: JSON.stringify(envelope.content) }))
|
|
156
|
+
if (isLog) {
|
|
157
|
+
dcgLogger('[插件=>后端]-[wsSend]:' + formatWsEnvelopeForLog(envelope))
|
|
158
|
+
}
|
|
159
|
+
return true
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* 媒体 / channel 出站:content 保持嵌套对象(单次编码)。
|
|
164
|
+
* `ctx` 须由调用方解析(如需合并覆盖可先 mergeSessionParams)。
|
|
165
|
+
*/
|
|
166
|
+
export function wsSendRaw(ctx: IMsgParams, content: Record<string, unknown>, isLog = true): boolean {
|
|
167
|
+
const ws = getWsConnection()
|
|
168
|
+
if (ws?.readyState !== WebSocket.OPEN) {
|
|
169
|
+
dcgLogger(`server socket not ready ${ws?.readyState}`, 'error')
|
|
170
|
+
return false
|
|
171
|
+
}
|
|
172
|
+
const envelope = buildOpenclawBotChat(ctx, content, { mergeChannelDefaults: true })
|
|
173
|
+
ws.send(JSON.stringify(envelope))
|
|
174
|
+
if (isLog) {
|
|
175
|
+
dcgLogger('[插件=>后端]-[wsSendRaw]:' + formatWsEnvelopeForLog(envelope))
|
|
176
|
+
}
|
|
177
|
+
return true
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function sendChunk(text: string, ctx: IMsgParams, chunkIdx: number): boolean {
|
|
181
|
+
return wsSend(ctx, { response: text, state: 'chunk', chunk_idx: chunkIdx }, false)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function sendFinal(ctx: IMsgParams, tag: string, text?: string, params?: Record<string, unknown>): boolean {
|
|
185
|
+
dcgLogger(` message handling complete state: to=${ctx.sessionId} final tag:${tag}`)
|
|
186
|
+
const mediaBucket = resolveOutboundMediaDedupeKey(ctx.messageId, ctx.sessionId, ctx.sessionKey)
|
|
187
|
+
if (mediaBucket) clearSentMediaKeys(mediaBucket)
|
|
188
|
+
return wsSend(ctx, { response: text || '', state: 'final', ...(params || {}) }, false)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function sendText(text: string, ctx: IMsgParams, event?: Record<string, unknown>): boolean {
|
|
192
|
+
return wsSend(ctx, { response: text, ...event })
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function sendError(errorMsg: string, ctx: IMsgParams): boolean {
|
|
196
|
+
return wsSend(ctx, { response: `[错误] ${errorMsg}`, state: 'final' })
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function sendEventMessage(params: Record<string, any> = {}) {
|
|
200
|
+
const ctx = getParamsDefaults()
|
|
201
|
+
const ws = getWsConnection()
|
|
202
|
+
if (isWsOpen()) {
|
|
203
|
+
const conntent = JSON.stringify({
|
|
204
|
+
messageType: 'openclaw_bot_event',
|
|
205
|
+
source: 'client',
|
|
206
|
+
content: {
|
|
207
|
+
bot_token: ctx.botToken,
|
|
208
|
+
domain_id: ctx.domainId,
|
|
209
|
+
app_id: ctx.appId,
|
|
210
|
+
bot_id: ctx.botId,
|
|
211
|
+
...params
|
|
212
|
+
}
|
|
213
|
+
})
|
|
214
|
+
dcgLogger(`[插件=>后端]-[sendEventMessage]:${conntent}`)
|
|
215
|
+
ws?.send(conntent)
|
|
216
|
+
}
|
|
217
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 插件配置(channels.dcgchat 下的字段)
|
|
3
|
+
*/
|
|
4
|
+
export type DcgchatConfig = {
|
|
5
|
+
enabled?: boolean
|
|
6
|
+
/** 后端 WebSocket 地址,例如 ws://localhost:8080/openclaw/ws */
|
|
7
|
+
wsUrl?: string
|
|
8
|
+
/** 连接认证 token */
|
|
9
|
+
botToken?: string
|
|
10
|
+
/** 用户标识 */
|
|
11
|
+
userId?: string
|
|
12
|
+
domainId?: string
|
|
13
|
+
appId?: string
|
|
14
|
+
/**
|
|
15
|
+
* 内置 `message` 工具走 OpenClaw 目标解析:`true`(默认)时仅将符合 sessionKey 形态的字符串视为合法 target,
|
|
16
|
+
* 纯数字(WS userId 等)会解析失败;设为 `false` 恢复旧版宽松行为(不推荐)。
|
|
17
|
+
*/
|
|
18
|
+
strictMessageToolTarget?: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type ResolvedDcgchatAccount = {
|
|
22
|
+
accountId: string
|
|
23
|
+
enabled: boolean
|
|
24
|
+
configured: boolean
|
|
25
|
+
wsUrl: string
|
|
26
|
+
botToken: string
|
|
27
|
+
userId: string
|
|
28
|
+
domainId?: string
|
|
29
|
+
appId?: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 下行消息:后端 → OpenClaw(用户发的消息)
|
|
34
|
+
*/
|
|
35
|
+
// export type InboundMessage = {
|
|
36
|
+
// type: "message";
|
|
37
|
+
// userId: string;
|
|
38
|
+
// text: string;
|
|
39
|
+
// };
|
|
40
|
+
export type InboundMessage = {
|
|
41
|
+
messageType: string // "openclaw_bot_chat",
|
|
42
|
+
_userId: number
|
|
43
|
+
source: string // 'server',
|
|
44
|
+
// content: string;
|
|
45
|
+
content: {
|
|
46
|
+
skills_scope?: Record<string, any>[]
|
|
47
|
+
bot_token: string
|
|
48
|
+
agent_clone_code?: string
|
|
49
|
+
domain_id?: string
|
|
50
|
+
app_id?: string
|
|
51
|
+
bot_id?: string
|
|
52
|
+
agent_id?: string
|
|
53
|
+
session_id: string
|
|
54
|
+
real_mobook: string | number
|
|
55
|
+
message_id: string
|
|
56
|
+
text: string
|
|
57
|
+
files?: {
|
|
58
|
+
url: string
|
|
59
|
+
name: string
|
|
60
|
+
}[]
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// {"_userId":40,"content":"{\"bot_token\":\"sk_b7f8a3e1c5d24e6f8a1b3c4d5e6f7a8b\",\"session_id\":\"1\",\"message_id\":\"1\",\"text\":\"你好\"}","messageType":"openclaw_bot_chat","msgId":398599,"source":"server","title":"OPENCLAW机器人对话"}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 上行消息:OpenClaw → 后端(Agent 回复)
|
|
68
|
+
*/
|
|
69
|
+
// export type OutboundReply = {
|
|
70
|
+
// type: "reply";
|
|
71
|
+
// userId: string;
|
|
72
|
+
// text: string;
|
|
73
|
+
// };
|
|
74
|
+
|
|
75
|
+
export type OutboundReply = {
|
|
76
|
+
messageType: string // "openclaw_bot_chat",
|
|
77
|
+
_userId: number // 100
|
|
78
|
+
source: string // 'client',
|
|
79
|
+
// content: string;
|
|
80
|
+
content: {
|
|
81
|
+
bot_token: string // ""
|
|
82
|
+
session_id: string // ""
|
|
83
|
+
message_id: string // ""
|
|
84
|
+
response: string // ""
|
|
85
|
+
state: string // final, chunk
|
|
86
|
+
domain_id?: string
|
|
87
|
+
app_id?: string
|
|
88
|
+
bot_id?: string
|
|
89
|
+
agent_id?: string
|
|
90
|
+
files?: { url: string; name: string }[]
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface IResponse<T = unknown> {
|
|
95
|
+
/** 响应状态码 */
|
|
96
|
+
code?: number | string
|
|
97
|
+
/** 响应数据 */
|
|
98
|
+
data?: T
|
|
99
|
+
/** 响应消息 */
|
|
100
|
+
message?: string
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface IStsToken {
|
|
104
|
+
bucket: string
|
|
105
|
+
endPoint: string
|
|
106
|
+
expiration: string
|
|
107
|
+
ossFileKey: string
|
|
108
|
+
policy: string
|
|
109
|
+
region: string
|
|
110
|
+
signature: string
|
|
111
|
+
sourceFileName: string
|
|
112
|
+
stsEndPoint: string
|
|
113
|
+
tempAccessKeyId: string
|
|
114
|
+
tempAccessKeySecret: string
|
|
115
|
+
tempSecurityToken: string
|
|
116
|
+
uploadDir: string
|
|
117
|
+
protocol: string
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface IStsTokenReq {
|
|
121
|
+
sourceFileName: string
|
|
122
|
+
isPrivate: number
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface IMsgParams {
|
|
126
|
+
userId?: number
|
|
127
|
+
botToken?: string
|
|
128
|
+
sessionId?: string
|
|
129
|
+
messageId?: string
|
|
130
|
+
domainId?: string
|
|
131
|
+
appId?: string
|
|
132
|
+
botId?: string
|
|
133
|
+
agentId?: string
|
|
134
|
+
/** 与 OpenClaw 路由一致,用于 map 与异步链路(工具 / HTTP / cron)对齐当前会话 */
|
|
135
|
+
sessionKey?: string
|
|
136
|
+
real_mobook?: string | number
|
|
137
|
+
is_finish?: number
|
|
138
|
+
message_tags?: Record<string, string>
|
|
139
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 识别 OpenClaw embedded agent 抛出的上下文/压缩相关错误(日志与异常文案可能略有差异)。
|
|
3
|
+
*/
|
|
4
|
+
function errorText(err: unknown): string {
|
|
5
|
+
if (err instanceof Error) return err.message
|
|
6
|
+
if (typeof err === 'string') return err
|
|
7
|
+
return String(err)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function isContextOverflowError(err: unknown): boolean {
|
|
11
|
+
const t = errorText(err)
|
|
12
|
+
return /context overflow/i.test(t) || /prompt too large/i.test(t) || /auto-compaction failed/i.test(t) || /\(precheck\)/i.test(t)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** 用户可见说明(不含 transport 层前缀) */
|
|
16
|
+
export function contextOverflowUserHint(): string {
|
|
17
|
+
return '当前对话过长,已超过模型上下文限制;自动压缩未完全生效时会出现此情况。请尝试新开对话、缩短任务,或换用更大上下文的模型。'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function ensureChinesePeriod(text: string): string {
|
|
21
|
+
const t = text.trim()
|
|
22
|
+
if (!t) return t
|
|
23
|
+
return /[。!?.!?]$/u.test(t) ? t : `${t}。`
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** 将 OpenClaw/agent 常见英文错误转换为用户可见中文说明。 */
|
|
27
|
+
export function agentErrorUserHint(err: unknown): string | undefined {
|
|
28
|
+
if (isContextOverflowError(err)) return contextOverflowUserHint()
|
|
29
|
+
|
|
30
|
+
const t = errorText(err).trim()
|
|
31
|
+
if (!t) return undefined
|
|
32
|
+
|
|
33
|
+
if (/Content Exists Risk/i.test(t)) {
|
|
34
|
+
return '当前检索内容包含风险信息,出于内容安全规范,暂不支持输出相关回答。'
|
|
35
|
+
}
|
|
36
|
+
if (/Agent couldn't generate a response\.\s*Note:\s*some tool actions may have already been executed/i.test(t)) {
|
|
37
|
+
return '很抱歉,本次对话未能生成回复。部分工具操作可能已经执行,请先核对执行结果,确认后再决定是否重试。'
|
|
38
|
+
}
|
|
39
|
+
if (/Agent couldn't generate a response/i.test(t)) {
|
|
40
|
+
return '很抱歉,本次对话未能生成回复,你可以重新发送提问再试试看~'
|
|
41
|
+
}
|
|
42
|
+
if (/API rate limit reached/i.test(t)) {
|
|
43
|
+
return 'API 调用频率已达上限,请稍后重试。'
|
|
44
|
+
}
|
|
45
|
+
if (/The AI service is temporarily overloaded/i.test(t)) {
|
|
46
|
+
return 'AI 服务当前繁忙,请稍后重试。'
|
|
47
|
+
}
|
|
48
|
+
if (/The AI service returned an error/i.test(t)) {
|
|
49
|
+
return 'AI 服务返回异常,请重试。'
|
|
50
|
+
}
|
|
51
|
+
if (/The model did not produce a response before the model idle timeout/i.test(t)) {
|
|
52
|
+
return '模型在超时时间内未返回结果,请重试。'
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const imageMax = t.match(/Image too large for the model\s*\(max\s+([^)]+)\)/i)
|
|
56
|
+
if (imageMax?.[1]) {
|
|
57
|
+
return `图片超过模型支持的大小(最大 ${imageMax[1].trim()}),请压缩或缩小图片后重试。`
|
|
58
|
+
}
|
|
59
|
+
if (/Image too large for the model/i.test(t)) {
|
|
60
|
+
return '图片超过模型支持的大小,请压缩或缩小图片后重试。'
|
|
61
|
+
}
|
|
62
|
+
if (/Request failed after repeated internal retries/i.test(t)) {
|
|
63
|
+
return '系统多次自动重试后仍未成功,请重试;如问题持续存在,请使用 /new 开启新的会话。'
|
|
64
|
+
}
|
|
65
|
+
if (/Message ordering conflict/i.test(t)) {
|
|
66
|
+
return '会话消息顺序发生冲突,请重试;如问题持续存在,请使用 /new 开启新的会话。'
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const backendFallback = t.match(
|
|
70
|
+
/couldn't reach the configured model backend\s+(.+?)\.\s*Fallback used\s+(.+?),\s*but it produced no visible reply/i
|
|
71
|
+
)
|
|
72
|
+
if (backendFallback?.[1] && backendFallback[2]) {
|
|
73
|
+
return `⚠️ 无法连接到配置的模型 ${backendFallback[1].trim()},已切换至备用模型 ${backendFallback[2].trim()},但仍未生成可见回复。`
|
|
74
|
+
}
|
|
75
|
+
if (/Follow-up completed, but OpenClaw could not deliver it to the originating channel/i.test(t)) {
|
|
76
|
+
return '后续任务已完成,但无法将回复发送回原始会话。为避免消息发送到错误的渠道,本次回复未自动转发。'
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const memoryFlush = t.match(/Memory flush failed after\s+(\S+)\s+attempts/i)
|
|
80
|
+
if (memoryFlush?.[1]) {
|
|
81
|
+
return `⚠️ Memory 写入连续失败 ${memoryFlush[1].trim()} 次,本轮已跳过,将在下一次整理时自动重试。`
|
|
82
|
+
}
|
|
83
|
+
if (/All models are temporarily rate-limited/i.test(t)) {
|
|
84
|
+
return '所有模型当前都受到频率限制,请稍后重试。'
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const allModelsFailed = t.match(/All models failed\s*\(([^)]+)\)/i)
|
|
88
|
+
if (allModelsFailed?.[1]) {
|
|
89
|
+
return `所有模型均调用失败(共 ${allModelsFailed[1].trim()} 个)。`
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const btwFailedWithMessage = t.match(/\/btw failed:\s*(.+)$/i)
|
|
93
|
+
if (btwFailedWithMessage?.[1]) {
|
|
94
|
+
return `⚠️ /btw 命令执行失败:${ensureChinesePeriod(btwFailedWithMessage[1])}`
|
|
95
|
+
}
|
|
96
|
+
if (/\/btw failed/i.test(t)) {
|
|
97
|
+
return '⚠️ /btw 命令执行失败。'
|
|
98
|
+
}
|
|
99
|
+
if (/The provider returned an empty response/i.test(t)) {
|
|
100
|
+
return '很抱歉,本次对话未能生成回复,你可以重新发送提问再试试看~。'
|
|
101
|
+
}
|
|
102
|
+
if (/Unknown error \(no error details in response\)/i.test(t)) {
|
|
103
|
+
return '未知错误(未获取到错误详情)。'
|
|
104
|
+
}
|
|
105
|
+
if (/Unknown error/i.test(t)) {
|
|
106
|
+
return '未知错误。'
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return undefined
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** 仅翻译 OpenClaw 明确标记为错误的助手载荷,普通正文始终原样返回。 */
|
|
113
|
+
export function agentErrorPayloadText(text: string, isError: boolean): string {
|
|
114
|
+
if (!isError) return text
|
|
115
|
+
return agentErrorUserHint(text) ?? text
|
|
116
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { ENV_CONFIG, DEFAULT_BUILD_ENV, BuildEnv } from './env-config.js'
|
|
2
|
+
|
|
3
|
+
declare const __ENV__: BuildEnv
|
|
4
|
+
declare const __CHANNEL_ID__: string
|
|
5
|
+
|
|
6
|
+
// esbuild define 会将 __ENV__ / __CHANNEL_ID__ 内联为字面量
|
|
7
|
+
|
|
8
|
+
export const ENV: BuildEnv =
|
|
9
|
+
typeof __ENV__ !== 'undefined' ? __ENV__ : DEFAULT_BUILD_ENV
|
|
10
|
+
export const CHANNEL_ID: string =
|
|
11
|
+
typeof __CHANNEL_ID__ !== 'undefined'
|
|
12
|
+
? __CHANNEL_ID__
|
|
13
|
+
: ENV_CONFIG[DEFAULT_BUILD_ENV].channelId
|
|
14
|
+
|
|
15
|
+
export const systemCommand = ['/new', '/status']
|
|
16
|
+
export const stopCommand = ['/stop']
|
|
17
|
+
|
|
18
|
+
export const ignoreToolCommand = ['/search', '/abort', '/queue interrupt', ...systemCommand, ...stopCommand]
|
|
19
|
+
|
|
20
|
+
/* ── 网络与定时器统一常量 ── */
|
|
21
|
+
|
|
22
|
+
/** WebSocket 心跳/Ping 间隔(毫秒) */
|
|
23
|
+
export const WS_HEARTBEAT_INTERVAL_MS = 30_000
|
|
24
|
+
|
|
25
|
+
/** WebSocket 重连延迟(毫秒) */
|
|
26
|
+
export const WS_RECONNECT_DELAY_MS = 5_000
|
|
27
|
+
|
|
28
|
+
/** OpenClaw Gateway WebSocket 握手协议版本(须与宿主 gateway 一致,当前为 v4) */
|
|
29
|
+
export const GATEWAY_PROTOCOL_VERSION = 4
|
|
30
|
+
|
|
31
|
+
/** /stop 后立即发新消息时,等待宿主 reply run 释放的 settle 时间(毫秒) */
|
|
32
|
+
export const AFTER_STOP_SETTLE_MS = 50
|
|
33
|
+
|
|
34
|
+
/** 等待子 agent 空闲的最大超时时间(毫秒) */
|
|
35
|
+
export const SUBAGENT_IDLE_TIMEOUT_MS = 60 * 60 * 1000
|
|
36
|
+
|
|
37
|
+
/* ── 通用重试工具 ── */
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 带指数退避的重试函数
|
|
41
|
+
* @param fn 需要重试的异步操作
|
|
42
|
+
* @param maxAttempts 最大尝试次数(默认3)
|
|
43
|
+
* @param initialDelayMs 初始延迟毫秒(默认500)
|
|
44
|
+
*/
|
|
45
|
+
export async function retryWithBackoff<T>(
|
|
46
|
+
fn: () => Promise<T>,
|
|
47
|
+
maxAttempts: number = 3,
|
|
48
|
+
initialDelayMs: number = 500
|
|
49
|
+
): Promise<T> {
|
|
50
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
51
|
+
try {
|
|
52
|
+
return await fn()
|
|
53
|
+
} catch (err) {
|
|
54
|
+
if (attempt === maxAttempts - 1) throw err
|
|
55
|
+
const delay = initialDelayMs * Math.pow(2, attempt)
|
|
56
|
+
await new Promise((resolve) => setTimeout(resolve, delay))
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
throw new Error('retryWithBackoff: unreachable')
|
|
60
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type BuildEnv = 'production' | 'test'
|
|
2
|
+
|
|
3
|
+
export interface EnvConfig {
|
|
4
|
+
packageName: string
|
|
5
|
+
channelId: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const ENV_CONFIG: Record<BuildEnv, EnvConfig> = {
|
|
9
|
+
production: {
|
|
10
|
+
packageName: '@dcrays/dcgchat',
|
|
11
|
+
channelId: 'dcgchat'
|
|
12
|
+
},
|
|
13
|
+
test: {
|
|
14
|
+
packageName: '@dcrays/dcgchat-test',
|
|
15
|
+
channelId: 'dcgchat-test'
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const DEFAULT_BUILD_ENV: BuildEnv = 'test'
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export function formatLlmInputEvent(event: Record<string, unknown>, ctx: any): string {
|
|
2
|
+
try {
|
|
3
|
+
const runId = String(event.runId ?? '?')
|
|
4
|
+
const sessionId = String(event.sessionId ?? '?')
|
|
5
|
+
const provider = String(event.provider ?? '?')
|
|
6
|
+
const model = String(event.model ?? '?')
|
|
7
|
+
const imagesCount = Number(event.imagesCount ?? 0)
|
|
8
|
+
const historyLen = Array.isArray(event.historyMessages) ? event.historyMessages.length : 0
|
|
9
|
+
const prompt = String(event.prompt ?? '')
|
|
10
|
+
const systemPrompt = event.systemPrompt != null ? String(event.systemPrompt) : ''
|
|
11
|
+
|
|
12
|
+
const sessionKey = ctx?.sessionKey || ''
|
|
13
|
+
const agentId = ctx?.agentId || ''
|
|
14
|
+
|
|
15
|
+
const lines: string[] = []
|
|
16
|
+
lines.push(`===== llm_input =====`)
|
|
17
|
+
lines.push(
|
|
18
|
+
`sessionKey=${sessionKey} agentId=${agentId} runId=${runId} sessionId=${sessionId} provider=${provider} model=${model} images=${imagesCount} historyMessages=${historyLen} systemPromptLenght=${systemPrompt.length} promptLength=${prompt.length}`
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
const sampleText = extractTextPreview(prompt, 200)
|
|
22
|
+
lines.push(`[TEXT] ${sampleText}${sampleText.length === 200 ? '...' : ''}`)
|
|
23
|
+
return lines.join('\n')
|
|
24
|
+
} catch (error) {
|
|
25
|
+
return `formatLlmInputEvent Error: ${error}`
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function extractTextPreview(prompt: string, maxLen: number): string {
|
|
30
|
+
let result = ''
|
|
31
|
+
|
|
32
|
+
for (const seg of prompt.split('\n\n')) {
|
|
33
|
+
const raw = seg.trim()
|
|
34
|
+
if (!raw) continue
|
|
35
|
+
if (raw.startsWith('### Inbound message metadata')) continue
|
|
36
|
+
if (raw.startsWith('### Chat history since last reply')) continue
|
|
37
|
+
if (raw.startsWith('[media attached:')) continue
|
|
38
|
+
if (raw.startsWith('Conversation info')) continue
|
|
39
|
+
if (raw.startsWith('Sender (untrusted metadata)')) continue
|
|
40
|
+
if (raw.startsWith('```json') && raw.includes('message_id')) continue
|
|
41
|
+
if (raw.includes('[media:') || raw.includes('<media:') || raw.includes('<file ')) continue
|
|
42
|
+
|
|
43
|
+
if (result) result += '\n\n'
|
|
44
|
+
result += raw
|
|
45
|
+
if (result.length >= maxLen) break
|
|
46
|
+
}
|
|
47
|
+
return result.slice(0, maxLen).replace(/\n/g, ' ')
|
|
48
|
+
}
|