@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 CHANGED
@@ -8,7 +8,8 @@
8
8
  - 多 AI 工具:支持 Claude、Codex、Cursor
9
9
  - 流式输出:实时回传 AI 回复与工具执行进度
10
10
  - 会话隔离:每个用户独立维护本地会话,`/new` 可重置
11
- - 常用命令:支持 `/help`、`/new`、`/cd`、`/pwd`、`/status`
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
- - 若配置了 `cardTemplateId`,会尝试 AI 助理 `prepare/update/finish` 流式卡片;失败则 fallback 为普通文本(自定义机器人/普通群场景下互动卡片 API 报 param.error,暂不支持单条流式更新)
188
+ - 如果配置了 `cardTemplateId` / `DINGTALK_CARD_TEMPLATE_ID`,AI 回复会升级为单条 `ai_card` 流式更新(`prepare / update / finish`)
188
189
  - 启动/关闭通知会发给最近一次已互动的钉钉会话;如果服务冷启动后还没有任何钉钉会话互动过,则没有可用目标可发
189
190
 
190
- 钉钉 AI 卡片模板:已适配官方「搜索结果卡片」模板,使用变量 `lastMessage`、`content`、`resources`、`users`、`flowStatus`。若使用该模板,无需修改模板即可实现流式更新。
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
- **钉钉没有流式更新**:prepare 失败时 fallback 为普通文本回复。自定义机器人/普通群场景下,AI 助理和互动卡片 API 均不可用,仅支持单条文本回复。
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 - 解析 `codex exec --json` 的 JSONL 输出。
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;
@@ -1,5 +1,7 @@
1
1
  /**
2
- * Codex CLI Runner - 解析 `codex exec --json` 的 JSONL 输出。
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
- const threadId = event.thread_id ?? '';
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(target: string | DingTalkStreamingTarget, templateId: string, cardData: Record<string, unknown>): Promise<string>;
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>;
@@ -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
- const msg = err instanceof Error ? err.message : String(err);
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(target, templateId, cardData) {
319
- const normalizedTarget = typeof target === 'string' ? { chatId: target } : target;
320
- const contentType = 'ai_card';
321
- const content = buildAiCardContent(templateId, cardData);
322
- const attempts = [];
323
- log.debug(`DingTalk prepare: conversationType=${normalizedTarget.conversationType ?? 'undefined'}, senderStaffId=${normalizedTarget.senderStaffId ?? 'undefined'}`);
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, dingtalkTarget) {
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, dingtalkTarget);
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 || config.dingtalkClientId,
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, undefined, undefined, dingtalkTarget);
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, target?: DingTalkStreamingTarget): Promise<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>;