@dcrays/dcgchat-test 0.2.26 → 0.2.28
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 +19 -17
- package/openclaw.plugin.json +4 -2
- package/package.json +5 -7
- package/src/bot.ts +214 -592
- package/src/channel.ts +119 -184
- package/src/monitor.ts +129 -178
- package/src/{api.ts → request/api.ts} +34 -35
- package/src/request/oss.ts +58 -0
- package/src/request/request.ts +198 -0
- package/src/{userInfo.ts → request/userInfo.ts} +36 -34
- package/src/skill.ts +119 -197
- package/src/tool.ts +109 -113
- package/src/transport.ts +108 -0
- package/src/types.ts +75 -64
- package/src/utils/constant.ts +4 -0
- package/src/utils/global.ts +112 -0
- package/src/utils/log.ts +15 -0
- package/src/utils/searchFile.ts +212 -0
- package/README.md +0 -83
- package/src/connection.ts +0 -11
- package/src/log.ts +0 -46
- package/src/oss.ts +0 -72
- package/src/request.ts +0 -201
- package/src/runtime.ts +0 -40
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 从文本中提取 /mobook 目录下的文件
|
|
3
|
+
* @param {string} text
|
|
4
|
+
* @returns {string[]}
|
|
5
|
+
*/
|
|
6
|
+
const EXT_LIST = [
|
|
7
|
+
// 文档类
|
|
8
|
+
'doc',
|
|
9
|
+
'docx',
|
|
10
|
+
'xls',
|
|
11
|
+
'xlsx',
|
|
12
|
+
'ppt',
|
|
13
|
+
'pptx',
|
|
14
|
+
'pdf',
|
|
15
|
+
'txt',
|
|
16
|
+
'rtf',
|
|
17
|
+
'odt',
|
|
18
|
+
|
|
19
|
+
// 数据/开发
|
|
20
|
+
'json',
|
|
21
|
+
'xml',
|
|
22
|
+
'csv',
|
|
23
|
+
'yaml',
|
|
24
|
+
'yml',
|
|
25
|
+
|
|
26
|
+
// 前端/文本
|
|
27
|
+
'html',
|
|
28
|
+
'htm',
|
|
29
|
+
'md',
|
|
30
|
+
'markdown',
|
|
31
|
+
'css',
|
|
32
|
+
'js',
|
|
33
|
+
'ts',
|
|
34
|
+
|
|
35
|
+
// 图片
|
|
36
|
+
'png',
|
|
37
|
+
'jpg',
|
|
38
|
+
'jpeg',
|
|
39
|
+
'gif',
|
|
40
|
+
'bmp',
|
|
41
|
+
'webp',
|
|
42
|
+
'svg',
|
|
43
|
+
'ico',
|
|
44
|
+
'tiff',
|
|
45
|
+
|
|
46
|
+
// 音频
|
|
47
|
+
'mp3',
|
|
48
|
+
'wav',
|
|
49
|
+
'ogg',
|
|
50
|
+
'aac',
|
|
51
|
+
'flac',
|
|
52
|
+
'm4a',
|
|
53
|
+
|
|
54
|
+
// 视频
|
|
55
|
+
'mp4',
|
|
56
|
+
'avi',
|
|
57
|
+
'mov',
|
|
58
|
+
'wmv',
|
|
59
|
+
'flv',
|
|
60
|
+
'mkv',
|
|
61
|
+
'webm',
|
|
62
|
+
|
|
63
|
+
// 压缩包
|
|
64
|
+
'zip',
|
|
65
|
+
'rar',
|
|
66
|
+
'7z',
|
|
67
|
+
'tar',
|
|
68
|
+
'gz',
|
|
69
|
+
'bz2',
|
|
70
|
+
'xz',
|
|
71
|
+
|
|
72
|
+
// 可执行/程序
|
|
73
|
+
'exe',
|
|
74
|
+
'dmg',
|
|
75
|
+
'pkg',
|
|
76
|
+
'apk',
|
|
77
|
+
'ipa',
|
|
78
|
+
|
|
79
|
+
// 其他常见
|
|
80
|
+
'log',
|
|
81
|
+
'dat',
|
|
82
|
+
'bin'
|
|
83
|
+
]
|
|
84
|
+
/**
|
|
85
|
+
* 扩展名按长度降序,用于正则交替,避免 xls 抢先匹配 xlsx、htm 抢先匹配 html 等
|
|
86
|
+
*/
|
|
87
|
+
const EXT_SORTED_FOR_REGEX = [...EXT_LIST].sort((a, b) => b.length - a.length)
|
|
88
|
+
|
|
89
|
+
/** 去除控制符、零宽字符等常见脏值 */
|
|
90
|
+
function stripMobookNoise(s: string) {
|
|
91
|
+
return s.replace(/[\u0000-\u001F\u007F\u200B-\u200D\u200E\u200F\uFEFF]/g, '')
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 从文本中扫描 `/mobook/` 片段,按最长后缀匹配合法扩展名(兜底,不依赖 FILE_NAME 字符集)
|
|
96
|
+
*/
|
|
97
|
+
function collectMobookPathsByScan(text: string, result: Set<string>): void {
|
|
98
|
+
const lower = text.toLowerCase()
|
|
99
|
+
const needle = '/mobook/'
|
|
100
|
+
let from = 0
|
|
101
|
+
while (from < text.length) {
|
|
102
|
+
const i = lower.indexOf(needle, from)
|
|
103
|
+
if (i < 0) break
|
|
104
|
+
const start = i + needle.length
|
|
105
|
+
const tail = text.slice(start)
|
|
106
|
+
const seg = tail.match(/^([^\s\]\)'"}\u3002,,]+)/)
|
|
107
|
+
if (!seg) {
|
|
108
|
+
from = start + 1
|
|
109
|
+
continue
|
|
110
|
+
}
|
|
111
|
+
let raw = stripMobookNoise(seg[1]).trim()
|
|
112
|
+
if (!raw || raw.includes('\uFFFD')) {
|
|
113
|
+
from = start + 1
|
|
114
|
+
continue
|
|
115
|
+
}
|
|
116
|
+
const low = raw.toLowerCase()
|
|
117
|
+
let matchedExt: string | undefined
|
|
118
|
+
for (const ext of EXT_SORTED_FOR_REGEX) {
|
|
119
|
+
if (low.endsWith(`.${ext}`)) {
|
|
120
|
+
matchedExt = ext
|
|
121
|
+
break
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (!matchedExt) {
|
|
125
|
+
from = start + 1
|
|
126
|
+
continue
|
|
127
|
+
}
|
|
128
|
+
const base = raw.slice(0, -(matchedExt.length + 1))
|
|
129
|
+
const fileName = `${base}.${matchedExt}`
|
|
130
|
+
if (isValidFileName(fileName)) {
|
|
131
|
+
result.add(normalizePath(`/mobook/${fileName}`))
|
|
132
|
+
}
|
|
133
|
+
from = start + 1
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function extractMobookFiles(text = '') {
|
|
138
|
+
if (typeof text !== 'string' || !text.trim()) return []
|
|
139
|
+
const result = new Set<string>()
|
|
140
|
+
// ✅ 扩展名(必须长扩展名优先,见 EXT_SORTED_FOR_REGEX)
|
|
141
|
+
const EXT = `(${EXT_SORTED_FOR_REGEX.join('|')})`
|
|
142
|
+
// ✅ 文件名字符(增强:支持中文、符号)
|
|
143
|
+
const FILE_NAME = `[\\w\\u4e00-\\u9fa5::《》()()\\-\\s]+?`
|
|
144
|
+
try {
|
|
145
|
+
// 1️⃣ `xxx.xxx`
|
|
146
|
+
const backtickReg = new RegExp(`\`([^\\\`]+?\\.${EXT})\``, 'gi')
|
|
147
|
+
;(text.match(backtickReg) || []).forEach((item) => {
|
|
148
|
+
const name = item.replace(/`/g, '').trim()
|
|
149
|
+
if (isValidFileName(name)) {
|
|
150
|
+
result.add(`/mobook/${name}`)
|
|
151
|
+
}
|
|
152
|
+
})
|
|
153
|
+
// 2️⃣ /mobook/xxx.xxx
|
|
154
|
+
const fullPathReg = new RegExp(`/mobook/${FILE_NAME}\\.${EXT}`, 'gi')
|
|
155
|
+
;(text.match(fullPathReg) || []).forEach((p) => {
|
|
156
|
+
result.add(normalizePath(p))
|
|
157
|
+
})
|
|
158
|
+
// 3️⃣ mobook下的 xxx.xxx
|
|
159
|
+
const inlineReg = new RegExp(`mobook下的\\s*(${FILE_NAME}\\.${EXT})`, 'gi')
|
|
160
|
+
;(text.match(inlineReg) || []).forEach((item) => {
|
|
161
|
+
const match = item.match(new RegExp(`${FILE_NAME}\\.${EXT}`, 'i'))
|
|
162
|
+
if (match && isValidFileName(match[0])) {
|
|
163
|
+
result.add(`/mobook/${match[0].trim()}`)
|
|
164
|
+
}
|
|
165
|
+
})
|
|
166
|
+
// 🆕 4️⃣ **xxx.xxx**
|
|
167
|
+
const boldReg = new RegExp(`\\*\\*(${FILE_NAME}\\.${EXT})\\*\\*`, 'gi')
|
|
168
|
+
;(text.match(boldReg) || []).forEach((item) => {
|
|
169
|
+
const name = item.replace(/\*\*/g, '').trim()
|
|
170
|
+
if (isValidFileName(name)) {
|
|
171
|
+
result.add(`/mobook/${name}`)
|
|
172
|
+
}
|
|
173
|
+
})
|
|
174
|
+
// 🆕 5️⃣ xxx.xxx (123字节)
|
|
175
|
+
const looseReg = new RegExp(`(${FILE_NAME}\\.${EXT})\\s*\\(`, 'gi')
|
|
176
|
+
;(text.match(looseReg) || []).forEach((item) => {
|
|
177
|
+
const name = item.replace(/\s*\(.+$/, '').trim()
|
|
178
|
+
if (isValidFileName(name)) {
|
|
179
|
+
result.add(`/mobook/${name}`)
|
|
180
|
+
}
|
|
181
|
+
})
|
|
182
|
+
// 6️⃣ 兜底:绝对路径等 `.../mobook/<文件名>.<扩展名>` + 最长后缀匹配 + 去脏字符
|
|
183
|
+
collectMobookPathsByScan(text, result)
|
|
184
|
+
} catch (e) {
|
|
185
|
+
console.warn('extractMobookFiles error:', e)
|
|
186
|
+
}
|
|
187
|
+
return [...result]
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* 校验文件名是否合法(避免脏数据)
|
|
192
|
+
*/
|
|
193
|
+
function isValidFileName(name: string) {
|
|
194
|
+
if (!name) return false
|
|
195
|
+
const cleaned = stripMobookNoise(name).trim()
|
|
196
|
+
if (!cleaned) return false
|
|
197
|
+
if (cleaned.includes('\uFFFD')) return false
|
|
198
|
+
// 过滤异常字符
|
|
199
|
+
if (/[\/\\<>:"|?*]/.test(cleaned)) return false
|
|
200
|
+
// 长度限制(防止异常长字符串)
|
|
201
|
+
if (cleaned.length > 200) return false
|
|
202
|
+
return true
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* 规范路径(去重用)
|
|
207
|
+
*/
|
|
208
|
+
function normalizePath(path: string) {
|
|
209
|
+
return path
|
|
210
|
+
.replace(/\/+/g, '/') // 多斜杠 → 单斜杠
|
|
211
|
+
.replace(/\/$/, '') // 去掉结尾 /
|
|
212
|
+
}
|
package/README.md
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
# OpenClaw 书灵墨宝 插件
|
|
2
|
-
|
|
3
|
-
连接 OpenClaw 与 书灵墨宝 产品的通道插件。
|
|
4
|
-
|
|
5
|
-
## 架构
|
|
6
|
-
|
|
7
|
-
```
|
|
8
|
-
┌──────────┐ WebSocket ┌──────────────┐ WebSocket ┌─────────────────────┐
|
|
9
|
-
│ Web 前端 │ ←───────────────→ │ 公司后端服务 │ ←───────────────→ │ OpenClaw(工作电脑) │
|
|
10
|
-
└──────────┘ └──────────────┘ (OpenClaw 主动连) └─────────────────────┘
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
- OpenClaw 插件**主动连接**后端的 WebSocket 服务(不需要公网 IP)
|
|
14
|
-
- 后端收到用户消息后转发给 OpenClaw,OpenClaw 回复后发回后端
|
|
15
|
-
|
|
16
|
-
## 快速开始
|
|
17
|
-
|
|
18
|
-
### 1. 安装插件
|
|
19
|
-
|
|
20
|
-
```bash
|
|
21
|
-
pnpm openclaw plugins install -l /path/to/openclaw-dcgchat
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
### 2. 配置
|
|
25
|
-
|
|
26
|
-
```bash
|
|
27
|
-
openclaw config set channels.dcgchat.enabled true
|
|
28
|
-
openclaw config set channels.dcgchat.wsUrl "ws://your-backend:8080/openclaw/ws"
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
### 3. 启动
|
|
32
|
-
|
|
33
|
-
```bash
|
|
34
|
-
pnpm openclaw gateway
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
## 消息协议(MVP)
|
|
38
|
-
|
|
39
|
-
### 下行:后端 → OpenClaw(用户消息)
|
|
40
|
-
|
|
41
|
-
```json
|
|
42
|
-
{ "type": "message", "userId": "user_001", "text": "你好" }
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
### 上行:OpenClaw → 后端(Agent 回复)
|
|
46
|
-
|
|
47
|
-
```json
|
|
48
|
-
{ "type": "reply", "userId": "user_001", "text": "你好!有什么可以帮你的?" }
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
## 配置项
|
|
52
|
-
|
|
53
|
-
| 配置键 | 类型 | 说明 |
|
|
54
|
-
|--------|------|------|
|
|
55
|
-
| `channels.dcgchat.enabled` | boolean | 是否启用 |
|
|
56
|
-
| `channels.dcgchat.wsUrl` | string | 后端 WebSocket 地址 |
|
|
57
|
-
|
|
58
|
-
## 开发
|
|
59
|
-
|
|
60
|
-
```bash
|
|
61
|
-
# 安装依赖
|
|
62
|
-
pnpm install
|
|
63
|
-
|
|
64
|
-
# 类型检查
|
|
65
|
-
pnpm typecheck
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
## 文件结构
|
|
69
|
-
|
|
70
|
-
- `index.ts` - 插件入口
|
|
71
|
-
- `src/channel.ts` - ChannelPlugin 定义
|
|
72
|
-
- `src/runtime.ts` - 插件 runtime
|
|
73
|
-
- `src/types.ts` - 类型定义
|
|
74
|
-
- `src/monitor.ts` - WebSocket 连接与断线重连
|
|
75
|
-
- `src/bot.ts` - 消息处理与 Agent 调用
|
|
76
|
-
|
|
77
|
-
## 后续迭代
|
|
78
|
-
|
|
79
|
-
- [ ] Token 认证
|
|
80
|
-
- [ ] 流式输出
|
|
81
|
-
- [ ] Typing 指示
|
|
82
|
-
- [ ] messageId 去重
|
|
83
|
-
- [ ] 错误消息类型
|
package/src/connection.ts
DELETED
package/src/log.ts
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import { fileURLToPath } from "url";
|
|
4
|
-
|
|
5
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
-
const logsDir = path.resolve(__dirname, "../logs");
|
|
7
|
-
|
|
8
|
-
function getLogFilePath(): string {
|
|
9
|
-
const date = new Date();
|
|
10
|
-
const yyyy = date.getFullYear();
|
|
11
|
-
const mm = String(date.getMonth() + 1).padStart(2, "0");
|
|
12
|
-
const dd = String(date.getDate()).padStart(2, "0");
|
|
13
|
-
return path.join(logsDir, `${yyyy}-${mm}-${dd}.log`);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function formatLine(level: string, message: string, extra?: unknown): string {
|
|
17
|
-
const now = new Date().toISOString();
|
|
18
|
-
const suffix = extra !== undefined ? " " + JSON.stringify(extra) : "";
|
|
19
|
-
return `[${now}] [${level}] ${message}${suffix}\n`;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function writeLog(level: string, message: string, extra?: unknown): void {
|
|
23
|
-
try {
|
|
24
|
-
if (!fs.existsSync(logsDir)) {
|
|
25
|
-
fs.mkdirSync(logsDir, { recursive: true });
|
|
26
|
-
}
|
|
27
|
-
fs.appendFileSync(getLogFilePath(), formatLine(level, message, extra), "utf-8");
|
|
28
|
-
} catch {
|
|
29
|
-
// 写日志失败时静默处理,避免影响主流程
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export const logDcgchat = {
|
|
34
|
-
info(message: string, extra?: unknown): void {
|
|
35
|
-
writeLog("INFO", message, extra);
|
|
36
|
-
},
|
|
37
|
-
warn(message: string, extra?: unknown): void {
|
|
38
|
-
writeLog("WARN", message, extra);
|
|
39
|
-
},
|
|
40
|
-
error(message: string, extra?: unknown): void {
|
|
41
|
-
writeLog("ERROR", message, extra);
|
|
42
|
-
},
|
|
43
|
-
debug(message: string, extra?: unknown): void {
|
|
44
|
-
writeLog("DEBUG", message, extra);
|
|
45
|
-
},
|
|
46
|
-
};
|
package/src/oss.ts
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import { createReadStream } from "node:fs";
|
|
2
|
-
import OSS from "ali-oss";
|
|
3
|
-
import { getStsToken, getUserToken } from "./api";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
/** 将 File/路径/Buffer 转为 ali-oss put 所需的 Buffer 或 ReadableStream */
|
|
7
|
-
async function toUploadContent(
|
|
8
|
-
input: File | string | Buffer,
|
|
9
|
-
): Promise<{ content: Buffer | ReturnType<typeof createReadStream>; fileName: string }> {
|
|
10
|
-
if (Buffer.isBuffer(input)) {
|
|
11
|
-
return { content: input, fileName: "file" };
|
|
12
|
-
}
|
|
13
|
-
if (typeof input === "string") {
|
|
14
|
-
return {
|
|
15
|
-
content: createReadStream(input),
|
|
16
|
-
fileName: input.split("/").pop() ?? "file",
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
// File: ali-oss 需要 Buffer/Stream,用 arrayBuffer 转 Buffer
|
|
20
|
-
const buf = Buffer.from(await input.arrayBuffer());
|
|
21
|
-
return { content: buf, fileName: input.name };
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export const ossUpload = async (file: File | string | Buffer, botToken: string) => {
|
|
25
|
-
await getUserToken(botToken);
|
|
26
|
-
|
|
27
|
-
const { content, fileName } = await toUploadContent(file);
|
|
28
|
-
const data = await getStsToken(fileName, botToken);
|
|
29
|
-
|
|
30
|
-
const options: OSS.Options = {
|
|
31
|
-
// 从STS服务获取的临时访问密钥(AccessKey ID和AccessKey Secret)。
|
|
32
|
-
accessKeyId: data.tempAccessKeyId,
|
|
33
|
-
accessKeySecret: data.tempAccessKeySecret,
|
|
34
|
-
// 从STS服务获取的安全令牌(SecurityToken)。
|
|
35
|
-
stsToken: data.tempSecurityToken,
|
|
36
|
-
// 填写Bucket名称。
|
|
37
|
-
bucket: data.bucket,
|
|
38
|
-
endpoint: data.endPoint,
|
|
39
|
-
region: data.region,
|
|
40
|
-
secure: true,
|
|
41
|
-
// refreshSTSToken: async () => {
|
|
42
|
-
// const tokenResponse = await getStsToken(fileName);
|
|
43
|
-
// return {
|
|
44
|
-
// accessKeyId: tokenResponse.tempAccessKeyId,
|
|
45
|
-
// accessKeySecret: tokenResponse.tempAccessKeySecret,
|
|
46
|
-
// stsToken: tokenResponse.tempSecurityToken,
|
|
47
|
-
// }
|
|
48
|
-
// },
|
|
49
|
-
// // 5 seconds
|
|
50
|
-
// refreshSTSTokenInterval: 5 * 1000,
|
|
51
|
-
// // // 5 minutes
|
|
52
|
-
// // refreshSTSTokenInterval: 5 * 60 * 1000,
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
const client = new OSS(options);
|
|
56
|
-
|
|
57
|
-
const name = `${data.uploadDir}${data.ossFileKey}`;
|
|
58
|
-
|
|
59
|
-
try {
|
|
60
|
-
const objectResult = await client.put(name, content);
|
|
61
|
-
if (objectResult?.res?.status !== 200) {
|
|
62
|
-
throw new Error("OSS 上传失败");
|
|
63
|
-
}
|
|
64
|
-
console.log(11111, JSON.stringify(objectResult));
|
|
65
|
-
// const url = `${data.protocol || 'http'}://${data.bucket}.${data.endPoint}/${data.uploadDir}${data.ossFileKey}`
|
|
66
|
-
return objectResult.name || objectResult.url;
|
|
67
|
-
} catch (error) {
|
|
68
|
-
console.error("OSS 上传失败:", error);
|
|
69
|
-
throw error;
|
|
70
|
-
}
|
|
71
|
-
};
|
|
72
|
-
|
package/src/request.ts
DELETED
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
import axios from "axios";
|
|
2
|
-
import md5 from "md5";
|
|
3
|
-
import type { IResponse } from "./types.js";
|
|
4
|
-
import { getUserTokenCache } from "./userInfo.js";
|
|
5
|
-
import { getMsgParams } from "./tool.js";
|
|
6
|
-
|
|
7
|
-
export const apiUrlMap = {
|
|
8
|
-
production: "https://api-gateway.shuwenda.com",
|
|
9
|
-
test: "https://api-gateway.shuwenda.icu",
|
|
10
|
-
develop: "https://shenyu-dev.shuwenda.icu",
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
export const appKey = {
|
|
14
|
-
production: "2A1C74D315CB4A01BF3DA8983695AFE2",
|
|
15
|
-
test: "7374A073CCBD4C8CA84FAD33896F0B69",
|
|
16
|
-
develop: "7374A073CCBD4C8CA84FAD33896F0B69",
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
export const signKey = {
|
|
20
|
-
production: "34E9023008EA445AAE6CC075CC954F46",
|
|
21
|
-
test: "FE93D3322CB94E978CE95BD4AA2A37D7",
|
|
22
|
-
develop: "FE93D3322CB94E978CE95BD4AA2A37D7",
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
const env = "production";
|
|
26
|
-
export const version = "1.0.0";
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* 根据 axios 请求配置生成等价 curl,便于复制给后端排查
|
|
30
|
-
*/
|
|
31
|
-
function toCurl(config: {
|
|
32
|
-
baseURL?: string;
|
|
33
|
-
url?: string;
|
|
34
|
-
method?: string;
|
|
35
|
-
headers?: Record<string, string | number | undefined>;
|
|
36
|
-
data?: unknown;
|
|
37
|
-
}): string {
|
|
38
|
-
const base = config.baseURL ?? "";
|
|
39
|
-
const path = config.url ?? "";
|
|
40
|
-
const url = path.startsWith("http")
|
|
41
|
-
? path
|
|
42
|
-
: `${base.replace(/\/$/, "")}/${path.replace(/^\//, "")}`;
|
|
43
|
-
const method = (config.method ?? "GET").toUpperCase();
|
|
44
|
-
const headers = config.headers ?? {};
|
|
45
|
-
const parts = ["curl", "-X", method, `'${url}'`];
|
|
46
|
-
for (const [k, v] of Object.entries(headers)) {
|
|
47
|
-
if (v !== undefined && v !== "") {
|
|
48
|
-
parts.push("-H", `'${k}: ${v}'`);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
if (method !== "GET" && config.data !== undefined) {
|
|
52
|
-
const body = typeof config.data === "string" ? config.data : JSON.stringify(config.data);
|
|
53
|
-
parts.push("-d", `'${body.replace(/'/g, "'\\''")}'`);
|
|
54
|
-
}
|
|
55
|
-
return parts.join(" ");
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* 生成签名
|
|
60
|
-
* @param {Object} body 请求体
|
|
61
|
-
* @param {number} timestamp 时间戳
|
|
62
|
-
* @param {string} path 请求地址
|
|
63
|
-
* @param {'production' | 'test' | 'develop'} env 请求环境
|
|
64
|
-
* @param {string} version 版本号
|
|
65
|
-
* @returns {string} 大写 MD5 签名
|
|
66
|
-
*/
|
|
67
|
-
export function getSignature(
|
|
68
|
-
body: Record<string, unknown>,
|
|
69
|
-
timestamp: number,
|
|
70
|
-
path: string,
|
|
71
|
-
env: "production" | "test" | "develop",
|
|
72
|
-
version: string = "1.0.0",
|
|
73
|
-
) {
|
|
74
|
-
// 1. 构造 map
|
|
75
|
-
const map = {
|
|
76
|
-
timestamp,
|
|
77
|
-
path,
|
|
78
|
-
version,
|
|
79
|
-
...body,
|
|
80
|
-
};
|
|
81
|
-
// 2. 按 key 进行自然排序
|
|
82
|
-
const sortedKeys = Object.keys(map).sort();
|
|
83
|
-
|
|
84
|
-
// 3. 拼接 key + value
|
|
85
|
-
const signStr =
|
|
86
|
-
sortedKeys
|
|
87
|
-
.map((key) => {
|
|
88
|
-
const val = map[key as keyof typeof map];
|
|
89
|
-
return val === undefined
|
|
90
|
-
? ""
|
|
91
|
-
: `${key}${typeof val === "object" ? JSON.stringify(val) : val}`;
|
|
92
|
-
})
|
|
93
|
-
.join("") + signKey[env];
|
|
94
|
-
|
|
95
|
-
// 4. MD5 加密并转大写
|
|
96
|
-
return md5(signStr).toUpperCase();
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function buildHeaders(data: Record<string, unknown>, url: string, userToken?: string) {
|
|
100
|
-
const timestamp = Date.now();
|
|
101
|
-
|
|
102
|
-
const headers: Record<string, string | number> = {
|
|
103
|
-
"Content-Type": "application/json",
|
|
104
|
-
appKey: appKey[env],
|
|
105
|
-
sign: getSignature(data, timestamp, url, env, version),
|
|
106
|
-
timestamp,
|
|
107
|
-
version,
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
// 如果提供了 userToken,添加到 headers
|
|
111
|
-
if (userToken) {
|
|
112
|
-
headers.authorization = userToken;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
return headers;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const axiosInstance = axios.create({
|
|
119
|
-
baseURL: apiUrlMap[env],
|
|
120
|
-
timeout: 10000,
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
// 请求拦截器:自动注入 userToken
|
|
124
|
-
axiosInstance.interceptors.request.use(
|
|
125
|
-
(config) => {
|
|
126
|
-
// 如果请求配置中已经有 authorization,优先使用
|
|
127
|
-
if (config.headers?.authorization) {
|
|
128
|
-
return config;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// 从请求上下文中获取 botToken(需要在调用时设置)
|
|
132
|
-
const botToken = (config as any).__botToken as string | undefined;
|
|
133
|
-
if (botToken) {
|
|
134
|
-
const cachedToken = getUserTokenCache(botToken);
|
|
135
|
-
if (cachedToken) {
|
|
136
|
-
config.headers = config.headers || {};
|
|
137
|
-
config.headers.authorization = cachedToken;
|
|
138
|
-
console.log(`[request] auto-injected userToken from cache for botToken=${botToken.slice(0, 10)}...`);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
return config;
|
|
143
|
-
},
|
|
144
|
-
(error) => {
|
|
145
|
-
return Promise.reject(error);
|
|
146
|
-
},
|
|
147
|
-
);
|
|
148
|
-
|
|
149
|
-
// 响应拦截器:打印 curl 便于调试
|
|
150
|
-
axiosInstance.interceptors.response.use(
|
|
151
|
-
(response) => {
|
|
152
|
-
const curl = toCurl(response.config);
|
|
153
|
-
console.log("[request] curl for backend:", curl);
|
|
154
|
-
return response.data;
|
|
155
|
-
},
|
|
156
|
-
(error) => {
|
|
157
|
-
const config = error.config ?? {};
|
|
158
|
-
const curl = toCurl(config);
|
|
159
|
-
console.log("[request] curl for backend (failed request):", curl);
|
|
160
|
-
return Promise.reject(error);
|
|
161
|
-
},
|
|
162
|
-
);
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* POST 请求(支持可选的 userToken 和 botToken)
|
|
168
|
-
* @param url 请求路径
|
|
169
|
-
* @param data 请求体
|
|
170
|
-
* @param options 可选配置
|
|
171
|
-
* @param options.userToken 直接提供的 userToken(优先级最高)
|
|
172
|
-
* @param options.botToken 用于从缓存获取 userToken 的 botToken
|
|
173
|
-
*/
|
|
174
|
-
export function post<T = Record<string, unknown>, R = unknown>(
|
|
175
|
-
url: string,
|
|
176
|
-
data: T,
|
|
177
|
-
options?: {
|
|
178
|
-
userToken?: string;
|
|
179
|
-
botToken?: string;
|
|
180
|
-
},
|
|
181
|
-
): Promise<IResponse<R>> {
|
|
182
|
-
const params = getMsgParams() || {}
|
|
183
|
-
const config: any = {
|
|
184
|
-
method: "POST",
|
|
185
|
-
url,
|
|
186
|
-
data: {
|
|
187
|
-
...data,
|
|
188
|
-
_appId: params.appId
|
|
189
|
-
},
|
|
190
|
-
headers: buildHeaders({
|
|
191
|
-
...data,
|
|
192
|
-
_appId: params.appId
|
|
193
|
-
} as Record<string, unknown>, url, options?.userToken),
|
|
194
|
-
};
|
|
195
|
-
|
|
196
|
-
// 将 botToken 附加到配置中,供请求拦截器使用
|
|
197
|
-
if (options?.botToken) {
|
|
198
|
-
config.__botToken = options.botToken;
|
|
199
|
-
}
|
|
200
|
-
return axiosInstance.request(config);
|
|
201
|
-
}
|
package/src/runtime.ts
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import type { PluginRuntime } from "openclaw/plugin-sdk";
|
|
2
|
-
import { logDcgchat } from "./log.js";
|
|
3
|
-
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const fs = require('fs');
|
|
6
|
-
const os = require("os")
|
|
7
|
-
|
|
8
|
-
function getWorkspacePath() {
|
|
9
|
-
const workspacePath = path.join(os.homedir(), '.openclaw', 'workspace');
|
|
10
|
-
if (fs.existsSync(workspacePath)) {
|
|
11
|
-
return workspacePath;
|
|
12
|
-
}
|
|
13
|
-
return null;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
let runtime: PluginRuntime | null = null;
|
|
17
|
-
let workspaceDir: string = getWorkspacePath();
|
|
18
|
-
|
|
19
|
-
export function setWorkspaceDir(dir?: string) {
|
|
20
|
-
if (dir) {
|
|
21
|
-
workspaceDir = dir;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
export function getWorkspaceDir(): string {
|
|
25
|
-
if (!workspaceDir) {
|
|
26
|
-
logDcgchat.error("Workspace directory not initialized");
|
|
27
|
-
}
|
|
28
|
-
return workspaceDir;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function setDcgchatRuntime(next: PluginRuntime) {
|
|
32
|
-
runtime = next;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function getDcgchatRuntime(): PluginRuntime {
|
|
36
|
-
if (!runtime) {
|
|
37
|
-
throw new Error("书灵墨宝 runtime not initialized");
|
|
38
|
-
}
|
|
39
|
-
return runtime;
|
|
40
|
-
}
|