@wu529778790/open-im 1.5.2-beta.2 → 1.5.2-beta.4
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 +26 -4
- package/dist/adapters/registry.d.ts +3 -0
- package/dist/adapters/registry.js +5 -2
- package/dist/codex/cli-runner.d.ts +3 -1
- package/dist/codex/cli-runner.js +12 -2
- package/dist/dingtalk/client.d.ts +1 -16
- package/dist/dingtalk/client.js +7 -321
- package/dist/dingtalk/event-handler.js +4 -12
- package/dist/dingtalk/message-sender.d.ts +1 -3
- package/dist/dingtalk/message-sender.js +21 -157
- package/dist/dingtalk/message-sender.test.js +2 -29
- package/dist/index.js +6 -2
- package/dist/session/session-manager.d.ts +3 -3
- package/dist/session/session-manager.js +38 -23
- package/dist/shared/ai-task.d.ts +3 -3
- package/dist/shared/ai-task.js +13 -2
- package/dist/shared/utils.d.ts +3 -2
- package/dist/shared/utils.js +18 -73
- package/dist/telegram/event-handler.js +50 -9
- package/dist/telegram/message-sender.d.ts +3 -0
- package/dist/telegram/message-sender.js +32 -0
- package/package.json +1 -1
- package/dist/session/session-manager.test.d.ts +0 -1
- package/dist/session/session-manager.test.js +0 -16
- package/dist/shared/utils.test.d.ts +0 -1
- package/dist/shared/utils.test.js +0 -43
package/README.md
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
- 多 AI 工具:支持 Claude、Codex、Cursor
|
|
9
9
|
- 流式输出:实时回传 AI 回复与工具执行进度
|
|
10
10
|
- 会话隔离:每个用户独立维护本地会话,`/new` 可重置
|
|
11
|
-
-
|
|
11
|
+
- 权限模式:支持 `ask`、`accept-edits`、`plan`、`yolo`
|
|
12
|
+
- 常用命令:支持 `/help`、`/mode`、`/new`、`/cd`、`/pwd`、`/status`
|
|
12
13
|
|
|
13
14
|
## 环境要求
|
|
14
15
|
|
|
@@ -184,16 +185,26 @@ Claude 默认使用 Agent SDK,不依赖本地 `claude` 可执行文件;通
|
|
|
184
185
|
说明:钉钉当前采用“Stream Mode 收消息 + OpenAPI 发送消息”的混合模式。
|
|
185
186
|
|
|
186
187
|
- 会话内普通文本回复默认走 `sessionWebhook`
|
|
187
|
-
-
|
|
188
|
+
- 如果配置了 `cardTemplateId` / `DINGTALK_CARD_TEMPLATE_ID`,AI 回复会升级为单条 `ai_card` 流式更新(`prepare / update / finish`)
|
|
188
189
|
- 启动/关闭通知会发给最近一次已互动的钉钉会话;如果服务冷启动后还没有任何钉钉会话互动过,则没有可用目标可发
|
|
189
190
|
|
|
190
|
-
钉钉 AI
|
|
191
|
+
钉钉 AI 卡片模板需至少兼容以下字段,建议模板按这些 key 取值:
|
|
192
|
+
|
|
193
|
+
- `title`
|
|
194
|
+
- `content`
|
|
195
|
+
- `note`
|
|
196
|
+
- `toolName`
|
|
197
|
+
- `status`
|
|
198
|
+
- `flowStatus`
|
|
199
|
+
- `displayText`
|
|
191
200
|
|
|
192
201
|
## IM 内命令
|
|
193
202
|
|
|
194
203
|
| 命令 | 说明 |
|
|
195
204
|
| ---- | ---- |
|
|
196
205
|
| `/help` | 显示帮助 |
|
|
206
|
+
| `/mode` | 飞书显示卡片,Telegram 显示按钮,其它平台(含钉钉)显示文本模式列表 |
|
|
207
|
+
| `/mode <模式>` | 直接切换:`ask` / `accept-edits` / `plan` / `yolo` |
|
|
197
208
|
| `/new` | 开始新会话 |
|
|
198
209
|
| `/status` | 显示 AI 工具、版本、会话目录、会话 ID |
|
|
199
210
|
| `/cd <路径>` | 切换会话目录 |
|
|
@@ -201,6 +212,17 @@ Claude 默认使用 Agent SDK,不依赖本地 `claude` 可执行文件;通
|
|
|
201
212
|
| `/allow` `/y` | 允许权限请求 |
|
|
202
213
|
| `/deny` `/n` | 拒绝权限请求 |
|
|
203
214
|
|
|
215
|
+
### 权限模式
|
|
216
|
+
|
|
217
|
+
与 Claude Code 官方命名保持一致,参考 [permissions](https://code.claude.com/docs/en/permissions):
|
|
218
|
+
|
|
219
|
+
| 模式 | Claude 名 | 说明 |
|
|
220
|
+
| ---- | --------- | ---- |
|
|
221
|
+
| `ask` | `default` | 首次使用工具时询问 |
|
|
222
|
+
| `accept-edits` | `acceptEdits` | 自动允许编辑 |
|
|
223
|
+
| `plan` | `plan` | 只读分析,不执行命令、不改文件 |
|
|
224
|
+
| `yolo` | `bypassPermissions` | 跳过所有权限确认 |
|
|
225
|
+
|
|
204
226
|
## 故障排除
|
|
205
227
|
|
|
206
228
|
**Telegram 无响应**:检查网络,必要时在 Telegram 平台配置中添加 `"proxy": "http://127.0.0.1:7890"` 或设置 `TELEGRAM_PROXY`。
|
|
@@ -211,7 +233,7 @@ Claude 默认使用 Agent SDK,不依赖本地 `claude` 可执行文件;通
|
|
|
211
233
|
|
|
212
234
|
**钉钉无法回复**:确认应用已启用机器人 Stream Mode,并检查 `DINGTALK_CLIENT_ID`、`DINGTALK_CLIENT_SECRET` 或 `platforms.dingtalk` 配置是否正确。
|
|
213
235
|
|
|
214
|
-
|
|
236
|
+
**钉钉没有流式更新**:如果未配置 `DINGTALK_CARD_TEMPLATE_ID`(或 `platforms.dingtalk.cardTemplateId`),会自动退回普通文本回复;配置 AI 卡片模板后才会启用单条流式更新。
|
|
215
237
|
|
|
216
238
|
**Cursor 报 `Authentication required`**:先执行 `agent login`,或在 `env` 中设置 `CURSOR_API_KEY`。
|
|
217
239
|
|
|
@@ -2,4 +2,7 @@ import type { Config } from '../config.js';
|
|
|
2
2
|
import type { ToolAdapter } from './tool-adapter.interface.js';
|
|
3
3
|
export declare function initAdapters(config: Config): void;
|
|
4
4
|
export declare function getAdapter(aiCommand: string): ToolAdapter | undefined;
|
|
5
|
+
/**
|
|
6
|
+
* Cleanup all adapter resources.
|
|
7
|
+
*/
|
|
5
8
|
export declare function cleanupAdapters(): void;
|
|
@@ -14,7 +14,7 @@ export function initAdapters(config) {
|
|
|
14
14
|
console.log('🚀 使用标准 Claude 适配器');
|
|
15
15
|
adapters.set('claude', new ClaudeAdapter(config.claudeCliPath, {
|
|
16
16
|
useProcessPool: true,
|
|
17
|
-
idleTimeoutMs: 2 * 60 * 1000,
|
|
17
|
+
idleTimeoutMs: 2 * 60 * 1000, // 2 minutes
|
|
18
18
|
}));
|
|
19
19
|
}
|
|
20
20
|
}
|
|
@@ -30,8 +30,11 @@ export function initAdapters(config) {
|
|
|
30
30
|
export function getAdapter(aiCommand) {
|
|
31
31
|
return adapters.get(aiCommand);
|
|
32
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Cleanup all adapter resources.
|
|
35
|
+
*/
|
|
33
36
|
export function cleanupAdapters() {
|
|
34
37
|
ClaudeAdapter.destroy();
|
|
35
|
-
ClaudeSDKAdapter.destroy();
|
|
38
|
+
ClaudeSDKAdapter.destroy(); // 清理 SDK 查询
|
|
36
39
|
adapters.clear();
|
|
37
40
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Codex CLI Runner - 解析
|
|
2
|
+
* Codex CLI Runner - 解析 codex exec --json 的 JSONL 输出
|
|
3
|
+
* 参考: https://developers.openai.com/codex/cli/reference/
|
|
4
|
+
* https://takopi.dev/reference/runners/codex/exec-json-cheatsheet/
|
|
3
5
|
*/
|
|
4
6
|
export interface CodexRunCallbacks {
|
|
5
7
|
onText: (accumulated: string) => void;
|
package/dist/codex/cli-runner.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Codex CLI Runner - 解析
|
|
2
|
+
* Codex CLI Runner - 解析 codex exec --json 的 JSONL 输出
|
|
3
|
+
* 参考: https://developers.openai.com/codex/cli/reference/
|
|
4
|
+
* https://takopi.dev/reference/runners/codex/exec-json-cheatsheet/
|
|
3
5
|
*/
|
|
4
6
|
import { spawn } from 'node:child_process';
|
|
5
7
|
import { execFileSync } from 'node:child_process';
|
|
@@ -49,6 +51,7 @@ function buildCodexArgs(_prompt, sessionId, workDir, options) {
|
|
|
49
51
|
: ["exec", ...newSessionOptions, "-"];
|
|
50
52
|
}
|
|
51
53
|
function quoteForWindowsCmd(arg) {
|
|
54
|
+
// 普通 flag / sessionId / 无空格路径不需要加引号,否则引号可能被原样传给子进程。
|
|
52
55
|
if (/^[A-Za-z0-9_./:=+\\-]+$/.test(arg)) {
|
|
53
56
|
return arg;
|
|
54
57
|
}
|
|
@@ -59,6 +62,7 @@ function quoteForWindowsCmd(arg) {
|
|
|
59
62
|
return `"${escaped}"`;
|
|
60
63
|
}
|
|
61
64
|
function formatWindowsCommandName(command) {
|
|
65
|
+
// 裸命令名(如 codex)依赖 PATH 查找,不能再包双引号,否则 cmd 会按字面量查找。
|
|
62
66
|
if (/^[A-Za-z0-9_.-]+$/.test(command)) {
|
|
63
67
|
return command;
|
|
64
68
|
}
|
|
@@ -114,6 +118,7 @@ function resolveWindowsCodexLaunch(cliPath, args) {
|
|
|
114
118
|
}
|
|
115
119
|
}
|
|
116
120
|
export function runCodex(cliPath, prompt, sessionId, workDir, callbacks, options) {
|
|
121
|
+
// codex exec --json 非交互模式
|
|
117
122
|
const args = buildCodexArgs(prompt, sessionId, workDir, options);
|
|
118
123
|
const env = {};
|
|
119
124
|
for (const [k, v] of Object.entries(process.env)) {
|
|
@@ -133,11 +138,13 @@ export function runCodex(cliPath, prompt, sessionId, workDir, callbacks, options
|
|
|
133
138
|
env.all_proxy = options.proxy;
|
|
134
139
|
}
|
|
135
140
|
if (process.platform === 'win32') {
|
|
141
|
+
// 强制子进程在 Windows 下使用 UTF-8,避免中文源码/命令输出乱码。
|
|
136
142
|
env.LANG = env.LANG || 'C.UTF-8';
|
|
137
143
|
env.LC_ALL = env.LC_ALL || 'C.UTF-8';
|
|
138
144
|
}
|
|
139
145
|
const argsForLog = args.join(' ');
|
|
140
146
|
log.info(`Spawning Codex CLI: path=${cliPath}, cwd=${workDir}, session=${sessionId ?? 'new'}, args=${argsForLog}`);
|
|
147
|
+
// Windows: .cmd/.bat 或简单命令名(如 codex)需通过 cmd.exe 执行,否则 spawn 报 ENOENT
|
|
141
148
|
const isWinCmd = process.platform === 'win32' &&
|
|
142
149
|
(/\.(cmd|bat)$/i.test(cliPath) || cliPath === 'codex');
|
|
143
150
|
const directWindowsLaunch = isWinCmd
|
|
@@ -164,11 +171,13 @@ export function runCodex(cliPath, prompt, sessionId, workDir, callbacks, options
|
|
|
164
171
|
env,
|
|
165
172
|
windowsHide: process.platform === 'win32',
|
|
166
173
|
});
|
|
174
|
+
// 通过 stdin 传 prompt,避免 Windows 下命令行参数引用导致中文/路径/空格被拆分。
|
|
167
175
|
child.stdin?.write(prompt);
|
|
168
176
|
child.stdin?.end();
|
|
169
177
|
let accumulated = '';
|
|
170
178
|
let accumulatedThinking = '';
|
|
171
179
|
let completed = false;
|
|
180
|
+
let threadId = '';
|
|
172
181
|
const toolStats = {};
|
|
173
182
|
const startTime = Date.now();
|
|
174
183
|
const MAX_TIMEOUT = 2_147_483_647;
|
|
@@ -217,7 +226,7 @@ export function runCodex(cliPath, prompt, sessionId, workDir, callbacks, options
|
|
|
217
226
|
const type = event.type;
|
|
218
227
|
log.debug(`[Codex event] type=${type}`);
|
|
219
228
|
if (type === 'thread.started') {
|
|
220
|
-
|
|
229
|
+
threadId = event.thread_id ?? '';
|
|
221
230
|
if (threadId)
|
|
222
231
|
callbacks.onSessionId?.(threadId);
|
|
223
232
|
return;
|
|
@@ -293,6 +302,7 @@ export function runCodex(cliPath, prompt, sessionId, workDir, callbacks, options
|
|
|
293
302
|
completed = true;
|
|
294
303
|
if (timeoutHandle)
|
|
295
304
|
clearTimeout(timeoutHandle);
|
|
305
|
+
const usage = event.usage;
|
|
296
306
|
const durationMs = Date.now() - startTime;
|
|
297
307
|
callbacks.onComplete({
|
|
298
308
|
success: true,
|
|
@@ -1,13 +1,6 @@
|
|
|
1
1
|
import { type DWClientDownStream } from 'dingtalk-stream';
|
|
2
2
|
import type { Config } from '../config.js';
|
|
3
3
|
import type { DingTalkActiveTarget } from '../shared/active-chats.js';
|
|
4
|
-
export interface DingTalkStreamingTarget {
|
|
5
|
-
chatId: string;
|
|
6
|
-
conversationType?: string;
|
|
7
|
-
senderStaffId?: string;
|
|
8
|
-
senderId?: string;
|
|
9
|
-
robotCode?: string;
|
|
10
|
-
}
|
|
11
4
|
export declare function registerSessionWebhook(chatId: string, sessionWebhook: string): void;
|
|
12
5
|
export declare function sendText(chatId: string, content: string): Promise<unknown>;
|
|
13
6
|
export declare function sendMarkdown(chatId: string, title: string, text: string): Promise<unknown>;
|
|
@@ -15,14 +8,6 @@ export declare function ackMessage(messageId: string, result?: unknown): void;
|
|
|
15
8
|
export declare function initDingTalk(cfg: Config, eventHandler: (data: DWClientDownStream) => Promise<void>): Promise<void>;
|
|
16
9
|
export declare function stopDingTalk(): void;
|
|
17
10
|
export declare function sendProactiveText(target: string | DingTalkActiveTarget, content: string): Promise<void>;
|
|
18
|
-
export declare function prepareStreamingCard(
|
|
11
|
+
export declare function prepareStreamingCard(chatId: string, templateId: string, cardData: Record<string, unknown>): Promise<string>;
|
|
19
12
|
export declare function updateStreamingCard(conversationToken: string, templateId: string, cardData: Record<string, unknown>): Promise<void>;
|
|
20
13
|
export declare function finishStreamingCard(conversationToken: string): Promise<void>;
|
|
21
|
-
/** 创建并投放卡片(卡片平台 API,支持普通群流式更新) */
|
|
22
|
-
export declare function createAndDeliverCard(target: DingTalkStreamingTarget, templateId: string, outTrackId: string, cardData: Record<string, unknown>): Promise<void>;
|
|
23
|
-
/** 更新卡片实例(用于流式更新) */
|
|
24
|
-
export declare function updateCardInstance(outTrackId: string, cardData: Record<string, unknown>): Promise<void>;
|
|
25
|
-
/** 互动卡片普通版:发送(用于 prepare 失败时的 fallback 流式) */
|
|
26
|
-
export declare function sendRobotInteractiveCard(target: DingTalkStreamingTarget, cardBizId: string, cardData: Record<string, unknown>): Promise<void>;
|
|
27
|
-
/** 互动卡片普通版:更新(单条消息流式更新) */
|
|
28
|
-
export declare function updateRobotInteractiveCard(cardBizId: string, cardData: Record<string, unknown>): Promise<void>;
|
package/dist/dingtalk/client.js
CHANGED
|
@@ -2,12 +2,10 @@ import { DWClient, TOPIC_ROBOT } from 'dingtalk-stream';
|
|
|
2
2
|
import { createLogger } from '../logger.js';
|
|
3
3
|
const log = createLogger('DingTalk');
|
|
4
4
|
const DINGTALK_OPENAPI_BASE = 'https://api.dingtalk.com';
|
|
5
|
-
const DINGTALK_OAPI_BASE = 'https://oapi.dingtalk.com';
|
|
6
5
|
const TEXT_MSG_KEY = 'sampleText';
|
|
7
6
|
let client = null;
|
|
8
7
|
let messageHandler = null;
|
|
9
8
|
const sessionWebhookByChat = new Map();
|
|
10
|
-
const unionIdByUserId = new Map();
|
|
11
9
|
function getClient() {
|
|
12
10
|
if (!client) {
|
|
13
11
|
throw new Error('DingTalk client not initialized');
|
|
@@ -117,72 +115,9 @@ async function callOpenApi(path, body) {
|
|
|
117
115
|
: text;
|
|
118
116
|
throw new Error(`DingTalk OpenAPI business error: ${String(errorCode)} ${errorMessage}`);
|
|
119
117
|
}
|
|
120
|
-
async function callOapi(path, body) {
|
|
121
|
-
const accessToken = await getClient().getAccessToken();
|
|
122
|
-
const res = await fetch(`${DINGTALK_OAPI_BASE}${path}?access_token=${encodeURIComponent(String(accessToken))}`, {
|
|
123
|
-
method: 'POST',
|
|
124
|
-
headers: {
|
|
125
|
-
'content-type': 'application/json',
|
|
126
|
-
},
|
|
127
|
-
body: JSON.stringify(body),
|
|
128
|
-
signal: AbortSignal.timeout(30000),
|
|
129
|
-
});
|
|
130
|
-
const text = await res.text();
|
|
131
|
-
if (!res.ok) {
|
|
132
|
-
throw new Error(`DingTalk OAPI failed: ${res.status} ${text}`);
|
|
133
|
-
}
|
|
134
|
-
let parsed;
|
|
135
|
-
try {
|
|
136
|
-
parsed = JSON.parse(text);
|
|
137
|
-
}
|
|
138
|
-
catch {
|
|
139
|
-
throw new Error(`DingTalk OAPI returned non-JSON response: ${text}`);
|
|
140
|
-
}
|
|
141
|
-
const errorCode = parsed.errcode;
|
|
142
|
-
if (errorCode === 0 || errorCode === '0' || errorCode === undefined) {
|
|
143
|
-
return parsed;
|
|
144
|
-
}
|
|
145
|
-
const errorMessage = typeof parsed.errmsg === 'string'
|
|
146
|
-
? parsed.errmsg
|
|
147
|
-
: typeof parsed.message === 'string'
|
|
148
|
-
? parsed.message
|
|
149
|
-
: text;
|
|
150
|
-
throw new Error(`DingTalk OAPI business error: ${String(errorCode)} ${errorMessage}`);
|
|
151
|
-
}
|
|
152
118
|
function normalizeConversationType(type) {
|
|
153
119
|
return type?.trim().toLowerCase();
|
|
154
120
|
}
|
|
155
|
-
function isSingleConversation(type) {
|
|
156
|
-
const normalizedType = normalizeConversationType(type);
|
|
157
|
-
return (normalizedType === '0' ||
|
|
158
|
-
normalizedType === 'single' ||
|
|
159
|
-
normalizedType === 'singlechat' ||
|
|
160
|
-
normalizedType === 'oto');
|
|
161
|
-
}
|
|
162
|
-
function isGroupConversation(type) {
|
|
163
|
-
const normalizedType = normalizeConversationType(type);
|
|
164
|
-
return (normalizedType === '1' ||
|
|
165
|
-
normalizedType === '2' ||
|
|
166
|
-
normalizedType === 'group' ||
|
|
167
|
-
normalizedType === 'groupchat');
|
|
168
|
-
}
|
|
169
|
-
async function resolveUnionIdByUserId(userId) {
|
|
170
|
-
if (!userId)
|
|
171
|
-
return undefined;
|
|
172
|
-
const cached = unionIdByUserId.get(userId);
|
|
173
|
-
if (cached)
|
|
174
|
-
return cached;
|
|
175
|
-
const result = await callOapi('/topapi/v2/user/get', {
|
|
176
|
-
userid: userId,
|
|
177
|
-
language: 'zh_CN',
|
|
178
|
-
});
|
|
179
|
-
const unionId = result.result?.unionid;
|
|
180
|
-
if (typeof unionId === 'string' && unionId.length > 0) {
|
|
181
|
-
unionIdByUserId.set(userId, unionId);
|
|
182
|
-
return unionId;
|
|
183
|
-
}
|
|
184
|
-
return undefined;
|
|
185
|
-
}
|
|
186
121
|
function buildProactiveAttempts(target, content) {
|
|
187
122
|
const robotCode = getRobotCode(target);
|
|
188
123
|
const payload = buildTextPayload(content);
|
|
@@ -279,7 +214,6 @@ export function stopDingTalk() {
|
|
|
279
214
|
}
|
|
280
215
|
finally {
|
|
281
216
|
sessionWebhookByChat.clear();
|
|
282
|
-
unionIdByUserId.clear();
|
|
283
217
|
client = null;
|
|
284
218
|
messageHandler = null;
|
|
285
219
|
log.info('DingTalk client stopped');
|
|
@@ -302,107 +236,19 @@ export async function sendProactiveText(target, content) {
|
|
|
302
236
|
}
|
|
303
237
|
catch (err) {
|
|
304
238
|
lastError = err;
|
|
305
|
-
|
|
306
|
-
if (msg.includes('robot') || msg.includes('resource.not.found')) {
|
|
307
|
-
log.debug(`DingTalk proactive ${attempt.label} send failed:`, err);
|
|
308
|
-
}
|
|
309
|
-
else {
|
|
310
|
-
log.warn(`DingTalk proactive ${attempt.label} send failed:`, err);
|
|
311
|
-
}
|
|
239
|
+
log.warn(`DingTalk proactive ${attempt.label} send failed:`, err);
|
|
312
240
|
}
|
|
313
241
|
}
|
|
314
242
|
throw lastError instanceof Error
|
|
315
243
|
? lastError
|
|
316
244
|
: new Error(`DingTalk proactive send failed for chat ${target.chatId}`);
|
|
317
245
|
}
|
|
318
|
-
export async function prepareStreamingCard(
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
if (isSingleConversation(normalizedTarget.conversationType)) {
|
|
325
|
-
let unionId;
|
|
326
|
-
try {
|
|
327
|
-
unionId = await resolveUnionIdByUserId(normalizedTarget.senderStaffId);
|
|
328
|
-
}
|
|
329
|
-
catch (err) {
|
|
330
|
-
log.debug('Failed to resolve DingTalk unionId from senderStaffId:', err);
|
|
331
|
-
}
|
|
332
|
-
if (unionId) {
|
|
333
|
-
attempts.push({
|
|
334
|
-
label: 'single-unionid',
|
|
335
|
-
body: { unionId, contentType, content },
|
|
336
|
-
});
|
|
337
|
-
}
|
|
338
|
-
if (normalizedTarget.chatId) {
|
|
339
|
-
attempts.push({
|
|
340
|
-
label: 'single-chatid',
|
|
341
|
-
body: { openConversationId: normalizedTarget.chatId, contentType, content },
|
|
342
|
-
});
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
else if (isGroupConversation(normalizedTarget.conversationType)) {
|
|
346
|
-
// 群聊时也优先尝试 unionId:部分场景下 conversationType 可能误报,或单聊被识别为群聊
|
|
347
|
-
let unionId;
|
|
348
|
-
try {
|
|
349
|
-
unionId = await resolveUnionIdByUserId(normalizedTarget.senderStaffId);
|
|
350
|
-
}
|
|
351
|
-
catch (err) {
|
|
352
|
-
log.debug('Failed to resolve DingTalk unionId for group (fallback):', err);
|
|
353
|
-
}
|
|
354
|
-
if (unionId) {
|
|
355
|
-
attempts.push({
|
|
356
|
-
label: 'group-unionid',
|
|
357
|
-
body: { unionId, contentType, content },
|
|
358
|
-
});
|
|
359
|
-
}
|
|
360
|
-
if (normalizedTarget.chatId) {
|
|
361
|
-
attempts.push({
|
|
362
|
-
label: 'group-chatid',
|
|
363
|
-
body: { openConversationId: normalizedTarget.chatId, contentType, content },
|
|
364
|
-
});
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
else {
|
|
368
|
-
let unionId;
|
|
369
|
-
try {
|
|
370
|
-
unionId = await resolveUnionIdByUserId(normalizedTarget.senderStaffId);
|
|
371
|
-
}
|
|
372
|
-
catch (err) {
|
|
373
|
-
log.debug('Failed to resolve DingTalk unionId for unknown conversation type:', err);
|
|
374
|
-
}
|
|
375
|
-
if (unionId) {
|
|
376
|
-
attempts.push({
|
|
377
|
-
label: 'unknown-unionid',
|
|
378
|
-
body: { unionId, contentType, content },
|
|
379
|
-
});
|
|
380
|
-
}
|
|
381
|
-
if (normalizedTarget.chatId) {
|
|
382
|
-
attempts.push({
|
|
383
|
-
label: 'unknown-chatid',
|
|
384
|
-
body: { openConversationId: normalizedTarget.chatId, contentType, content },
|
|
385
|
-
});
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
if (attempts.length === 0) {
|
|
389
|
-
throw new Error('DingTalk prepare target is incomplete');
|
|
390
|
-
}
|
|
391
|
-
let result;
|
|
392
|
-
let lastError;
|
|
393
|
-
for (const attempt of attempts) {
|
|
394
|
-
try {
|
|
395
|
-
result = await callOpenApi('/v1.0/aiInteraction/prepare', attempt.body);
|
|
396
|
-
break;
|
|
397
|
-
}
|
|
398
|
-
catch (err) {
|
|
399
|
-
lastError = err;
|
|
400
|
-
log.debug(`DingTalk prepare attempt failed (${attempt.label}):`, err);
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
if (!result) {
|
|
404
|
-
throw lastError instanceof Error ? lastError : new Error('DingTalk prepare failed');
|
|
405
|
-
}
|
|
246
|
+
export async function prepareStreamingCard(chatId, templateId, cardData) {
|
|
247
|
+
const result = await callOpenApi('/v1.0/aiInteraction/prepare', {
|
|
248
|
+
openConversationId: chatId,
|
|
249
|
+
contentType: 'ai_card',
|
|
250
|
+
content: buildAiCardContent(templateId, cardData),
|
|
251
|
+
});
|
|
406
252
|
const token = result.result?.conversationToken;
|
|
407
253
|
if (typeof token !== 'string' || token.length === 0) {
|
|
408
254
|
throw new Error(`DingTalk prepare did not return conversationToken: ${JSON.stringify(result)}`);
|
|
@@ -429,163 +275,3 @@ export async function finishStreamingCard(conversationToken) {
|
|
|
429
275
|
throw new Error(`DingTalk finish returned success=false: ${JSON.stringify(result)}`);
|
|
430
276
|
}
|
|
431
277
|
}
|
|
432
|
-
/** 创建并投放卡片(卡片平台 API,支持普通群流式更新) */
|
|
433
|
-
export async function createAndDeliverCard(target, templateId, outTrackId, cardData) {
|
|
434
|
-
const { chatId, robotCode, conversationType, senderStaffId } = target;
|
|
435
|
-
if (!robotCode) {
|
|
436
|
-
throw new Error('DingTalk robotCode required for createAndDeliver');
|
|
437
|
-
}
|
|
438
|
-
const isSingle = isSingleConversation(conversationType);
|
|
439
|
-
const cardParamMap = buildCardParamMap(cardData);
|
|
440
|
-
if (!cardParamMap.content && !cardParamMap.lastMessage)
|
|
441
|
-
cardParamMap.content = '...';
|
|
442
|
-
const lastMsg = String(cardData.lastMessage ?? cardData.displayText ?? cardData.content ?? cardData.title ?? 'AI').slice(0, 50);
|
|
443
|
-
const body = {
|
|
444
|
-
userId: senderStaffId ?? 'system',
|
|
445
|
-
cardTemplateId: templateId,
|
|
446
|
-
outTrackId,
|
|
447
|
-
cardData: { cardParamMap },
|
|
448
|
-
};
|
|
449
|
-
if (isSingle && senderStaffId) {
|
|
450
|
-
body.openSpaceId = `dtv1.card//im_robot.${senderStaffId}`;
|
|
451
|
-
body.imRobotOpenSpaceModel = {
|
|
452
|
-
lastMessageI18n: { zh_CN: lastMsg },
|
|
453
|
-
searchSupport: { searchIcon: '', searchTypeName: '消息', searchDesc: '' },
|
|
454
|
-
notification: { alertContent: lastMsg },
|
|
455
|
-
};
|
|
456
|
-
body.imRobotOpenDeliverModel = { spaceType: 'IM_ROBOT' };
|
|
457
|
-
}
|
|
458
|
-
else {
|
|
459
|
-
body.openSpaceId = `dtv1.card//im_group.${chatId}`;
|
|
460
|
-
body.imGroupOpenSpaceModel = {
|
|
461
|
-
lastMessageI18n: { zh_CN: lastMsg },
|
|
462
|
-
searchSupport: { searchIcon: '', searchTypeName: '消息', searchDesc: '' },
|
|
463
|
-
notification: { alertContent: lastMsg },
|
|
464
|
-
};
|
|
465
|
-
body.imGroupOpenDeliverModel = {
|
|
466
|
-
robotCode,
|
|
467
|
-
atUserIds: {},
|
|
468
|
-
recipients: [],
|
|
469
|
-
};
|
|
470
|
-
}
|
|
471
|
-
await callOpenApiWithMethod('POST', '/v1.0/card/instances/createAndDeliver', body);
|
|
472
|
-
}
|
|
473
|
-
/** 将 cardData 转为 cardParamMap(对象/数组需 JSON 序列化) */
|
|
474
|
-
function buildCardParamMap(cardData) {
|
|
475
|
-
const cardParamMap = {};
|
|
476
|
-
for (const [k, v] of Object.entries(cardData)) {
|
|
477
|
-
if (v === undefined || v === null)
|
|
478
|
-
continue;
|
|
479
|
-
if (typeof v === 'object') {
|
|
480
|
-
cardParamMap[k] = JSON.stringify(v);
|
|
481
|
-
}
|
|
482
|
-
else {
|
|
483
|
-
cardParamMap[k] = String(v);
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
return cardParamMap;
|
|
487
|
-
}
|
|
488
|
-
/** 更新卡片实例(用于流式更新) */
|
|
489
|
-
export async function updateCardInstance(outTrackId, cardData) {
|
|
490
|
-
const cardParamMap = buildCardParamMap(cardData);
|
|
491
|
-
await callOpenApiWithMethod('PUT', '/v1.0/card/instances', {
|
|
492
|
-
outTrackId,
|
|
493
|
-
cardData: { cardParamMap },
|
|
494
|
-
});
|
|
495
|
-
}
|
|
496
|
-
/** StandardCard 模板结构(与钉钉官方 Go 打字机示例完全一致:text=标题,markdown=内容) */
|
|
497
|
-
function buildStandardCardData(cardData) {
|
|
498
|
-
const title = String(cardData.title ?? 'AI');
|
|
499
|
-
const content = String(cardData.content ?? cardData.displayText ?? '').trim() || '...';
|
|
500
|
-
const schema = {
|
|
501
|
-
config: { autoLayout: true, enableForward: true },
|
|
502
|
-
header: {
|
|
503
|
-
title: { type: 'text', text: title },
|
|
504
|
-
logo: '@lALPDfJ6V_FPDmvNAfTNAfQ',
|
|
505
|
-
},
|
|
506
|
-
contents: [
|
|
507
|
-
{ type: 'text', text: title, id: 'text_1693929551595' },
|
|
508
|
-
{ type: 'divider', id: 'divider_1693929551595' },
|
|
509
|
-
{ type: 'markdown', text: content, id: 'markdown_1693929674245' },
|
|
510
|
-
],
|
|
511
|
-
};
|
|
512
|
-
return JSON.stringify(schema);
|
|
513
|
-
}
|
|
514
|
-
/** 互动卡片普通版:发送(用于 prepare 失败时的 fallback 流式) */
|
|
515
|
-
export async function sendRobotInteractiveCard(target, cardBizId, cardData) {
|
|
516
|
-
const { chatId, robotCode, conversationType } = target;
|
|
517
|
-
if (!robotCode) {
|
|
518
|
-
throw new Error('DingTalk robotCode required for interactive card');
|
|
519
|
-
}
|
|
520
|
-
const cardDataStr = buildStandardCardData(cardData);
|
|
521
|
-
const isSingle = isSingleConversation(conversationType);
|
|
522
|
-
const body = {
|
|
523
|
-
cardTemplateId: 'StandardCard',
|
|
524
|
-
cardBizId,
|
|
525
|
-
outTrackId: cardBizId,
|
|
526
|
-
robotCode,
|
|
527
|
-
cardData: cardDataStr,
|
|
528
|
-
};
|
|
529
|
-
if (isSingle && target.senderStaffId) {
|
|
530
|
-
body.singleChatReceiver = JSON.stringify({ userid: target.senderStaffId });
|
|
531
|
-
}
|
|
532
|
-
else {
|
|
533
|
-
body.openConversationId = chatId;
|
|
534
|
-
}
|
|
535
|
-
log.debug(`DingTalk sendRobotInteractiveCard: isSingle=${isSingle}, robotCode=${robotCode?.slice(0, 8)}..., chatIdLen=${chatId?.length}`);
|
|
536
|
-
try {
|
|
537
|
-
await callOpenApiWithMethod('POST', '/v1.0/im/v1.0/robot/interactiveCards/send', body);
|
|
538
|
-
}
|
|
539
|
-
catch (err) {
|
|
540
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
541
|
-
if (msg.includes('param.error') || msg.includes('参数无效')) {
|
|
542
|
-
log.warn('DingTalk robot interactive card param.error - request body (no secrets):', JSON.stringify({ ...body, robotCode: body.robotCode ? '[REDACTED]' : undefined }, null, 2));
|
|
543
|
-
}
|
|
544
|
-
throw err;
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
/** 互动卡片普通版:更新(单条消息流式更新) */
|
|
548
|
-
export async function updateRobotInteractiveCard(cardBizId, cardData) {
|
|
549
|
-
const cardDataStr = buildStandardCardData(cardData);
|
|
550
|
-
const body = { cardBizId, cardData: cardDataStr };
|
|
551
|
-
await callOpenApiWithMethod('PUT', '/v1.0/im/robots/interactiveCards', body);
|
|
552
|
-
}
|
|
553
|
-
async function callOpenApiWithMethod(method, path, body) {
|
|
554
|
-
const accessToken = await getClient().getAccessToken();
|
|
555
|
-
const res = await fetch(`${DINGTALK_OPENAPI_BASE}${path}`, {
|
|
556
|
-
method,
|
|
557
|
-
headers: {
|
|
558
|
-
'content-type': 'application/json',
|
|
559
|
-
'x-acs-dingtalk-access-token': String(accessToken),
|
|
560
|
-
},
|
|
561
|
-
body: JSON.stringify(body),
|
|
562
|
-
signal: AbortSignal.timeout(30000),
|
|
563
|
-
});
|
|
564
|
-
const text = await res.text();
|
|
565
|
-
if (!res.ok) {
|
|
566
|
-
throw new Error(`DingTalk OpenAPI failed: ${res.status} ${text}`);
|
|
567
|
-
}
|
|
568
|
-
let parsed;
|
|
569
|
-
try {
|
|
570
|
-
parsed = JSON.parse(text);
|
|
571
|
-
}
|
|
572
|
-
catch {
|
|
573
|
-
return text;
|
|
574
|
-
}
|
|
575
|
-
const errorCode = parsed.errorcode ?? parsed.errcode;
|
|
576
|
-
const success = parsed.success;
|
|
577
|
-
if (errorCode === 0 ||
|
|
578
|
-
errorCode === '0' ||
|
|
579
|
-
success === true ||
|
|
580
|
-
(errorCode === undefined && success === undefined)) {
|
|
581
|
-
return parsed;
|
|
582
|
-
}
|
|
583
|
-
const errorMessage = typeof parsed.errmsg === 'string'
|
|
584
|
-
? parsed.errmsg
|
|
585
|
-
: typeof parsed.errormsg === 'string'
|
|
586
|
-
? parsed.errormsg
|
|
587
|
-
: typeof parsed.message === 'string'
|
|
588
|
-
? parsed.message
|
|
589
|
-
: text;
|
|
590
|
-
throw new Error(`DingTalk OpenAPI business error: ${String(errorCode)} ${errorMessage}`);
|
|
591
|
-
}
|
|
@@ -24,7 +24,6 @@ function parseRobotMessage(data) {
|
|
|
24
24
|
export function setupDingTalkHandlers(config, sessionManager) {
|
|
25
25
|
configureDingTalkMessageSender({
|
|
26
26
|
cardTemplateId: config.dingtalkCardTemplateId,
|
|
27
|
-
robotCodeFallback: config.dingtalkClientId,
|
|
28
27
|
});
|
|
29
28
|
if (config.dingtalkCardTemplateId) {
|
|
30
29
|
log.info('DingTalk AI card streaming enabled');
|
|
@@ -44,7 +43,7 @@ export function setupDingTalkHandlers(config, sessionManager) {
|
|
|
44
43
|
getRunningTasksSize: () => runningTasks.size,
|
|
45
44
|
});
|
|
46
45
|
registerPermissionSender('dingtalk', { sendTextReply, sendPermissionCard });
|
|
47
|
-
async function handleAIRequest(userId, chatId, prompt, workDir, convId, _threadCtx, replyToMessageId
|
|
46
|
+
async function handleAIRequest(userId, chatId, prompt, workDir, convId, _threadCtx, replyToMessageId) {
|
|
48
47
|
log.info(`[AI_REQUEST] userId=${userId}, chatId=${chatId}, promptLength=${prompt.length}`);
|
|
49
48
|
const toolAdapter = getAdapter(config.aiCommand);
|
|
50
49
|
if (!toolAdapter) {
|
|
@@ -56,7 +55,7 @@ export function setupDingTalkHandlers(config, sessionManager) {
|
|
|
56
55
|
: undefined;
|
|
57
56
|
log.info(`[AI_REQUEST] Running ${config.aiCommand} for user ${userId}, sessionId=${sessionId ?? 'new'}`);
|
|
58
57
|
const toolId = config.aiCommand;
|
|
59
|
-
const msgId = await sendThinkingMessage(chatId, replyToMessageId, toolId
|
|
58
|
+
const msgId = await sendThinkingMessage(chatId, replyToMessageId, toolId);
|
|
60
59
|
const stopTyping = startTypingLoop(chatId);
|
|
61
60
|
const taskKey = `${userId}:${msgId}`;
|
|
62
61
|
await runAITask({ config, sessionManager }, { userId, chatId, workDir, sessionId, convId, platform: 'dingtalk', taskKey }, prompt, toolAdapter, {
|
|
@@ -113,7 +112,7 @@ export function setupDingTalkHandlers(config, sessionManager) {
|
|
|
113
112
|
chatId,
|
|
114
113
|
userId,
|
|
115
114
|
conversationType: robotMessage.conversationType,
|
|
116
|
-
robotCode: robotMessage.robotCode
|
|
115
|
+
robotCode: robotMessage.robotCode,
|
|
117
116
|
});
|
|
118
117
|
setChatUser(chatId, userId, 'dingtalk');
|
|
119
118
|
try {
|
|
@@ -128,15 +127,8 @@ export function setupDingTalkHandlers(config, sessionManager) {
|
|
|
128
127
|
}
|
|
129
128
|
const workDir = sessionManager.getWorkDir(userId);
|
|
130
129
|
const convId = sessionManager.getConvId(userId);
|
|
131
|
-
const dingtalkTarget = {
|
|
132
|
-
chatId,
|
|
133
|
-
conversationType: robotMessage.conversationType,
|
|
134
|
-
senderStaffId: robotMessage.senderStaffId,
|
|
135
|
-
senderId: robotMessage.senderId,
|
|
136
|
-
robotCode: robotMessage.robotCode || config.dingtalkClientId,
|
|
137
|
-
};
|
|
138
130
|
const enqueueResult = requestQueue.enqueue(userId, convId, text, async (prompt) => {
|
|
139
|
-
await handleAIRequest(userId, chatId, prompt, workDir, convId
|
|
131
|
+
await handleAIRequest(userId, chatId, prompt, workDir, convId);
|
|
140
132
|
});
|
|
141
133
|
if (enqueueResult === 'rejected') {
|
|
142
134
|
await sendTextReply(chatId, '请求队列已满,请稍后再试。');
|
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
import type { DingTalkStreamingTarget } from './client.js';
|
|
2
1
|
import type { ThreadContext } from '../shared/types.js';
|
|
3
2
|
import type { DingTalkActiveTarget } from '../shared/active-chats.js';
|
|
4
3
|
export type MessageStatus = 'thinking' | 'streaming' | 'done' | 'error';
|
|
5
4
|
interface SenderSettings {
|
|
6
5
|
cardTemplateId?: string;
|
|
7
|
-
robotCodeFallback?: string;
|
|
8
6
|
}
|
|
9
7
|
export declare function configureDingTalkMessageSender(settings: SenderSettings): void;
|
|
10
|
-
export declare function sendThinkingMessage(chatId: string, _replyToMessageId?: string, toolId?: string
|
|
8
|
+
export declare function sendThinkingMessage(chatId: string, _replyToMessageId?: string, toolId?: string): Promise<string>;
|
|
11
9
|
export declare function updateMessage(chatId: string, messageId: string, content: string, status: MessageStatus, note?: string, toolId?: string): Promise<void>;
|
|
12
10
|
export declare function sendFinalMessages(chatId: string, messageId: string, fullContent: string, note: string, toolId?: string): Promise<void>;
|
|
13
11
|
export declare function sendErrorMessage(chatId: string, messageId: string, error: string, toolId?: string): Promise<void>;
|