dsh-caduceus 0.5.1 → 0.7.1
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 +7 -1
- package/README.zh.md +7 -1
- package/dist/bridge/bridge.js +93 -6
- package/dist/bridge/types.d.ts +8 -0
- package/dist/commands/parse.d.ts +3 -1
- package/dist/commands/parse.js +4 -1
- package/dist/config.d.ts +29 -0
- package/dist/config.js +21 -0
- package/dist/doctor.d.ts +5 -0
- package/dist/doctor.js +9 -0
- package/dist/index.js +4 -1
- package/dist/media.d.ts +27 -0
- package/dist/media.js +68 -0
- package/dist/transport/feishu-api.d.ts +6 -2
- package/dist/transport/lark.d.ts +2 -0
- package/dist/transport/lark.js +29 -0
- package/dist/transport/normalize.js +3 -1
- package/dist/transport/reliable.d.ts +2 -0
- package/dist/transport/reliable.js +18 -2
- package/dist/transport/types.d.ts +11 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
- **One scope, one session** — p2p, topic, and group-per-user sessions are kept separate.
|
|
16
16
|
- **Visible titles** — generated from the first prompt and shown on the turn card.
|
|
17
17
|
- **One card per turn** — status and answer live in the same card, edited in place; the final answer is not a second message.
|
|
18
|
-
- **
|
|
18
|
+
- **12 commands** — `/new`, `/sessions`, `/rename`, `/resume`, `/adopt`, `/status`, `/stop`, `/doctor`, `/selfcheck`, `/persona`, `/whoami`, `/help`.
|
|
19
19
|
- **Persona** — `SOUL.md` is synced into the `AGENTS.md` block, automatically at startup.
|
|
20
20
|
- **Observability** — two-stage self-check, `/doctor`, and in-chat degradation warnings.
|
|
21
21
|
|
|
@@ -90,6 +90,11 @@ onlyBuiltDependencies:
|
|
|
90
90
|
| `images` | `true` | Accept image messages and attach them to the agent prompt |
|
|
91
91
|
| `files` | `true` | Accept file messages (durable attachment or saved to cwd) |
|
|
92
92
|
| `maxAttachmentBytes` | `20971520` | Per-attachment download limit (20 MiB) |
|
|
93
|
+
| `sendMedia` | `true` | Send `MEDIA:/path` outputs from the agent back to Feishu |
|
|
94
|
+
| `maxSendFileBytes` | `31457280` | Outbound file limit (30 MiB) |
|
|
95
|
+
| `maxSendImageBytes` | `10485760` | Outbound image limit (10 MiB) |
|
|
96
|
+
| `mediaRoots` | `[]` | Allowed roots for outbound paths (empty = session cwd) |
|
|
97
|
+
| `admins` | `[]` | Admin open-id allowlist; empty disables access control |
|
|
93
98
|
|
|
94
99
|
## Commands
|
|
95
100
|
|
|
@@ -105,6 +110,7 @@ onlyBuiltDependencies:
|
|
|
105
110
|
| `/doctor [--json]` | Health report: runtime / Feishu / persona / session / storage / versions |
|
|
106
111
|
| `/selfcheck` | Re-run both self-check stages and print every item |
|
|
107
112
|
| `/persona [show\|sync]` | Show or sync the SOUL persona block |
|
|
113
|
+
| `/whoami` | Show your open_id, current chat/topic, and admin/guest role |
|
|
108
114
|
| `/help` | Command table |
|
|
109
115
|
|
|
110
116
|
## Troubleshooting
|
package/README.zh.md
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
- **一个 scope 一个会话** — 私聊、话题、普通群按发送者隔离。
|
|
16
16
|
- **标题可见** — 由第一条 prompt 生成,显示在回合卡上。
|
|
17
17
|
- **每回合一张卡** — 状态与答案在同一张卡里,原地编辑;终稿不再另发一条消息。
|
|
18
|
-
- **
|
|
18
|
+
- **12 条命令** — `/new`、`/sessions`、`/rename`、`/resume`、`/adopt`、`/status`、`/stop`、`/doctor`、`/selfcheck`、`/persona`、`/whoami`、`/help`。
|
|
19
19
|
- **人格** — `SOUL.md` 同步到 `AGENTS.md` 的标记块,启动时自动执行。
|
|
20
20
|
- **可观测性** — 两阶段自检、`/doctor`、对话内的降级提示。
|
|
21
21
|
|
|
@@ -89,6 +89,11 @@ onlyBuiltDependencies:
|
|
|
89
89
|
| `images` | `true` | 接收图片消息并作为附件交给 agent |
|
|
90
90
|
| `files` | `true` | 接收文件消息(附件服务或落到 cwd) |
|
|
91
91
|
| `maxAttachmentBytes` | `20971520` | 单个附件下载上限(20 MiB) |
|
|
92
|
+
| `sendMedia` | `true` | 把 agent 的 `MEDIA:/path` 产物发回飞书 |
|
|
93
|
+
| `maxSendFileBytes` | `31457280` | 发送文件上限(30 MiB) |
|
|
94
|
+
| `maxSendImageBytes` | `10485760` | 发送图片上限(10 MiB) |
|
|
95
|
+
| `mediaRoots` | `[]` | 允许发送的目录(空=会话 cwd) |
|
|
96
|
+
| `admins` | `[]` | 管理员 open_id 白名单;空=关闭访问控制 |
|
|
92
97
|
|
|
93
98
|
## 命令
|
|
94
99
|
|
|
@@ -104,6 +109,7 @@ onlyBuiltDependencies:
|
|
|
104
109
|
| `/doctor [--json]` | 体检报告:运行时 / 飞书 / 人格 / 会话 / 存储 / 版本 |
|
|
105
110
|
| `/selfcheck` | 立即重跑两阶段自检并输出逐项明细 |
|
|
106
111
|
| `/persona [show\|sync]` | 查看或同步 SOUL 人格块 |
|
|
112
|
+
| `/whoami` | 显示你的 open_id、当前会话与身份 |
|
|
107
113
|
| `/help` | 命令表 |
|
|
108
114
|
|
|
109
115
|
## Troubleshooting(故障排查)
|
package/dist/bridge/bridge.js
CHANGED
|
@@ -17,15 +17,16 @@
|
|
|
17
17
|
* @module dsh-caduceus/bridge/bridge
|
|
18
18
|
*/
|
|
19
19
|
import { createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
20
|
-
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
20
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
21
21
|
import { join } from 'node:path';
|
|
22
22
|
import { planSlashLine } from '../commands/dispatch.js';
|
|
23
|
-
import { helpText } from '../commands/parse.js';
|
|
23
|
+
import { helpText, parseCommand } from '../commands/parse.js';
|
|
24
24
|
import { evaluateGate, logGate } from '../gate.js';
|
|
25
25
|
import { silentLogger } from '../logger.js';
|
|
26
26
|
import { formatScopeLine, sessionIdTimestamp, sessionTimeLabel, shortSessionId, } from '../observability.js';
|
|
27
27
|
import { buildDoctorReport, renderDoctorJson, renderDoctorText, } from '../doctor.js';
|
|
28
28
|
import { writeOwnership } from '../ownership.js';
|
|
29
|
+
import { extractMediaMarkers, resolveMedia } from '../media.js';
|
|
29
30
|
import { finalCard, finalCardPlainText, progressCard, } from '../render/cards.js';
|
|
30
31
|
import { CardStream, systemClock } from '../render/stream.js';
|
|
31
32
|
import { mintSessionId, scopeKey, SCOPE_KEY_SEPARATOR } from '../session-key.js';
|
|
@@ -180,8 +181,27 @@ export class Bridge {
|
|
|
180
181
|
}
|
|
181
182
|
if (!decision.accept)
|
|
182
183
|
return;
|
|
184
|
+
if (!this.#isAdmin(message.senderOpenId) && !this.#isPublicCommand(message.text)) {
|
|
185
|
+
const state = this.#stateFor(message);
|
|
186
|
+
state.lastInboundMessageId = message.messageId;
|
|
187
|
+
state.ownerOpenId = message.senderOpenId;
|
|
188
|
+
this.#logger.info(`gate drop: not-admin sender=${message.senderOpenId.slice(0, 12) || '-'}`);
|
|
189
|
+
await this.#replyText(state, '访客无执行权限。');
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
183
192
|
await this.#queue.run(scopeKey(message.chatType, message.chatId, message.threadId, message.senderOpenId, this.#config.groupSessionsPerUser), () => this.#processInbound(message));
|
|
184
193
|
}
|
|
194
|
+
#isAdmin(openId) {
|
|
195
|
+
const admins = this.#config.admins ?? [];
|
|
196
|
+
return admins.length === 0 || admins.includes(openId);
|
|
197
|
+
}
|
|
198
|
+
/** `/help` and `/whoami` are safe for any sender (no execution, no session). */
|
|
199
|
+
#isPublicCommand(text) {
|
|
200
|
+
const command = parseCommand(text);
|
|
201
|
+
return (command.kind === 'help' ||
|
|
202
|
+
command.kind === 'whoami' ||
|
|
203
|
+
(command.kind === 'usage' && (command.command === 'help' || command.command === 'whoami')));
|
|
204
|
+
}
|
|
185
205
|
/** Feed one dsh session event into the render/status layer. */
|
|
186
206
|
async handleSessionEvent(session, event) {
|
|
187
207
|
const key = this.#scopeBySession.get(String(session.id));
|
|
@@ -409,6 +429,13 @@ export class Bridge {
|
|
|
409
429
|
await this.#replyText(state, result.text);
|
|
410
430
|
return;
|
|
411
431
|
}
|
|
432
|
+
case 'whoami': {
|
|
433
|
+
const openId = state.ownerOpenId ?? '';
|
|
434
|
+
const identity = this.#isAdmin(openId) ? '管理员' : '访客';
|
|
435
|
+
const topic = state.threadId === undefined ? '' : `[话题 ${state.threadId}]`;
|
|
436
|
+
await this.#replyText(state, `你的 open_id:${openId}\n当前会话:${state.chatId}${topic}\n身份:${identity}`);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
412
439
|
case 'help': {
|
|
413
440
|
await this.#replyText(state, helpText());
|
|
414
441
|
return;
|
|
@@ -583,9 +610,11 @@ export class Bridge {
|
|
|
583
610
|
// but re-apply to guarantee /status never disagrees with the final card.
|
|
584
611
|
state.status.apply(event);
|
|
585
612
|
const snapshot = state.status.get();
|
|
586
|
-
const
|
|
613
|
+
const rawBody = state.lastOutput.trim().length > 0
|
|
587
614
|
? state.lastOutput
|
|
588
615
|
: `本轮已结束(${finalStateLabel})。`;
|
|
616
|
+
const outbound = this.#planOutboundMedia(state, rawBody);
|
|
617
|
+
const body = outbound.body;
|
|
589
618
|
const finalInput = this.#finalCardInput(state, snapshot, body, finalState, finalStateLabel);
|
|
590
619
|
const stream = state.progress;
|
|
591
620
|
const messageId = stream?.messageId;
|
|
@@ -611,6 +640,7 @@ export class Bridge {
|
|
|
611
640
|
await this.#finalFallback(state, finalInput, error);
|
|
612
641
|
}
|
|
613
642
|
}
|
|
643
|
+
await this.#sendOutboundMedia(state, outbound.entries);
|
|
614
644
|
if (shouldNotifyTerminal(kind)) {
|
|
615
645
|
const notice = terminalNotice(kind, snapshot.errorMessage);
|
|
616
646
|
if (notice !== undefined)
|
|
@@ -619,6 +649,60 @@ export class Bridge {
|
|
|
619
649
|
await this.#finishReactions(state, kind);
|
|
620
650
|
state.lastOutput = '';
|
|
621
651
|
}
|
|
652
|
+
/** Extract MEDIA markers, validate paths, and annotate the final card body. */
|
|
653
|
+
#planOutboundMedia(state, rawBody) {
|
|
654
|
+
if (this.#config.sendMedia === false)
|
|
655
|
+
return { body: rawBody, entries: [] };
|
|
656
|
+
const parsed = extractMediaMarkers(rawBody);
|
|
657
|
+
if (parsed.paths.length === 0)
|
|
658
|
+
return { body: parsed.text, entries: [] };
|
|
659
|
+
const cwd = this.#sessionMap.getCwd(state.key) ?? this.#config.defaultCwd;
|
|
660
|
+
const roots = this.#config.mediaRoots?.length ? this.#config.mediaRoots : [cwd];
|
|
661
|
+
const entries = [];
|
|
662
|
+
const notes = [];
|
|
663
|
+
for (const path of parsed.paths) {
|
|
664
|
+
const resolved = resolveMedia(path, {
|
|
665
|
+
roots,
|
|
666
|
+
maxFileBytes: this.#config.maxSendFileBytes ?? 30 * 1024 * 1024,
|
|
667
|
+
maxImageBytes: this.#config.maxSendImageBytes ?? 10 * 1024 * 1024,
|
|
668
|
+
});
|
|
669
|
+
if (!resolved.ok) {
|
|
670
|
+
notes.push(`📎 ${resolved.name}:未发送(${resolved.reason})`);
|
|
671
|
+
continue;
|
|
672
|
+
}
|
|
673
|
+
entries.push({ path: resolved.path, name: resolved.name, image: resolved.image });
|
|
674
|
+
notes.push(`📎 ${resolved.name} × 1`);
|
|
675
|
+
}
|
|
676
|
+
const body = parsed.text.length === 0 ? notes.join('\n') : `${parsed.text}\n\n${notes.join('\n')}`;
|
|
677
|
+
return { body, entries };
|
|
678
|
+
}
|
|
679
|
+
/** Upload and reply each validated file individually; one failure never stops the rest. */
|
|
680
|
+
async #sendOutboundMedia(state, entries) {
|
|
681
|
+
if (entries.length === 0)
|
|
682
|
+
return;
|
|
683
|
+
const replyTo = state.lastInboundMessageId;
|
|
684
|
+
if (replyTo === undefined) {
|
|
685
|
+
this.#logger.warn('media send skipped: no inbound message to reply to');
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
const threadOptions = state.threadId === undefined ? {} : { threadId: state.threadId };
|
|
689
|
+
for (const entry of entries) {
|
|
690
|
+
try {
|
|
691
|
+
const bytes = readFileSync(entry.path);
|
|
692
|
+
if (entry.image) {
|
|
693
|
+
const imageKey = await this.#transport.uploadImage(bytes);
|
|
694
|
+
await this.#transport.reply(state.chatId, replyTo, { kind: 'image', imageKey }, threadOptions);
|
|
695
|
+
}
|
|
696
|
+
else {
|
|
697
|
+
const fileKey = await this.#transport.uploadFile(entry.name, bytes);
|
|
698
|
+
await this.#transport.reply(state.chatId, replyTo, { kind: 'file', fileKey, fileName: entry.name }, threadOptions);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
catch (error) {
|
|
702
|
+
this.#logger.warn(`media send failed for ${entry.name}: ${errorMessage(error)}`);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
622
706
|
#finalCardInput(state, snapshot, body, finalState, finalStateLabel) {
|
|
623
707
|
const sessionTitle = this.#titleOf(state)?.trim() ?? state.title;
|
|
624
708
|
return {
|
|
@@ -1371,8 +1455,10 @@ export class Bridge {
|
|
|
1371
1455
|
});
|
|
1372
1456
|
}
|
|
1373
1457
|
async #sendNewCard(state, card) {
|
|
1374
|
-
|
|
1375
|
-
|
|
1458
|
+
// A topic must receive a thread reply; a normal chat must get a plain card
|
|
1459
|
+
// message (quoting the user's message creates a fake "topic" UI).
|
|
1460
|
+
if (state.threadId !== undefined && state.lastInboundMessageId !== undefined) {
|
|
1461
|
+
return this.#transport.reply(state.chatId, state.lastInboundMessageId, { kind: 'card', card }, { threadId: state.threadId });
|
|
1376
1462
|
}
|
|
1377
1463
|
return this.#transport.sendCard(state.chatId, card);
|
|
1378
1464
|
}
|
|
@@ -1390,10 +1476,11 @@ export class Bridge {
|
|
|
1390
1476
|
if (text.length === 0)
|
|
1391
1477
|
return;
|
|
1392
1478
|
try {
|
|
1393
|
-
if (state.lastInboundMessageId !== undefined) {
|
|
1479
|
+
if (state.threadId !== undefined && state.lastInboundMessageId !== undefined) {
|
|
1394
1480
|
await this.#transport.reply(state.chatId, state.lastInboundMessageId, { kind: 'text', text }, { threadId: state.threadId });
|
|
1395
1481
|
}
|
|
1396
1482
|
else {
|
|
1483
|
+
// Normal chat: a fresh message, not a quoted "topic reply".
|
|
1397
1484
|
await this.#transport.sendText(state.chatId, text);
|
|
1398
1485
|
}
|
|
1399
1486
|
}
|
package/dist/bridge/types.d.ts
CHANGED
|
@@ -114,6 +114,14 @@ export interface BridgeConfig {
|
|
|
114
114
|
readonly files?: boolean;
|
|
115
115
|
/** Per-attachment byte cap (default 20 MiB). */
|
|
116
116
|
readonly maxAttachmentBytes?: number;
|
|
117
|
+
/** Send MEDIA:/path outputs back to Feishu (default true). */
|
|
118
|
+
readonly sendMedia?: boolean;
|
|
119
|
+
readonly maxSendFileBytes?: number;
|
|
120
|
+
readonly maxSendImageBytes?: number;
|
|
121
|
+
/** Allowed roots for outbound MEDIA paths; empty = session cwd. */
|
|
122
|
+
readonly mediaRoots?: readonly string[];
|
|
123
|
+
/** Admin allowlist; empty disables access control (default). */
|
|
124
|
+
readonly admins?: readonly string[];
|
|
117
125
|
}
|
|
118
126
|
/** Everything the bridge needs; unit tests provide fakes for every seam. */
|
|
119
127
|
export interface BridgeOptions {
|
package/dist/commands/parse.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Commands dsh-caduceus owns. Keep in sync with README's command table. */
|
|
2
|
-
export declare const COMMAND_NAMES: readonly ["new", "sessions", "rename", "resume", "adopt", "selfcheck", "doctor", "stop", "status", "persona", "help"];
|
|
2
|
+
export declare const COMMAND_NAMES: readonly ["new", "sessions", "rename", "resume", "adopt", "selfcheck", "doctor", "stop", "status", "persona", "whoami", "help"];
|
|
3
3
|
export type CommandName = (typeof COMMAND_NAMES)[number];
|
|
4
4
|
export interface CommandSpec {
|
|
5
5
|
readonly name: CommandName;
|
|
@@ -36,6 +36,8 @@ export type ParsedCommand = {
|
|
|
36
36
|
} | {
|
|
37
37
|
readonly kind: 'persona';
|
|
38
38
|
readonly action: PersonaAction;
|
|
39
|
+
} | {
|
|
40
|
+
readonly kind: 'whoami';
|
|
39
41
|
} | {
|
|
40
42
|
readonly kind: 'help';
|
|
41
43
|
} | {
|
package/dist/commands/parse.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { stripLeadingMentions } from '../mentions.js';
|
|
13
13
|
/** Commands dsh-caduceus owns. Keep in sync with README's command table. */
|
|
14
|
-
export const COMMAND_NAMES = ['new', 'sessions', 'rename', 'resume', 'adopt', 'selfcheck', 'doctor', 'stop', 'status', 'persona', 'help'];
|
|
14
|
+
export const COMMAND_NAMES = ['new', 'sessions', 'rename', 'resume', 'adopt', 'selfcheck', 'doctor', 'stop', 'status', 'persona', 'whoami', 'help'];
|
|
15
15
|
/** Command metadata; `helpText()` and the README command table both derive from it. */
|
|
16
16
|
export const COMMAND_SPECS = [
|
|
17
17
|
{ name: 'new', usage: '/new', description: '为当前聊天/话题新建会话,旧会话保留' },
|
|
@@ -24,6 +24,7 @@ export const COMMAND_SPECS = [
|
|
|
24
24
|
{ name: 'stop', usage: '/stop', description: '中断当前正在运行的一轮' },
|
|
25
25
|
{ name: 'status', usage: '/status', description: '显示本会话的运行状态、步数与起止时间' },
|
|
26
26
|
{ name: 'persona', usage: '/persona [show|sync]', description: '查看或同步 SOUL 人格块' },
|
|
27
|
+
{ name: 'whoami', usage: '/whoami', description: '显示你的 open_id、当前会话与身份' },
|
|
27
28
|
{ name: 'help', usage: '/help', description: '显示命令表' },
|
|
28
29
|
];
|
|
29
30
|
function usageResult(command, reason) {
|
|
@@ -63,6 +64,8 @@ export function parseCommand(raw) {
|
|
|
63
64
|
return rest === '' ? { kind: 'stop' } : usageResult('stop', '/stop 不接受参数');
|
|
64
65
|
case 'status':
|
|
65
66
|
return rest === '' ? { kind: 'status' } : usageResult('status', '/status 不接受参数');
|
|
67
|
+
case 'whoami':
|
|
68
|
+
return rest === '' ? { kind: 'whoami' } : usageResult('whoami', '/whoami 不接受参数');
|
|
66
69
|
case 'help':
|
|
67
70
|
return { kind: 'help' };
|
|
68
71
|
case 'rename':
|
package/dist/config.d.ts
CHANGED
|
@@ -41,6 +41,16 @@ export interface GatewayConfig {
|
|
|
41
41
|
readonly files?: boolean;
|
|
42
42
|
/** Per-attachment byte cap (default 20 MiB). */
|
|
43
43
|
readonly maxAttachmentBytes?: number;
|
|
44
|
+
/** Send MEDIA:/path outputs back to Feishu (default true). */
|
|
45
|
+
readonly sendMedia?: boolean;
|
|
46
|
+
/** Outbound file size limit (default 30 MiB). */
|
|
47
|
+
readonly maxSendFileBytes?: number;
|
|
48
|
+
/** Outbound image size limit (default 10 MiB). */
|
|
49
|
+
readonly maxSendImageBytes?: number;
|
|
50
|
+
/** Allowed roots for MEDIA paths; empty = session cwd only. */
|
|
51
|
+
readonly mediaRoots?: readonly string[];
|
|
52
|
+
/** Admin open-id allowlist; empty disables access control (default). */
|
|
53
|
+
readonly admins?: readonly string[];
|
|
44
54
|
}
|
|
45
55
|
/** Schemastery schema for the `caduceus-gateway` entry. */
|
|
46
56
|
export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
@@ -67,6 +77,11 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
|
67
77
|
images: z<boolean, boolean, "plain">;
|
|
68
78
|
files: z<boolean, boolean, "plain">;
|
|
69
79
|
maxAttachmentBytes: z<number, number, "plain">;
|
|
80
|
+
sendMedia: z<boolean, boolean, "plain">;
|
|
81
|
+
maxSendFileBytes: z<number, number, "plain">;
|
|
82
|
+
maxSendImageBytes: z<number, number, "plain">;
|
|
83
|
+
mediaRoots: z<string[], string[], "plain">;
|
|
84
|
+
admins: z<string[], string[], "plain">;
|
|
70
85
|
}>>, Schemastery.ObjectT<NoInfer<{
|
|
71
86
|
appId: z<string, string, "plain">;
|
|
72
87
|
appSecret: z<string, string, "plain">;
|
|
@@ -91,6 +106,11 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
|
91
106
|
images: z<boolean, boolean, "plain">;
|
|
92
107
|
files: z<boolean, boolean, "plain">;
|
|
93
108
|
maxAttachmentBytes: z<number, number, "plain">;
|
|
109
|
+
sendMedia: z<boolean, boolean, "plain">;
|
|
110
|
+
maxSendFileBytes: z<number, number, "plain">;
|
|
111
|
+
maxSendImageBytes: z<number, number, "plain">;
|
|
112
|
+
mediaRoots: z<string[], string[], "plain">;
|
|
113
|
+
admins: z<string[], string[], "plain">;
|
|
94
114
|
}>>, "plain">;
|
|
95
115
|
/** Default card patch throttle: half of Feishu's 5 QPS budget. */
|
|
96
116
|
export declare const DEFAULT_CARD_THROTTLE_MS = 400;
|
|
@@ -108,6 +128,10 @@ export declare const DEFAULT_AGENT_PRESET = "standard";
|
|
|
108
128
|
export declare const DEFAULT_IMAGES = true;
|
|
109
129
|
export declare const DEFAULT_FILES = true;
|
|
110
130
|
export declare const DEFAULT_MAX_ATTACHMENT_BYTES: number;
|
|
131
|
+
/** Default outbound media switches/limits. */
|
|
132
|
+
export declare const DEFAULT_SEND_MEDIA = true;
|
|
133
|
+
export declare const DEFAULT_MAX_SEND_FILE_BYTES: number;
|
|
134
|
+
export declare const DEFAULT_MAX_SEND_IMAGE_BYTES: number;
|
|
111
135
|
/** Default message reactions. */
|
|
112
136
|
export declare const DEFAULT_REACTIONS = true;
|
|
113
137
|
export declare const DEFAULT_REACTION_ON_START = "OnIt";
|
|
@@ -119,6 +143,11 @@ export declare const DEFAULT_APPROVAL_POLICY: ApprovalPolicy;
|
|
|
119
143
|
export declare const DEFAULT_APPROVAL_CARD = true;
|
|
120
144
|
export declare const DEFAULT_QUESTION_CARD = true;
|
|
121
145
|
export declare const DEFAULT_INTERACTIVE_TIMEOUT_MS = 300000;
|
|
146
|
+
/**
|
|
147
|
+
* Startup warning for access control (empty admin list = disabled).
|
|
148
|
+
* Exported so the log line stays testable without mounting the plugin.
|
|
149
|
+
*/
|
|
150
|
+
export declare function accessControlWarning(admins: readonly string[]): string | undefined;
|
|
122
151
|
export interface ResolvedGatewayConfig {
|
|
123
152
|
readonly credentials?: {
|
|
124
153
|
readonly appId: string;
|
package/dist/config.js
CHANGED
|
@@ -35,6 +35,11 @@ export const Config = z.object({
|
|
|
35
35
|
images: z.boolean().required(false),
|
|
36
36
|
files: z.boolean().required(false),
|
|
37
37
|
maxAttachmentBytes: z.natural().min(1).required(false),
|
|
38
|
+
sendMedia: z.boolean().required(false),
|
|
39
|
+
maxSendFileBytes: z.natural().min(1).required(false),
|
|
40
|
+
maxSendImageBytes: z.natural().min(1).required(false),
|
|
41
|
+
mediaRoots: z.array(z.string()).required(false),
|
|
42
|
+
admins: z.array(z.string()).required(false),
|
|
38
43
|
});
|
|
39
44
|
/** Default card patch throttle: half of Feishu's 5 QPS budget. */
|
|
40
45
|
export const DEFAULT_CARD_THROTTLE_MS = 400;
|
|
@@ -52,6 +57,10 @@ export const DEFAULT_AGENT_PRESET = 'standard';
|
|
|
52
57
|
export const DEFAULT_IMAGES = true;
|
|
53
58
|
export const DEFAULT_FILES = true;
|
|
54
59
|
export const DEFAULT_MAX_ATTACHMENT_BYTES = 20 * 1024 * 1024;
|
|
60
|
+
/** Default outbound media switches/limits. */
|
|
61
|
+
export const DEFAULT_SEND_MEDIA = true;
|
|
62
|
+
export const DEFAULT_MAX_SEND_FILE_BYTES = 30 * 1024 * 1024;
|
|
63
|
+
export const DEFAULT_MAX_SEND_IMAGE_BYTES = 10 * 1024 * 1024;
|
|
55
64
|
/** Default message reactions. */
|
|
56
65
|
export const DEFAULT_REACTIONS = true;
|
|
57
66
|
export const DEFAULT_REACTION_ON_START = 'OnIt';
|
|
@@ -63,6 +72,13 @@ export const DEFAULT_APPROVAL_POLICY = 'ask';
|
|
|
63
72
|
export const DEFAULT_APPROVAL_CARD = true;
|
|
64
73
|
export const DEFAULT_QUESTION_CARD = true;
|
|
65
74
|
export const DEFAULT_INTERACTIVE_TIMEOUT_MS = 300_000;
|
|
75
|
+
/**
|
|
76
|
+
* Startup warning for access control (empty admin list = disabled).
|
|
77
|
+
* Exported so the log line stays testable without mounting the plugin.
|
|
78
|
+
*/
|
|
79
|
+
export function accessControlWarning(admins) {
|
|
80
|
+
return admins.length === 0 ? 'access control disabled (no admins configured)' : undefined;
|
|
81
|
+
}
|
|
66
82
|
function firstNonEmpty(...values) {
|
|
67
83
|
for (const value of values) {
|
|
68
84
|
if (value !== undefined && value.length > 0)
|
|
@@ -96,6 +112,11 @@ export function resolveGatewayConfig(config = {}, env = process.env, cwd = proce
|
|
|
96
112
|
images: config.images ?? DEFAULT_IMAGES,
|
|
97
113
|
files: config.files ?? DEFAULT_FILES,
|
|
98
114
|
maxAttachmentBytes: config.maxAttachmentBytes ?? DEFAULT_MAX_ATTACHMENT_BYTES,
|
|
115
|
+
sendMedia: config.sendMedia ?? DEFAULT_SEND_MEDIA,
|
|
116
|
+
maxSendFileBytes: config.maxSendFileBytes ?? DEFAULT_MAX_SEND_FILE_BYTES,
|
|
117
|
+
maxSendImageBytes: config.maxSendImageBytes ?? DEFAULT_MAX_SEND_IMAGE_BYTES,
|
|
118
|
+
mediaRoots: config.mediaRoots ?? [],
|
|
119
|
+
admins: config.admins ?? [],
|
|
99
120
|
};
|
|
100
121
|
return {
|
|
101
122
|
...(appId === undefined || appSecret === undefined ? {} : { credentials: { appId, appSecret } }),
|
package/dist/doctor.d.ts
CHANGED
|
@@ -57,6 +57,11 @@ export interface DoctorInputs {
|
|
|
57
57
|
readonly env?: HomeEnv;
|
|
58
58
|
};
|
|
59
59
|
readonly session: DoctorSessionInfo;
|
|
60
|
+
/** Access-control state (`admins` allowlist). */
|
|
61
|
+
readonly access?: {
|
|
62
|
+
readonly enabled: boolean;
|
|
63
|
+
readonly adminCount: number;
|
|
64
|
+
};
|
|
60
65
|
readonly storage: {
|
|
61
66
|
readonly mapVersion?: number;
|
|
62
67
|
readonly mapWritable?: boolean;
|
package/dist/doctor.js
CHANGED
|
@@ -116,6 +116,14 @@ export async function buildDoctorReport(inputs) {
|
|
|
116
116
|
detail: `preset=${presetText},工具 ${toolsText},${relativeAge(lastTurnInput.at, now)}`,
|
|
117
117
|
};
|
|
118
118
|
});
|
|
119
|
+
const accessItem = inputs.access === undefined
|
|
120
|
+
? undefined
|
|
121
|
+
: await safeItem('runtime', 'access', '访问控制', () => ({
|
|
122
|
+
mark: inputs.access?.enabled === true ? 'ok' : 'skip',
|
|
123
|
+
detail: inputs.access?.enabled === true
|
|
124
|
+
? `开启(${inputs.access.adminCount} 人)`
|
|
125
|
+
: '关闭(未配置 admins)',
|
|
126
|
+
}));
|
|
119
127
|
const titleService = await safeItem('runtime', 'session-title', '标题服务', () => {
|
|
120
128
|
if (inputs.runtime.sessionTitleService === undefined)
|
|
121
129
|
return { mark: 'skip', detail: '不可用' };
|
|
@@ -249,6 +257,7 @@ export async function buildDoctorReport(inputs) {
|
|
|
249
257
|
tools,
|
|
250
258
|
sessionTools,
|
|
251
259
|
...(lastTurnItem === undefined ? [] : [lastTurnItem]),
|
|
260
|
+
...(accessItem === undefined ? [] : [accessItem]),
|
|
252
261
|
titleService,
|
|
253
262
|
agentsService,
|
|
254
263
|
],
|
package/dist/index.js
CHANGED
|
@@ -16,7 +16,7 @@ import { dirname, join } from 'node:path';
|
|
|
16
16
|
import { backfillSessionOwnership, scanSessionDirs } from './backfill.js';
|
|
17
17
|
import { Bridge } from './bridge/bridge.js';
|
|
18
18
|
import { runHarnessCommand } from './commands/harness.js';
|
|
19
|
-
import { Config, resolveGatewayConfig } from './config.js';
|
|
19
|
+
import { Config, accessControlWarning, resolveGatewayConfig, } from './config.js';
|
|
20
20
|
import { FileLogger } from './file-logger.js';
|
|
21
21
|
import { defaultLogPath } from './home.js';
|
|
22
22
|
import { adaptLogger, teeLoggers } from './logger.js';
|
|
@@ -140,6 +140,9 @@ export function apply(ctx, config = {}) {
|
|
|
140
140
|
busyMode: resolved.bridge.busyMode,
|
|
141
141
|
}));
|
|
142
142
|
logger.info(`agent preset=${resolved.agentPreset}`);
|
|
143
|
+
const accessWarning = accessControlWarning(resolved.bridge.admins ?? []);
|
|
144
|
+
if (accessWarning !== undefined)
|
|
145
|
+
logger.warn(accessWarning);
|
|
143
146
|
const selfCheck = new SelfChecker({
|
|
144
147
|
logger,
|
|
145
148
|
getAgentPresets: () => ctx.get('agentPresets'),
|
package/dist/media.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface MediaMarkerResult {
|
|
2
|
+
/** Answer text with every MEDIA marker line removed. */
|
|
3
|
+
readonly text: string;
|
|
4
|
+
/** Absolute paths in marker order. */
|
|
5
|
+
readonly paths: string[];
|
|
6
|
+
}
|
|
7
|
+
/** Strip one-line `MEDIA:/abs/path` markers; other lines stay untouched. */
|
|
8
|
+
export declare function extractMediaMarkers(text: string): MediaMarkerResult;
|
|
9
|
+
export declare function isImagePath(path: string): boolean;
|
|
10
|
+
export interface MediaLimits {
|
|
11
|
+
readonly roots: readonly string[];
|
|
12
|
+
readonly maxFileBytes: number;
|
|
13
|
+
readonly maxImageBytes: number;
|
|
14
|
+
}
|
|
15
|
+
export type MediaResolution = {
|
|
16
|
+
readonly ok: true;
|
|
17
|
+
readonly path: string;
|
|
18
|
+
readonly name: string;
|
|
19
|
+
readonly bytes: number;
|
|
20
|
+
readonly image: boolean;
|
|
21
|
+
} | {
|
|
22
|
+
readonly ok: false;
|
|
23
|
+
readonly name: string;
|
|
24
|
+
readonly reason: string;
|
|
25
|
+
};
|
|
26
|
+
/** Resolve and validate one marker path (absolute, inside roots, size limit). */
|
|
27
|
+
export declare function resolveMedia(path: string, limits: MediaLimits): MediaResolution;
|
package/dist/media.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `MEDIA:<absolute path>` markers in the agent's final answer, plus the safety
|
|
3
|
+
* boundary for sending local files back to Feishu.
|
|
4
|
+
*
|
|
5
|
+
* @module dsh-caduceus/media
|
|
6
|
+
*/
|
|
7
|
+
import { realpathSync, statSync } from 'node:fs';
|
|
8
|
+
import { basename, extname, isAbsolute, sep } from 'node:path';
|
|
9
|
+
const MEDIA_LINE = /^\s*MEDIA:(\S.*?)\s*$/;
|
|
10
|
+
/** Strip one-line `MEDIA:/abs/path` markers; other lines stay untouched. */
|
|
11
|
+
export function extractMediaMarkers(text) {
|
|
12
|
+
const kept = [];
|
|
13
|
+
const paths = [];
|
|
14
|
+
for (const line of text.split('\n')) {
|
|
15
|
+
const match = MEDIA_LINE.exec(line);
|
|
16
|
+
if (match !== null) {
|
|
17
|
+
const path = match[1].trim();
|
|
18
|
+
if (path.length > 0)
|
|
19
|
+
paths.push(path);
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
kept.push(line);
|
|
23
|
+
}
|
|
24
|
+
return { text: kept.join('\n').trim(), paths };
|
|
25
|
+
}
|
|
26
|
+
const IMAGE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.webp', '.gif']);
|
|
27
|
+
export function isImagePath(path) {
|
|
28
|
+
return IMAGE_EXTENSIONS.has(extname(path).toLowerCase());
|
|
29
|
+
}
|
|
30
|
+
/** Resolve and validate one marker path (absolute, inside roots, size limit). */
|
|
31
|
+
export function resolveMedia(path, limits) {
|
|
32
|
+
const name = basename(path) || path;
|
|
33
|
+
if (!isAbsolute(path))
|
|
34
|
+
return { ok: false, name, reason: '仅支持绝对路径' };
|
|
35
|
+
let real;
|
|
36
|
+
try {
|
|
37
|
+
real = realpathSync(path);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return { ok: false, name, reason: '文件不存在' };
|
|
41
|
+
}
|
|
42
|
+
const realRoots = [];
|
|
43
|
+
for (const root of limits.roots) {
|
|
44
|
+
try {
|
|
45
|
+
realRoots.push(realpathSync(root));
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// A missing root simply cannot authorize anything.
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const inside = realRoots.some((root) => real === root || real.startsWith(root.endsWith(sep) ? root : `${root}${sep}`));
|
|
52
|
+
if (!inside)
|
|
53
|
+
return { ok: false, name, reason: '路径不在允许目录内' };
|
|
54
|
+
let stat;
|
|
55
|
+
try {
|
|
56
|
+
stat = statSync(real);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return { ok: false, name, reason: '文件不存在' };
|
|
60
|
+
}
|
|
61
|
+
if (!stat.isFile())
|
|
62
|
+
return { ok: false, name, reason: '不是普通文件' };
|
|
63
|
+
const image = isImagePath(real);
|
|
64
|
+
const max = image ? limits.maxImageBytes : limits.maxFileBytes;
|
|
65
|
+
if (stat.size > max)
|
|
66
|
+
return { ok: false, name, reason: '超过大小限制' };
|
|
67
|
+
return { ok: true, path: real, name, bytes: stat.size, image };
|
|
68
|
+
}
|
|
@@ -10,14 +10,14 @@
|
|
|
10
10
|
import type { CardAction, FeishuCard, InboundMessage } from './types.js';
|
|
11
11
|
export interface CreateMessageInput {
|
|
12
12
|
readonly chatId: string;
|
|
13
|
-
readonly msgType: 'text' | 'interactive';
|
|
13
|
+
readonly msgType: 'text' | 'interactive' | 'file' | 'image';
|
|
14
14
|
/** Serialized `msg_type` content (JSON string). */
|
|
15
15
|
readonly content: string;
|
|
16
16
|
readonly uuid?: string;
|
|
17
17
|
}
|
|
18
18
|
export interface ReplyMessageInput {
|
|
19
19
|
readonly messageId: string;
|
|
20
|
-
readonly msgType: 'text' | 'interactive';
|
|
20
|
+
readonly msgType: 'text' | 'interactive' | 'file' | 'image';
|
|
21
21
|
readonly content: string;
|
|
22
22
|
/** Post the reply inside the message's topic (`reply_in_thread`). */
|
|
23
23
|
readonly replyInThread: boolean;
|
|
@@ -34,6 +34,10 @@ export interface FeishuApi {
|
|
|
34
34
|
addReaction(messageId: string, emojiType: string): Promise<string>;
|
|
35
35
|
/** Remove one reaction previously added by this bot. */
|
|
36
36
|
removeReaction(messageId: string, reactionId: string): Promise<void>;
|
|
37
|
+
/** Upload one file and return its Feishu file_key. */
|
|
38
|
+
uploadFile(fileName: string, bytes: Uint8Array): Promise<string>;
|
|
39
|
+
/** Upload one image message and return its Feishu image_key. */
|
|
40
|
+
uploadImage(bytes: Uint8Array): Promise<string>;
|
|
37
41
|
/** Download a message resource (image/file), enforcing a byte limit. */
|
|
38
42
|
downloadResource(messageId: string, fileKey: string, type: 'image' | 'file', maxBytes: number): Promise<{
|
|
39
43
|
readonly bytes: Uint8Array;
|
package/dist/transport/lark.d.ts
CHANGED
|
@@ -42,6 +42,8 @@ export declare class LarkFeishuApi implements FeishuApi {
|
|
|
42
42
|
}>;
|
|
43
43
|
addReaction(messageId: string, emojiType: string): Promise<string>;
|
|
44
44
|
removeReaction(messageId: string, reactionId: string): Promise<void>;
|
|
45
|
+
uploadFile(fileName: string, bytes: Uint8Array): Promise<string>;
|
|
46
|
+
uploadImage(bytes: Uint8Array): Promise<string>;
|
|
45
47
|
downloadResource(messageId: string, fileKey: string, type: 'image' | 'file', maxBytes: number): Promise<{
|
|
46
48
|
readonly bytes: Uint8Array;
|
|
47
49
|
readonly contentType?: string;
|
package/dist/transport/lark.js
CHANGED
|
@@ -166,6 +166,35 @@ export class LarkFeishuApi {
|
|
|
166
166
|
}));
|
|
167
167
|
this.assertOk(response, 'im.v1.message.reaction.delete');
|
|
168
168
|
}
|
|
169
|
+
async uploadFile(fileName, bytes) {
|
|
170
|
+
const response = (await this.client.im.v1.file.create({
|
|
171
|
+
data: {
|
|
172
|
+
file_type: 'stream',
|
|
173
|
+
file_name: fileName,
|
|
174
|
+
file: Buffer.from(bytes),
|
|
175
|
+
},
|
|
176
|
+
}));
|
|
177
|
+
this.assertOk(response, 'im.v1.file.create');
|
|
178
|
+
const fileKey = response.data?.file_key;
|
|
179
|
+
if (fileKey === undefined || fileKey.length === 0) {
|
|
180
|
+
throw new FeishuApiError('feishu im.v1.file.create returned no file_key', -1);
|
|
181
|
+
}
|
|
182
|
+
return fileKey;
|
|
183
|
+
}
|
|
184
|
+
async uploadImage(bytes) {
|
|
185
|
+
const response = (await this.client.im.v1.image.create({
|
|
186
|
+
data: {
|
|
187
|
+
image_type: 'message',
|
|
188
|
+
image: Buffer.from(bytes),
|
|
189
|
+
},
|
|
190
|
+
}));
|
|
191
|
+
this.assertOk(response, 'im.v1.image.create');
|
|
192
|
+
const imageKey = response.data?.image_key;
|
|
193
|
+
if (imageKey === undefined || imageKey.length === 0) {
|
|
194
|
+
throw new FeishuApiError('feishu im.v1.image.create returned no image_key', -1);
|
|
195
|
+
}
|
|
196
|
+
return imageKey;
|
|
197
|
+
}
|
|
169
198
|
async downloadResource(messageId, fileKey, type, maxBytes) {
|
|
170
199
|
const response = await this.client.im.v1.messageResource.get({
|
|
171
200
|
params: { type },
|
|
@@ -88,7 +88,9 @@ export function normalizeMessageEvent(raw, botOpenId) {
|
|
|
88
88
|
if (senderType !== 'user')
|
|
89
89
|
return undefined;
|
|
90
90
|
const message = raw.message;
|
|
91
|
-
|
|
91
|
+
// Only a real thread_id makes a topic. In p2p, root_id can equal the
|
|
92
|
+
// message id; treating it as threadId produced bogus "topic replies".
|
|
93
|
+
const threadId = firstNonEmpty(message.thread_id);
|
|
92
94
|
const rootId = firstNonEmpty(message.root_id);
|
|
93
95
|
const mentions = (message.mentions ?? [])
|
|
94
96
|
.map((mention) => mention.id?.open_id)
|
|
@@ -34,6 +34,8 @@ export declare class ReliableTransport implements Transport {
|
|
|
34
34
|
sendCard(chatId: string, card: FeishuCard, options?: SendOptions): Promise<SentMessage>;
|
|
35
35
|
addReaction(messageId: string, emojiType: string): Promise<string>;
|
|
36
36
|
removeReaction(messageId: string, reactionId: string): Promise<void>;
|
|
37
|
+
uploadFile(fileName: string, bytes: Uint8Array): Promise<string>;
|
|
38
|
+
uploadImage(bytes: Uint8Array): Promise<string>;
|
|
37
39
|
downloadResource(messageId: string, fileKey: string, type: 'image' | 'file', maxBytes: number): Promise<{
|
|
38
40
|
readonly bytes: Uint8Array;
|
|
39
41
|
readonly contentType?: string;
|
|
@@ -106,6 +106,12 @@ export class ReliableTransport {
|
|
|
106
106
|
removeReaction(messageId, reactionId) {
|
|
107
107
|
return this.api.removeReaction(messageId, reactionId);
|
|
108
108
|
}
|
|
109
|
+
uploadFile(fileName, bytes) {
|
|
110
|
+
return this.api.uploadFile(fileName, bytes);
|
|
111
|
+
}
|
|
112
|
+
uploadImage(bytes) {
|
|
113
|
+
return this.api.uploadImage(bytes);
|
|
114
|
+
}
|
|
109
115
|
downloadResource(messageId, fileKey, type, maxBytes) {
|
|
110
116
|
return this.api.downloadResource(messageId, fileKey, type, maxBytes);
|
|
111
117
|
}
|
|
@@ -115,8 +121,18 @@ export class ReliableTransport {
|
|
|
115
121
|
}
|
|
116
122
|
async reply(chatId, messageId, payload, options = {}) {
|
|
117
123
|
const uuid = options.uuid ?? this.uuidFactory();
|
|
118
|
-
const msgType = payload.kind === 'text'
|
|
119
|
-
|
|
124
|
+
const msgType = payload.kind === 'text'
|
|
125
|
+
? 'text'
|
|
126
|
+
: payload.kind === 'card'
|
|
127
|
+
? 'interactive'
|
|
128
|
+
: payload.kind;
|
|
129
|
+
const content = payload.kind === 'text'
|
|
130
|
+
? textPayload(payload.text)
|
|
131
|
+
: payload.kind === 'card'
|
|
132
|
+
? cardPayload(payload.card)
|
|
133
|
+
: payload.kind === 'file'
|
|
134
|
+
? JSON.stringify({ file_key: payload.fileKey })
|
|
135
|
+
: JSON.stringify({ image_key: payload.imageKey });
|
|
120
136
|
const replyInThread = options.threadId !== undefined && options.threadId.length > 0;
|
|
121
137
|
const kind = payload.kind === 'card' ? '卡片' : '消息';
|
|
122
138
|
try {
|
|
@@ -109,6 +109,13 @@ export type ReplyPayload = {
|
|
|
109
109
|
} | {
|
|
110
110
|
readonly kind: 'card';
|
|
111
111
|
readonly card: FeishuCard;
|
|
112
|
+
} | {
|
|
113
|
+
readonly kind: 'file';
|
|
114
|
+
readonly fileKey: string;
|
|
115
|
+
readonly fileName: string;
|
|
116
|
+
} | {
|
|
117
|
+
readonly kind: 'image';
|
|
118
|
+
readonly imageKey: string;
|
|
112
119
|
};
|
|
113
120
|
export interface SendOptions {
|
|
114
121
|
/** Idempotency key; the same key may succeed only once per hour on Feishu. */
|
|
@@ -134,6 +141,10 @@ export interface Transport {
|
|
|
134
141
|
addReaction(messageId: string, emojiType: string): Promise<string>;
|
|
135
142
|
/** Remove one reaction previously added by this bot. */
|
|
136
143
|
removeReaction(messageId: string, reactionId: string): Promise<void>;
|
|
144
|
+
/** Upload one file and return its Feishu file_key. */
|
|
145
|
+
uploadFile(fileName: string, bytes: Uint8Array): Promise<string>;
|
|
146
|
+
/** Upload one image message and return its Feishu image_key. */
|
|
147
|
+
uploadImage(bytes: Uint8Array): Promise<string>;
|
|
137
148
|
/** Download a message resource (image/file), enforcing a byte limit. */
|
|
138
149
|
downloadResource(messageId: string, fileKey: string, type: 'image' | 'file', maxBytes: number): Promise<{
|
|
139
150
|
readonly bytes: Uint8Array;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-caduceus",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Feishu/Lark surface plugin for DeepSeek Harness (dsh): one topic per session, SOUL persona sync, streaming process cards, /status and /doctor.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|