@dcrays/dcgchat 0.2.32 → 0.3.18
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.ts +1 -0
- package/package.json +3 -3
- package/src/bot.ts +175 -80
- package/src/channel.ts +52 -31
- package/src/cron.ts +174 -0
- package/src/cronToolCall.ts +187 -0
- package/src/gateway/index.ts +466 -0
- package/src/gateway/security.ts +95 -0
- package/src/gateway/socket.ts +283 -0
- package/src/monitor.ts +66 -46
- package/src/request/api.ts +4 -18
- package/src/request/oss.ts +5 -4
- package/src/request/request.ts +2 -2
- package/src/skill.ts +2 -0
- package/src/tool.ts +72 -69
- package/src/transport.ts +142 -49
- package/src/types.ts +13 -8
- package/src/utils/constant.ts +2 -2
- package/src/utils/global.ts +41 -13
- package/src/utils/params.ts +67 -0
- package/src/utils/searchFile.ts +21 -5
package/src/cron.ts
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import type { IMsgParams } from './types.js'
|
|
4
|
+
import { mergeDefaultParams, sendEventMessage, sendFinal } from './transport.js'
|
|
5
|
+
import { getCronMessageId, getWorkspaceDir, removeCronMessageId, setCronMessageId, setMsgStatus } from './utils/global.js'
|
|
6
|
+
import { ossUpload } from './request/oss.js'
|
|
7
|
+
import { dcgLogger } from './utils/log.js'
|
|
8
|
+
import { sendMessageToGateway } from './gateway/socket.js'
|
|
9
|
+
import { getCurrentSessionKey, getEffectiveMsgParams } from './utils/params.js'
|
|
10
|
+
|
|
11
|
+
export function getCronJobsPath(): string {
|
|
12
|
+
const workspaceDir = getWorkspaceDir()
|
|
13
|
+
const cronDir = workspaceDir.replace('workspace', 'cron')
|
|
14
|
+
return path.join(cronDir, 'jobs.json')
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type CronJobsFile = {
|
|
18
|
+
version?: number
|
|
19
|
+
jobs?: Array<{ id?: string; sessionKey?: string }>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 在 `jobPath` 指向的 jobs.json(通常为 getCronJobsPath())中按 id 查找任务并返回其 sessionKey。
|
|
24
|
+
*/
|
|
25
|
+
export function readCronJobSessionKey(jobPath: string, jobId: string): string | null {
|
|
26
|
+
const id = jobId?.trim()
|
|
27
|
+
if (!id) return null
|
|
28
|
+
if (!fs.existsSync(jobPath)) {
|
|
29
|
+
dcgLogger(`readCronJobSessionKey: file not found ${jobPath}`, 'error')
|
|
30
|
+
return null
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
const raw = fs.readFileSync(jobPath, 'utf8')
|
|
34
|
+
const data = JSON.parse(raw) as CronJobsFile
|
|
35
|
+
const job = (data.jobs ?? []).find((j) => j.id === id)
|
|
36
|
+
const sk = job?.sessionKey?.trim()
|
|
37
|
+
return sk || null
|
|
38
|
+
} catch (e) {
|
|
39
|
+
dcgLogger(`readCronJobSessionKey: failed to read ${jobPath}: ${String(e)}`, 'error')
|
|
40
|
+
return null
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function msgParamsToCtx(p: IMsgParams): IMsgParams | null {
|
|
45
|
+
if (!p?.botToken) return null
|
|
46
|
+
return p
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const CRON_UPLOAD_DEBOUNCE_MS = 6600
|
|
50
|
+
|
|
51
|
+
/** 待合并的上传上下文(短时间内多次调用只保留最后一次) */
|
|
52
|
+
let pendingCronUploadCtx: IMsgParams | null = null
|
|
53
|
+
let cronUploadFlushTimer: ReturnType<typeof setTimeout> | null = null
|
|
54
|
+
|
|
55
|
+
async function runCronJobsUpload(msgCtx: IMsgParams): Promise<void> {
|
|
56
|
+
const jobPath = getCronJobsPath()
|
|
57
|
+
if (fs.existsSync(jobPath)) {
|
|
58
|
+
try {
|
|
59
|
+
const url = await ossUpload(jobPath, msgCtx.botToken ?? '', 0)
|
|
60
|
+
dcgLogger(`定时任务创建成功: ${url}`)
|
|
61
|
+
if (!msgCtx.sessionKey) {
|
|
62
|
+
dcgLogger('runCronJobsUpload: missing sessionKey on msgCtx', 'error')
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
sendEventMessage(url, msgCtx.sessionKey)
|
|
66
|
+
} catch (error) {
|
|
67
|
+
dcgLogger(`${jobPath} upload failed: ${error}`, 'error')
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
70
|
+
dcgLogger(`${jobPath} not found`, 'error')
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function flushCronUploadQueue(): void {
|
|
75
|
+
cronUploadFlushTimer = null
|
|
76
|
+
const ctx = pendingCronUploadCtx
|
|
77
|
+
pendingCronUploadCtx = null
|
|
78
|
+
if (!ctx) return
|
|
79
|
+
void runCronJobsUpload(ctx)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 将 jobs.json 同步到 OSS 并推送事件。30s 内多次调用合并为一次上传;定时触发后清空待处理项,避免重复执行。
|
|
84
|
+
* @param msgCtx 可选;省略时使用当前会话 getEffectiveMsgParams(sessionKey) 快照
|
|
85
|
+
*/
|
|
86
|
+
export function sendDcgchatCron(): void {
|
|
87
|
+
const ctx = msgParamsToCtx(getEffectiveMsgParams(getCurrentSessionKey() ?? ''))
|
|
88
|
+
if (!ctx) {
|
|
89
|
+
dcgLogger('sendDcgchatCron: no message context (missing token / params)', 'error')
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
pendingCronUploadCtx = ctx
|
|
93
|
+
if (cronUploadFlushTimer !== null) {
|
|
94
|
+
clearTimeout(cronUploadFlushTimer)
|
|
95
|
+
}
|
|
96
|
+
cronUploadFlushTimer = setTimeout(flushCronUploadQueue, CRON_UPLOAD_DEBOUNCE_MS)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 通过 OpenClaw CLI 删除定时任务(走 Gateway,与内存状态一致)。
|
|
101
|
+
* 文档:运行中请勿手改 jobs.json,应使用 `openclaw cron rm` 或工具 API。
|
|
102
|
+
*/
|
|
103
|
+
export const onRemoveCronJob = async (jobId: string) => {
|
|
104
|
+
const id = jobId?.trim()
|
|
105
|
+
if (!id) {
|
|
106
|
+
dcgLogger('onRemoveCronJob: empty jobId', 'error')
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
sendMessageToGateway(JSON.stringify({ method: 'cron.remove', params: { id: jobId } }))
|
|
110
|
+
}
|
|
111
|
+
export const onDisabledCronJob = async (jobId: string) => {
|
|
112
|
+
const id = jobId?.trim()
|
|
113
|
+
if (!id) {
|
|
114
|
+
dcgLogger('onRemoveCronJob: empty jobId', 'error')
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
sendMessageToGateway(JSON.stringify({ method: 'cron.update', params: { id: jobId, patch: { enabled: false } } }))
|
|
118
|
+
}
|
|
119
|
+
export const onEnabledCronJob = async (jobId: string) => {
|
|
120
|
+
const id = jobId?.trim()
|
|
121
|
+
if (!id) {
|
|
122
|
+
dcgLogger('onRemoveCronJob: empty jobId', 'error')
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
sendMessageToGateway(JSON.stringify({ method: 'cron.update', params: { id: jobId, patch: { enabled: true } } }))
|
|
126
|
+
}
|
|
127
|
+
export const onRunCronJob = async (jobId: string, messageId: string) => {
|
|
128
|
+
const id = jobId?.trim()
|
|
129
|
+
if (!id) {
|
|
130
|
+
dcgLogger('onRemoveCronJob: empty jobId', 'error')
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
const jobPath = getCronJobsPath()
|
|
134
|
+
const sessionKey = readCronJobSessionKey(jobPath, jobId) || ''
|
|
135
|
+
if (!sessionKey) {
|
|
136
|
+
dcgLogger(`finishedDcgchatCron: no sessionKey for job id=${id}`, 'error')
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
setCronMessageId(sessionKey, messageId)
|
|
140
|
+
sendMessageToGateway(JSON.stringify({ method: 'cron.run', params: { id: jobId } }))
|
|
141
|
+
}
|
|
142
|
+
export const finishedDcgchatCron = async (jobId: string) => {
|
|
143
|
+
const id = jobId?.trim()
|
|
144
|
+
if (!id) {
|
|
145
|
+
dcgLogger('finishedDcgchatCron: empty jobId', 'error')
|
|
146
|
+
return
|
|
147
|
+
}
|
|
148
|
+
const jobPath = getCronJobsPath()
|
|
149
|
+
const sessionKey = readCronJobSessionKey(jobPath, id)
|
|
150
|
+
if (!sessionKey) {
|
|
151
|
+
dcgLogger(`finishedDcgchatCron: no sessionKey for job id=${id}`, 'error')
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
const outboundCtx = getEffectiveMsgParams(sessionKey)
|
|
155
|
+
const messageId = getCronMessageId(sessionKey)
|
|
156
|
+
if (outboundCtx?.sessionId) {
|
|
157
|
+
const newCtx = messageId ? { ...outboundCtx, messageId } : outboundCtx
|
|
158
|
+
sendFinal(newCtx)
|
|
159
|
+
} else {
|
|
160
|
+
const sessionInfo = sessionKey.split(':')
|
|
161
|
+
const sessionId = sessionInfo.at(-1) ?? ''
|
|
162
|
+
const agentId = sessionInfo.at(-2) ?? ''
|
|
163
|
+
const merged = mergeDefaultParams({
|
|
164
|
+
agentId: agentId,
|
|
165
|
+
sessionId: `${sessionId}`,
|
|
166
|
+
messageId: messageId,
|
|
167
|
+
is_finish: -1,
|
|
168
|
+
real_mobook: !sessionId ? 1 : ''
|
|
169
|
+
})
|
|
170
|
+
sendFinal(merged, 'cron send')
|
|
171
|
+
}
|
|
172
|
+
removeCronMessageId(sessionKey)
|
|
173
|
+
dcgLogger(`finishedDcgchatCron: job=${id} sessionKey=${sessionKey}`)
|
|
174
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { channelInfo, ENV } from './utils/constant.js'
|
|
2
|
+
/**
|
|
3
|
+
* cron-delivery-guard — 定时任务投递守护插件
|
|
4
|
+
*
|
|
5
|
+
* 核心机制:通过 before_tool_call 钩子拦截 cron 工具调用,
|
|
6
|
+
* 当 delivery.mode 为 "announce" 且未指定 channel 时,
|
|
7
|
+
* 自动注入 bestEffort: true,使投递失败时静默降级,
|
|
8
|
+
* 不影响 cron 执行结果的保存。
|
|
9
|
+
*
|
|
10
|
+
* 背景:
|
|
11
|
+
* - 定时任务的 delivery 设为 announce 模式,如果没有指定 channel,
|
|
12
|
+
* 投递可能因找不到有效渠道而失败
|
|
13
|
+
* - bestEffort: true 让框架在投递失败时不报错,避免丢失执行结果
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { dcgLogger } from './utils/log.js'
|
|
17
|
+
|
|
18
|
+
const LOG_TAG = 'cron-delivery-guard'
|
|
19
|
+
|
|
20
|
+
// ---- 类型定义 ----
|
|
21
|
+
|
|
22
|
+
interface ToolCallEvent {
|
|
23
|
+
toolName: string
|
|
24
|
+
toolCallId: string
|
|
25
|
+
params: Record<string, unknown>
|
|
26
|
+
result?: { content: string }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface HookContext {
|
|
30
|
+
agentId: string
|
|
31
|
+
sessionKey: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface BeforeToolCallResult {
|
|
35
|
+
block?: boolean
|
|
36
|
+
blockReason?: string
|
|
37
|
+
params?: Record<string, unknown>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ---- delivery 类型 ----
|
|
41
|
+
|
|
42
|
+
interface CronDelivery {
|
|
43
|
+
mode?: string
|
|
44
|
+
channel?: string
|
|
45
|
+
to?: string
|
|
46
|
+
bestEffort?: boolean
|
|
47
|
+
[key: string]: unknown
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* 解析 OpenClaw mobook direct 会话 key。
|
|
51
|
+
* 形如 `agent:main:mobook:direct:14:5466`(大小写不敏感,与路由 toLowerCase 一致)
|
|
52
|
+
* - 倒数第二段:account / peer(delivery.accountId)
|
|
53
|
+
* - 最后一段:会话 id(delivery.to)
|
|
54
|
+
*/
|
|
55
|
+
export function formatterSessionKey(sessionKey: string): { agentId: string; sessionId: string } {
|
|
56
|
+
const parts = sessionKey.split(':').filter((s) => s.length > 0)
|
|
57
|
+
const norm = parts.map((s) => s.toLowerCase())
|
|
58
|
+
if (parts.length >= 6 && norm[0] === 'agent' && norm[2] === 'mobook' && norm[3] === 'direct') {
|
|
59
|
+
return {
|
|
60
|
+
agentId: parts[4] ?? '',
|
|
61
|
+
sessionId: parts[5] ?? ''
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (parts.length >= 2) {
|
|
65
|
+
return {
|
|
66
|
+
agentId: parts[parts.length - 2] ?? '',
|
|
67
|
+
sessionId: parts[parts.length - 1] ?? ''
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return { agentId: '', sessionId: '' }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ---- 辅助函数 ----
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 判断是否为 cron 工具调用
|
|
77
|
+
*/
|
|
78
|
+
function isCronTool(toolName: string): boolean {
|
|
79
|
+
return toolName === 'cron'
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 从 cron 参数中提取 delivery 配置
|
|
84
|
+
* cron 工具的参数结构可能是:
|
|
85
|
+
* - params.delivery (顶层)
|
|
86
|
+
* - params.job.delivery (嵌套在 job 中)
|
|
87
|
+
*/
|
|
88
|
+
function extractDelivery(params: Record<string, unknown>): CronDelivery | null {
|
|
89
|
+
// 尝试顶层 delivery
|
|
90
|
+
if (params.delivery && typeof params.delivery === 'object') {
|
|
91
|
+
return params.delivery as CronDelivery
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 尝试 job.delivery
|
|
95
|
+
const job = params.job as Record<string, unknown> | undefined
|
|
96
|
+
if (job?.delivery && typeof job.delivery === 'object') {
|
|
97
|
+
return job.delivery as CronDelivery
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 尝试 payload 中的 deliver 相关字段 (兼容 qqbot-cron 风格)
|
|
101
|
+
const payload = job?.payload as Record<string, unknown> | undefined
|
|
102
|
+
if (payload?.deliver === true && payload.channel === undefined) {
|
|
103
|
+
// payload 风格: { deliver: true, channel?: string }
|
|
104
|
+
// 这种情况不是 delivery 对象,跳过
|
|
105
|
+
return null
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return null
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 判断 delivery 是否需要注入 bestEffort
|
|
113
|
+
* 条件: mode 为 "announce" 且没有 channel
|
|
114
|
+
*/
|
|
115
|
+
function needsBestEffort(delivery: CronDelivery): boolean {
|
|
116
|
+
return delivery.mode === 'announce' && !delivery.channel && !delivery.bestEffort
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 深拷贝 params 并注入 bestEffort: true
|
|
121
|
+
*/
|
|
122
|
+
function injectBestEffort(params: Record<string, unknown>, sk: string): Record<string, unknown> {
|
|
123
|
+
const newParams = JSON.parse(JSON.stringify(params)) as Record<string, unknown>
|
|
124
|
+
const { agentId, sessionId } = formatterSessionKey(sk)
|
|
125
|
+
// 顶层 delivery
|
|
126
|
+
if (newParams.delivery && typeof newParams.delivery === 'object') {
|
|
127
|
+
;(newParams.delivery as CronDelivery).bestEffort = true
|
|
128
|
+
;(newParams.delivery as CronDelivery).to = sessionId
|
|
129
|
+
;(newParams.delivery as CronDelivery).accountId = agentId
|
|
130
|
+
;(newParams.delivery as CronDelivery).channel = "dcgchat"
|
|
131
|
+
newParams.sessionKey = sk
|
|
132
|
+
return newParams
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// job.delivery
|
|
136
|
+
const job = newParams.job as Record<string, unknown> | undefined
|
|
137
|
+
if (job?.delivery && typeof job.delivery === 'object') {
|
|
138
|
+
;(job.delivery as CronDelivery).bestEffort = true
|
|
139
|
+
;(newParams.delivery as CronDelivery).to = sessionId
|
|
140
|
+
;(newParams.delivery as CronDelivery).accountId = agentId
|
|
141
|
+
;(newParams.delivery as CronDelivery).channel = "dcgchat"
|
|
142
|
+
newParams.sessionKey = sk
|
|
143
|
+
return newParams
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return newParams
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function cronToolCall(event: { toolName: any; params: any; toolCallId: any }, sk: string) {
|
|
150
|
+
const { toolName, params, toolCallId } = event
|
|
151
|
+
|
|
152
|
+
// 仅处理 cron 工具
|
|
153
|
+
if (isCronTool(toolName)) {
|
|
154
|
+
const delivery = extractDelivery(params)
|
|
155
|
+
if (!delivery) {
|
|
156
|
+
dcgLogger(`[${LOG_TAG}] cron call (${toolCallId}) has no delivery config, skip.`)
|
|
157
|
+
return params
|
|
158
|
+
}
|
|
159
|
+
if (!needsBestEffort(delivery)) {
|
|
160
|
+
dcgLogger(
|
|
161
|
+
`[${LOG_TAG}] cron call (${toolCallId}) delivery does not need bestEffort ` +
|
|
162
|
+
`(mode=${String(delivery.mode)}, channel=${String(delivery.channel)}, bestEffort=${String(delivery.bestEffort)}), skip.`
|
|
163
|
+
)
|
|
164
|
+
return params
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ★ 核心:注入 bestEffort: true
|
|
168
|
+
const newParams = injectBestEffort(params, sk)
|
|
169
|
+
dcgLogger(
|
|
170
|
+
`[${LOG_TAG}] cron call (${toolCallId}) injected bestEffort=true ` +
|
|
171
|
+
`(mode=announce, no channel). delivery=${JSON.stringify(newParams.delivery || (newParams.job as Record<string, unknown>)?.delivery)}`
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
return { params: newParams }
|
|
175
|
+
} else if (toolName === 'exec') {
|
|
176
|
+
if (params.command.indexOf('cron create') > -1 || params.command.indexOf('cron add') > -1) {
|
|
177
|
+
const newParams = JSON.parse(JSON.stringify(params)) as Record<string, unknown>
|
|
178
|
+
newParams.command =
|
|
179
|
+
params.command.replace('--json', '') + ` --session-key ${sk} --channel ${"dcgchat"} --to ${sk} --json`
|
|
180
|
+
return { params: newParams }
|
|
181
|
+
} else {
|
|
182
|
+
return params
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return params
|
|
187
|
+
}
|