lark-remote 0.3.0 → 0.4.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/CHANGELOG.md +35 -0
- package/README.en.md +6 -6
- package/README.md +8 -7
- package/dist/binder.d.ts +1 -1
- package/dist/binder.js +1 -1
- package/dist/bridge/final-usage.js +2 -2
- package/dist/bridge/inbound-media.d.ts +13 -20
- package/dist/bridge/inbound-media.js +107 -91
- package/dist/bridge/index.d.ts +6 -6
- package/dist/bridge/index.js +81 -87
- package/dist/cli.js +5 -1
- package/dist/clone.d.ts +1 -1
- package/dist/common/fs.js +1 -1
- package/dist/config/dir.d.ts +9 -0
- package/dist/config/dir.js +69 -0
- package/dist/config/index.d.ts +22 -3
- package/dist/config/index.js +26 -4
- package/dist/connector/index.d.ts +28 -1
- package/dist/connector/index.js +155 -31
- package/dist/error-classification.d.ts +7 -0
- package/dist/error-classification.js +37 -1
- package/dist/inbound/event.d.ts +41 -0
- package/dist/inbound/event.js +11 -0
- package/dist/inbound/placeholder.d.ts +26 -0
- package/dist/inbound/placeholder.js +149 -0
- package/dist/inbound/prompt.d.ts +20 -0
- package/dist/inbound/prompt.js +47 -0
- package/dist/inbound/receipt.d.ts +28 -0
- package/dist/inbound/receipt.js +62 -0
- package/dist/inbound/turn-assembler.d.ts +63 -0
- package/dist/inbound/turn-assembler.js +233 -0
- package/dist/inbound/turn.d.ts +48 -0
- package/dist/inbound/turn.js +10 -0
- package/dist/index.js +108 -73
- package/dist/platform/command.js +1 -1
- package/dist/platform/sleep-blocker.d.ts +17 -0
- package/dist/platform/sleep-blocker.js +73 -0
- package/dist/platform/terminator-win32.js +1 -1
- package/dist/restart.d.ts +4 -4
- package/dist/restart.js +5 -5
- package/dist/router/config/kimi.js +4 -3
- package/dist/router/config/opencode.d.ts +1 -1
- package/dist/router/config/opencode.js +11 -0
- package/dist/router/index.d.ts +17 -0
- package/dist/router/index.js +53 -15
- package/dist/runner/common/acp/base-acp-runner.js +7 -2
- package/dist/runner/common/acp/base-translator.d.ts +19 -0
- package/dist/runner/common/acp/base-translator.js +82 -0
- package/dist/runner/common/acp/protocol-types.d.ts +16 -2
- package/dist/runner/common/acp/protocol-types.js +3 -0
- package/dist/runner/common/connection-based-runner.js +6 -2
- package/dist/runner/kimi/acp/runner.d.ts +1 -1
- package/dist/runner/kimi/acp/runner.js +1 -1
- package/dist/runner/kimi/acp/translator.js +60 -4
- package/dist/runner/opencode/acp/runner.d.ts +1 -1
- package/dist/session/session-store.d.ts +1 -1
- package/dist/session/session-store.js +1 -1
- package/dist/workspace/index.d.ts +1 -1
- package/dist/workspace/index.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 装配器事件(设计依据见 `docs/zh/architecture/inbound-unified-input-design.md`)。
|
|
3
|
+
*
|
|
4
|
+
* 设计文档早期的媒体事件形态是「下载完成后的 attachments/failures」。实际链路里
|
|
5
|
+
* 下载是异步的(`index.ts` 先过 owner/enabled 闸门再下载),若等下载完成才投递事件,
|
|
6
|
+
* 「先图后文」在慢下载下会先提交一个只含文本的 turn(正是要修的症状)。因此媒体事件
|
|
7
|
+
* 携带一个 **outcome promise**:装配器立刻知道「本 turn 还有 in-flight 下载」,
|
|
8
|
+
* 窗口到期后等它落定再 commit(§5.2)。
|
|
9
|
+
*/
|
|
10
|
+
import type { PlaceholderKind } from './placeholder.js';
|
|
11
|
+
import type { MediaOutcome } from './turn.js';
|
|
12
|
+
export interface InboundEventBase {
|
|
13
|
+
userId: string;
|
|
14
|
+
chatId: string;
|
|
15
|
+
messageId: string;
|
|
16
|
+
replyToMessageId?: string;
|
|
17
|
+
/** 飞书原始 msg_type(`text` / `image` / `post` / `merge_forward` …)。 */
|
|
18
|
+
rawContentType: string;
|
|
19
|
+
}
|
|
20
|
+
export interface TextInboundEvent extends InboundEventBase {
|
|
21
|
+
kind: 'text';
|
|
22
|
+
/** 已剥离结构占位符的文本(可为空串:整条消息都是占位符)。 */
|
|
23
|
+
text: string;
|
|
24
|
+
/** 本条消息命中的占位符种类,用于「不支持」回执。 */
|
|
25
|
+
placeholders: PlaceholderKind[];
|
|
26
|
+
/** 未知标签(未来 SDK 新增),上层负责 warn。 */
|
|
27
|
+
unknownTags: string[];
|
|
28
|
+
}
|
|
29
|
+
export interface MediaInboundEvent extends InboundEventBase {
|
|
30
|
+
kind: 'media';
|
|
31
|
+
/** 下载 + 落盘结果;装配器在窗口到期后等它落定才 commit。 */
|
|
32
|
+
outcome: Promise<MediaOutcome>;
|
|
33
|
+
}
|
|
34
|
+
export interface RejectedInboundEvent extends InboundEventBase {
|
|
35
|
+
kind: 'rejected';
|
|
36
|
+
/** 面向人的说明(如「入站媒体保存已关闭」)。 */
|
|
37
|
+
reason: string;
|
|
38
|
+
/** 拒绝项的 kind(默认 `unknown`)。 */
|
|
39
|
+
rejectedKind?: string;
|
|
40
|
+
}
|
|
41
|
+
export type InboundEvent = TextInboundEvent | MediaInboundEvent | RejectedInboundEvent;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 装配器事件(设计依据见 `docs/zh/architecture/inbound-unified-input-design.md`)。
|
|
3
|
+
*
|
|
4
|
+
* 设计文档早期的媒体事件形态是「下载完成后的 attachments/failures」。实际链路里
|
|
5
|
+
* 下载是异步的(`index.ts` 先过 owner/enabled 闸门再下载),若等下载完成才投递事件,
|
|
6
|
+
* 「先图后文」在慢下载下会先提交一个只含文本的 turn(正是要修的症状)。因此媒体事件
|
|
7
|
+
* 携带一个 **outcome promise**:装配器立刻知道「本 turn 还有 in-flight 下载」,
|
|
8
|
+
* 窗口到期后等它落定再 commit(§5.2)。
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=event.js.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 入站消息结构占位符识别与剥离(纯函数,无 IO,便于表驱动测试)。
|
|
3
|
+
*
|
|
4
|
+
* 背景(P0 事故 2026-09-15):`@larksuite/channel` 把非文本消息渲染成结构占位符,
|
|
5
|
+
* 富文本(`post`)里的图片是 `` —— 首字符恰好是 `!`,于是被
|
|
6
|
+
* `src/router/index.ts` 的裸前缀判定当成 bash 命令执行了整段消息。
|
|
7
|
+
*
|
|
8
|
+
* 规则来源:`docs/zh/architecture/inbound-message-matrix.md` §1 的 22 种 message_type
|
|
9
|
+
* 实测渲染串。判定顺序**先清洗后判语义**:任何前缀判定都必须发生在剥离之后。
|
|
10
|
+
*
|
|
11
|
+
* 取舍(有意为之):剥离对所有消息生效,所以用户在**纯文本**消息里手打
|
|
12
|
+
* `` / `<file key="…"/>` 这类字面量也会被当成占位符剥掉。代价是极少数
|
|
13
|
+
* 「讨论本工具自身语法」的消息会丢片段;换来的是任何入口都不可能把占位符当命令
|
|
14
|
+
* (P0 事故正是 `` 的首字符 `!` 命中 bash 判定)。
|
|
15
|
+
*/
|
|
16
|
+
export type PlaceholderKind = 'image' | 'file' | 'video' | 'audio' | 'sticker' | 'share' | 'location' | 'folder' | 'vote' | 'todo' | 'meeting' | 'calendar' | 'hongbao' | 'forwarded' | 'unsupported' | 'unknown';
|
|
17
|
+
export interface StripResult {
|
|
18
|
+
/** 剥离占位符后的文本(trim 后;forwarded 块只剥标签、保留内部文本)。 */
|
|
19
|
+
clean: string;
|
|
20
|
+
/** 命中的占位符种类(去重、保序),用于 rejected 回执与排障。 */
|
|
21
|
+
kinds: PlaceholderKind[];
|
|
22
|
+
/** 未知标签(未来 SDK 新增)——必须 warn,防止再次静默漏。 */
|
|
23
|
+
unknownTags: string[];
|
|
24
|
+
}
|
|
25
|
+
export declare function isDownloadablePlaceholder(kind: PlaceholderKind): boolean;
|
|
26
|
+
export declare function stripPlaceholders(raw: string): StripResult;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 入站消息结构占位符识别与剥离(纯函数,无 IO,便于表驱动测试)。
|
|
3
|
+
*
|
|
4
|
+
* 背景(P0 事故 2026-09-15):`@larksuite/channel` 把非文本消息渲染成结构占位符,
|
|
5
|
+
* 富文本(`post`)里的图片是 `` —— 首字符恰好是 `!`,于是被
|
|
6
|
+
* `src/router/index.ts` 的裸前缀判定当成 bash 命令执行了整段消息。
|
|
7
|
+
*
|
|
8
|
+
* 规则来源:`docs/zh/architecture/inbound-message-matrix.md` §1 的 22 种 message_type
|
|
9
|
+
* 实测渲染串。判定顺序**先清洗后判语义**:任何前缀判定都必须发生在剥离之后。
|
|
10
|
+
*
|
|
11
|
+
* 取舍(有意为之):剥离对所有消息生效,所以用户在**纯文本**消息里手打
|
|
12
|
+
* `` / `<file key="…"/>` 这类字面量也会被当成占位符剥掉。代价是极少数
|
|
13
|
+
* 「讨论本工具自身语法」的消息会丢片段;换来的是任何入口都不可能把占位符当命令
|
|
14
|
+
* (P0 事故正是 `` 的首字符 `!` 命中 bash 判定)。
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* 可下载资源类占位符:这些占位符对应的资源会由媒体通道下载落盘,
|
|
18
|
+
* 因此文本侧不把它们记成「不支持」(否则图文混排会双重报错)。
|
|
19
|
+
*/
|
|
20
|
+
const DOWNLOADABLE_KINDS = new Set([
|
|
21
|
+
'image',
|
|
22
|
+
'file',
|
|
23
|
+
'video',
|
|
24
|
+
'audio',
|
|
25
|
+
'sticker',
|
|
26
|
+
]);
|
|
27
|
+
export function isDownloadablePlaceholder(kind) {
|
|
28
|
+
return DOWNLOADABLE_KINDS.has(kind);
|
|
29
|
+
}
|
|
30
|
+
/** 协议块哨兵:`<attachments>` 是本项目自己生成的 prompt 附件块,剥离时必须原样保留。 */
|
|
31
|
+
const ATTACHMENT_BLOCK_RE = /<attachments>[\s\S]*?<\/attachments>/g;
|
|
32
|
+
const SENTINEL_PREFIX = '\u0000lark-inbound-att-';
|
|
33
|
+
const SENTINEL_SUFFIX = '\u0000';
|
|
34
|
+
/** 自闭合标签 → kind(`<file key="…" name="…"/>` 等,属性可缺省)。 */
|
|
35
|
+
const SELF_CLOSING_RULES = [
|
|
36
|
+
[/<file(?:\s[^<>]*)?\/>/g, 'file'],
|
|
37
|
+
[/<video(?:\s[^<>]*)?\/>/g, 'video'],
|
|
38
|
+
[/<audio(?:\s[^<>]*)?\/>/g, 'audio'],
|
|
39
|
+
[/<sticker(?:\s[^<>]*)?\/>/g, 'sticker'],
|
|
40
|
+
[/<group_card(?:\s[^<>]*)?\/>/g, 'share'],
|
|
41
|
+
[/<contact_card(?:\s[^<>]*)?\/>/g, 'share'],
|
|
42
|
+
[/<location(?:\s[^<>]*)?\/>/g, 'location'],
|
|
43
|
+
[/<folder(?:\s[^<>]*)?\/>/g, 'folder'],
|
|
44
|
+
[/<hongbao(?:\s[^<>]*)?\/>/g, 'hongbao'],
|
|
45
|
+
];
|
|
46
|
+
/** 整块剥离(含内部内容)的容器标签:内容无用户语义,只作噪声。 */
|
|
47
|
+
const BLOCK_RULES = [
|
|
48
|
+
[/<vote>[\s\S]*?<\/vote>/g, 'vote'],
|
|
49
|
+
[/<todo>[\s\S]*?<\/todo>/g, 'todo'],
|
|
50
|
+
[/<meeting>[\s\S]*?<\/meeting>/g, 'meeting'],
|
|
51
|
+
[/<calendar_invite>[\s\S]*?<\/calendar_invite>/g, 'calendar'],
|
|
52
|
+
[/<calendar_share>[\s\S]*?<\/calendar_share>/g, 'calendar'],
|
|
53
|
+
[/<calendar>[\s\S]*?<\/calendar>/g, 'calendar'],
|
|
54
|
+
];
|
|
55
|
+
/** 图片渲染串:`` / `![]()`,富文本内嵌图与纯 image 消息同形。 */
|
|
56
|
+
const IMAGE_MD_RE = /!\[(?:image)?\]\([^)]*\)/g;
|
|
57
|
+
/** 合并转发:只剥标签、保留内部文本(子消息文本有语义)。 */
|
|
58
|
+
const FORWARDED_TAG_RE = /<\/?forwarded_messages\s*\/?>/g;
|
|
59
|
+
/** 提及标签:保留内部显示名,不作为占位符上报。 */
|
|
60
|
+
const AT_TAG_RE = /<at(?:\s[^<>]*)?>([\s\S]*?)<\/at>/g;
|
|
61
|
+
/** converter 的降级态(缺 key / 未识别类型 —— 都是纯噪声)。 */
|
|
62
|
+
const BRACKET_FALLBACK_RE = /\[(?:unsupported message|rich text message|interactive card|image|file|video|audio|sticker|system message|folder)\]/g;
|
|
63
|
+
/**
|
|
64
|
+
* 未知标签兜底:只在「有属性」或「成对闭合」时才剥离。
|
|
65
|
+
* 要求有属性可显著降低误伤——用户正文里的 `<3`、`a < b`、`<div>` 不会被吃掉。
|
|
66
|
+
*/
|
|
67
|
+
const UNKNOWN_SELF_CLOSING_RE = /<([a-z][a-z0-9_]*)\s[^<>]*\/>/g;
|
|
68
|
+
const UNKNOWN_PAIRED_RE = /<([a-z][a-z0-9_]*)(?:\s[^<>]*)?>[\s\S]*?<\/\1>/g;
|
|
69
|
+
/** 剥离后遗留的连续空行折叠为单空行(属剥离产物,不算对用户文本加工)。 */
|
|
70
|
+
function collapseBlankLines(text) {
|
|
71
|
+
return text.replace(/\n{3,}/g, '\n\n');
|
|
72
|
+
}
|
|
73
|
+
export function stripPlaceholders(raw) {
|
|
74
|
+
const kinds = [];
|
|
75
|
+
const unknownTags = [];
|
|
76
|
+
/** 是否真的剥离过东西:没有命中时不做任何文本加工(保持用户原文)。 */
|
|
77
|
+
let stripped = false;
|
|
78
|
+
// 1. 先把本项目自己的 <attachments> 协议块摘出去,最后原样放回。
|
|
79
|
+
const protectedBlocks = [];
|
|
80
|
+
let working = raw.replace(ATTACHMENT_BLOCK_RE, (match) => {
|
|
81
|
+
const token = `${SENTINEL_PREFIX}${protectedBlocks.length}${SENTINEL_SUFFIX}`;
|
|
82
|
+
protectedBlocks.push(match);
|
|
83
|
+
return token;
|
|
84
|
+
});
|
|
85
|
+
const hit = (kind) => {
|
|
86
|
+
stripped = true;
|
|
87
|
+
if (!kinds.includes(kind))
|
|
88
|
+
kinds.push(kind);
|
|
89
|
+
};
|
|
90
|
+
// 2. 图片渲染串(必须在 forwarded 标签剥离之前,覆盖块内子消息的图)。
|
|
91
|
+
working = working.replace(IMAGE_MD_RE, () => {
|
|
92
|
+
hit('image');
|
|
93
|
+
return '';
|
|
94
|
+
});
|
|
95
|
+
// 3. 已知自闭合标签。
|
|
96
|
+
for (const [re, kind] of SELF_CLOSING_RULES) {
|
|
97
|
+
working = working.replace(re, () => {
|
|
98
|
+
hit(kind);
|
|
99
|
+
return '';
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
// 4. 已知容器块(整块剥离)。
|
|
103
|
+
for (const [re, kind] of BLOCK_RULES) {
|
|
104
|
+
working = working.replace(re, () => {
|
|
105
|
+
hit(kind);
|
|
106
|
+
return '';
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
// 5. 合并转发:只剥标签。
|
|
110
|
+
working = working.replace(FORWARDED_TAG_RE, () => {
|
|
111
|
+
hit('forwarded');
|
|
112
|
+
return '';
|
|
113
|
+
});
|
|
114
|
+
// 6. 提及标签:保留显示名。
|
|
115
|
+
working = working.replace(AT_TAG_RE, (_match, inner) => {
|
|
116
|
+
stripped = true;
|
|
117
|
+
return inner;
|
|
118
|
+
});
|
|
119
|
+
// 7. converter 降级态。
|
|
120
|
+
working = working.replace(BRACKET_FALLBACK_RE, () => {
|
|
121
|
+
hit('unsupported');
|
|
122
|
+
return '';
|
|
123
|
+
});
|
|
124
|
+
// 8. 未知标签兜底(未来 SDK 新增类型):剥离 + 记录 tag 名。
|
|
125
|
+
working = working.replace(UNKNOWN_SELF_CLOSING_RE, (match, tag) => {
|
|
126
|
+
hit('unknown');
|
|
127
|
+
if (!unknownTags.includes(tag))
|
|
128
|
+
unknownTags.push(tag);
|
|
129
|
+
return '';
|
|
130
|
+
});
|
|
131
|
+
working = working.replace(UNKNOWN_PAIRED_RE, (match, tag) => {
|
|
132
|
+
hit('unknown');
|
|
133
|
+
if (!unknownTags.includes(tag))
|
|
134
|
+
unknownTags.push(tag);
|
|
135
|
+
return '';
|
|
136
|
+
});
|
|
137
|
+
// 9. 放回协议块。
|
|
138
|
+
if (protectedBlocks.length > 0) {
|
|
139
|
+
stripped = true;
|
|
140
|
+
for (let i = 0; i < protectedBlocks.length; i += 1) {
|
|
141
|
+
working = working.split(`${SENTINEL_PREFIX}${i}${SENTINEL_SUFFIX}`).join(protectedBlocks[i]);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// 未命中任何占位符时**不做文本加工**(保留原文的连续空行等),
|
|
145
|
+
// 确保 router.handle 对纯文本消息的转发与原行为逐字一致。
|
|
146
|
+
const clean = stripped ? collapseBlankLines(working).trim() : working.trim();
|
|
147
|
+
return { clean, kinds, unknownTags };
|
|
148
|
+
}
|
|
149
|
+
//# sourceMappingURL=placeholder.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* prompt 组装(结构化 `<attachments>` 标签块,设计依据见
|
|
3
|
+
* `docs/zh/architecture/inbound-unified-input-design.md`)。
|
|
4
|
+
*
|
|
5
|
+
* 输出形态:用户文本(按到达顺序、原样)+ 空行 + 附件块。
|
|
6
|
+
*
|
|
7
|
+
* <用户文本,原样>
|
|
8
|
+
*
|
|
9
|
+
* <attachments>
|
|
10
|
+
* <file path="/abs/x.png" kind="image"/>
|
|
11
|
+
* <file path="/abs/a.mp4" kind="video" duration="80.9s" name="a.mp4"/>
|
|
12
|
+
* </attachments>
|
|
13
|
+
*
|
|
14
|
+
* 约定:无附件时 prompt 就是纯文本(与旧行为兼容,缩小回归面);**绝不输出 file_key**
|
|
15
|
+
* (对 agent 无意义,且是敏感凭据)。
|
|
16
|
+
*/
|
|
17
|
+
import type { InboundTurn } from './turn.js';
|
|
18
|
+
/** 时长格式化(对齐 SDK 的 formatDuration:80.9s / 3s / 500ms)。 */
|
|
19
|
+
export declare function formatDuration(ms: number): string | undefined;
|
|
20
|
+
export declare function buildPrompt(turn: InboundTurn): string;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* prompt 组装(结构化 `<attachments>` 标签块,设计依据见
|
|
3
|
+
* `docs/zh/architecture/inbound-unified-input-design.md`)。
|
|
4
|
+
*
|
|
5
|
+
* 输出形态:用户文本(按到达顺序、原样)+ 空行 + 附件块。
|
|
6
|
+
*
|
|
7
|
+
* <用户文本,原样>
|
|
8
|
+
*
|
|
9
|
+
* <attachments>
|
|
10
|
+
* <file path="/abs/x.png" kind="image"/>
|
|
11
|
+
* <file path="/abs/a.mp4" kind="video" duration="80.9s" name="a.mp4"/>
|
|
12
|
+
* </attachments>
|
|
13
|
+
*
|
|
14
|
+
* 约定:无附件时 prompt 就是纯文本(与旧行为兼容,缩小回归面);**绝不输出 file_key**
|
|
15
|
+
* (对 agent 无意义,且是敏感凭据)。
|
|
16
|
+
*/
|
|
17
|
+
/** 属性值转义:只处理会破坏引号包裹的字符。 */
|
|
18
|
+
function escapeAttr(value) {
|
|
19
|
+
return value.replace(/"/g, '"');
|
|
20
|
+
}
|
|
21
|
+
/** 时长格式化(对齐 SDK 的 formatDuration:80.9s / 3s / 500ms)。 */
|
|
22
|
+
export function formatDuration(ms) {
|
|
23
|
+
if (!Number.isFinite(ms) || ms < 0)
|
|
24
|
+
return undefined;
|
|
25
|
+
if (ms < 1000)
|
|
26
|
+
return `${Math.round(ms)}ms`;
|
|
27
|
+
if (ms % 1000 === 0)
|
|
28
|
+
return `${ms / 1000}s`;
|
|
29
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
30
|
+
}
|
|
31
|
+
export function buildPrompt(turn) {
|
|
32
|
+
const text = turn.texts.join('\n');
|
|
33
|
+
if (turn.attachments.length === 0)
|
|
34
|
+
return text;
|
|
35
|
+
const lines = turn.attachments.map((att) => {
|
|
36
|
+
let line = ` <file path="${escapeAttr(att.path)}" kind="${att.kind}"`;
|
|
37
|
+
const duration = att.durationMs === undefined ? undefined : formatDuration(att.durationMs);
|
|
38
|
+
if (duration)
|
|
39
|
+
line += ` duration="${duration}"`;
|
|
40
|
+
if (att.originalName)
|
|
41
|
+
line += ` name="${escapeAttr(att.originalName)}"`;
|
|
42
|
+
return `${line}/>`;
|
|
43
|
+
});
|
|
44
|
+
const block = `<attachments>\n${lines.join('\n')}\n</attachments>`;
|
|
45
|
+
return text === '' ? block : `${text}\n\n${block}`;
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=prompt.js.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 入站回执文案(设计依据见 `docs/zh/architecture/inbound-unified-input-design.md`)。
|
|
3
|
+
*
|
|
4
|
+
* 两类回执合并成一条:
|
|
5
|
+
* 1. 无文本纯附件(决策 2):不自动起 turn,只回已保存路径 + 引导语;
|
|
6
|
+
* 2. 拿不到的东西(不支持类型 / 超限 / 下载失败 / 媒体保存已关闭):明确告知,
|
|
7
|
+
* 而不是把占位符塞给 agent。
|
|
8
|
+
*
|
|
9
|
+
* 用户可见文案指代本程序时只能写 `lark-remote`(AGENTS.md 命名纪律)。
|
|
10
|
+
*/
|
|
11
|
+
import type { PlaceholderKind } from './placeholder.js';
|
|
12
|
+
import type { RejectedItem } from './turn.js';
|
|
13
|
+
/** 单条回执最多列出的文件数;超出折叠为 "… 等 N 个文件"(沿用旧提示口径)。 */
|
|
14
|
+
export declare const MAX_PATHS_IN_RECEIPT = 10;
|
|
15
|
+
/** 「不支持」回执句式(§5.7 表:不支持类型统一「暂不支持 X」)。 */
|
|
16
|
+
export declare function unsupportedReason(kind: PlaceholderKind): string;
|
|
17
|
+
export interface ReceiptInput {
|
|
18
|
+
/** 本次已落盘的绝对路径(仅无文本场景展示)。 */
|
|
19
|
+
saved: string[];
|
|
20
|
+
/** 未处理项(不支持 / 超限 / 失败)。 */
|
|
21
|
+
rejected: RejectedItem[];
|
|
22
|
+
/** 本次没有用户文本:附上「说一句话我就开始处理」引导。 */
|
|
23
|
+
startWorkHint: boolean;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* 生成回执文案;没有任何需要告知的内容时返回 undefined(调用方不发消息)。
|
|
27
|
+
*/
|
|
28
|
+
export declare function buildReceipt(input: ReceiptInput): string | undefined;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 入站回执文案(设计依据见 `docs/zh/architecture/inbound-unified-input-design.md`)。
|
|
3
|
+
*
|
|
4
|
+
* 两类回执合并成一条:
|
|
5
|
+
* 1. 无文本纯附件(决策 2):不自动起 turn,只回已保存路径 + 引导语;
|
|
6
|
+
* 2. 拿不到的东西(不支持类型 / 超限 / 下载失败 / 媒体保存已关闭):明确告知,
|
|
7
|
+
* 而不是把占位符塞给 agent。
|
|
8
|
+
*
|
|
9
|
+
* 用户可见文案指代本程序时只能写 `lark-remote`(AGENTS.md 命名纪律)。
|
|
10
|
+
*/
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
/** 单条回执最多列出的文件数;超出折叠为 "… 等 N 个文件"(沿用旧提示口径)。 */
|
|
13
|
+
export const MAX_PATHS_IN_RECEIPT = 10;
|
|
14
|
+
/** 占位符种类 → 面向人的名称。 */
|
|
15
|
+
const PLACEHOLDER_LABELS = {
|
|
16
|
+
image: '图片',
|
|
17
|
+
file: '文件',
|
|
18
|
+
video: '视频',
|
|
19
|
+
audio: '语音',
|
|
20
|
+
sticker: '表情',
|
|
21
|
+
share: '名片/群分享',
|
|
22
|
+
location: '位置',
|
|
23
|
+
folder: '文件夹',
|
|
24
|
+
vote: '投票',
|
|
25
|
+
todo: '待办',
|
|
26
|
+
meeting: '视频会议',
|
|
27
|
+
calendar: '日程',
|
|
28
|
+
hongbao: '红包',
|
|
29
|
+
forwarded: '合并转发',
|
|
30
|
+
unsupported: '此类',
|
|
31
|
+
unknown: '未知类型',
|
|
32
|
+
};
|
|
33
|
+
/** 「不支持」回执句式(§5.7 表:不支持类型统一「暂不支持 X」)。 */
|
|
34
|
+
export function unsupportedReason(kind) {
|
|
35
|
+
return `暂不支持${PLACEHOLDER_LABELS[kind]}消息`;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* 生成回执文案;没有任何需要告知的内容时返回 undefined(调用方不发消息)。
|
|
39
|
+
*/
|
|
40
|
+
export function buildReceipt(input) {
|
|
41
|
+
const parts = [];
|
|
42
|
+
if (input.saved.length > 0) {
|
|
43
|
+
const lines = input.saved.slice(0, MAX_PATHS_IN_RECEIPT).map((p) => `- ${p}`);
|
|
44
|
+
const overflow = input.saved.length > MAX_PATHS_IN_RECEIPT ? `\n… 等 ${input.saved.length} 个文件` : '';
|
|
45
|
+
parts.push(`📎 已保存 ${input.saved.length} 个文件:\n${lines.join('\n')}${overflow}`);
|
|
46
|
+
}
|
|
47
|
+
if (input.rejected.length > 0) {
|
|
48
|
+
parts.push(`⚠️ ${input.rejected.map((r) => r.reason).join(';')}`);
|
|
49
|
+
}
|
|
50
|
+
if (input.startWorkHint && input.saved.length > 0) {
|
|
51
|
+
// 保存目录去重(跨分钟时可能落在两个目录,此时全部列出)。
|
|
52
|
+
const dirs = [...new Set(input.saved.map((p) => path.dirname(p)))];
|
|
53
|
+
const example = dirs.length === 1
|
|
54
|
+
? `请处理 ${dirs[0]} 下的文件`
|
|
55
|
+
: `请处理以下目录下的文件:${dirs.join('、')}`;
|
|
56
|
+
parts.push(`💡 说一句话我就开始处理,例如:${example}`);
|
|
57
|
+
}
|
|
58
|
+
if (parts.length === 0)
|
|
59
|
+
return undefined;
|
|
60
|
+
return parts.join('\n');
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=receipt.js.map
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* InboundTurnAssembler:统一静默期窗口 + 合并 + 下载等待 + commit。
|
|
3
|
+
*
|
|
4
|
+
* 设计依据:`docs/zh/architecture/inbound-unified-input-design.md` §4/§5.2。
|
|
5
|
+
*
|
|
6
|
+
* 规则:
|
|
7
|
+
* - 任何事件到达 → **重置** 700ms 定时器(按 `userId:chatId` 分桶),保证图/文任意顺序等价;
|
|
8
|
+
* - 定时器到点 → 若本 turn 仍有 in-flight 下载 → 等待,落定后再 commit;
|
|
9
|
+
* - 下载失败/超时 → 该项进 `rejected`,然后照常 commit;
|
|
10
|
+
* - commit → 构造 prompt(`prompt.ts`)→ `onCommit`;
|
|
11
|
+
* - **无文本且只有附件 → 不 commit**(决策 2),只发 `onReceipt` 回执;
|
|
12
|
+
* - 命令消息由调用方 `flush()` 强制提交,不等窗口。
|
|
13
|
+
*/
|
|
14
|
+
import type { InboundEvent } from './event.js';
|
|
15
|
+
import { type CommitReason, type InboundTurn } from './turn.js';
|
|
16
|
+
/** 统一静默期窗口(ms)——决策 1:不做「纯文本零延迟」快路径。 */
|
|
17
|
+
export declare const DEFAULT_TURN_WINDOW_MS = 700;
|
|
18
|
+
export interface AssemblerContext {
|
|
19
|
+
userId: string;
|
|
20
|
+
chatId: string;
|
|
21
|
+
messageId: string;
|
|
22
|
+
}
|
|
23
|
+
export interface AssemblerDeps {
|
|
24
|
+
/** 静默期窗口(毫秒),默认 700。 */
|
|
25
|
+
windowMs?: number;
|
|
26
|
+
/** 定时器注入(便于 vitest fake timers / 单元测试确定性)。 */
|
|
27
|
+
setTimer?: (fn: () => void, ms: number) => unknown;
|
|
28
|
+
clearTimer?: (handle: unknown) => void;
|
|
29
|
+
/** commit 回调:组装完成的 turn + prompt 交给上层(进 agent)。 */
|
|
30
|
+
onCommit: (turn: InboundTurn, prompt: string) => void | Promise<void>;
|
|
31
|
+
/** 回执回调:面向用户的提示(无文本纯附件 / 有 rejected 项)。 */
|
|
32
|
+
onReceipt?: (ctx: AssemblerContext, text: string) => void | Promise<void>;
|
|
33
|
+
}
|
|
34
|
+
export declare class InboundTurnAssembler {
|
|
35
|
+
private readonly deps;
|
|
36
|
+
private readonly buckets;
|
|
37
|
+
private readonly windowMs;
|
|
38
|
+
private readonly setTimer;
|
|
39
|
+
private readonly clearTimer;
|
|
40
|
+
private disposed;
|
|
41
|
+
constructor(deps: AssemblerDeps);
|
|
42
|
+
/** 投递一个入站事件(文本 / 媒体[in-flight] / 拒绝)。 */
|
|
43
|
+
ingest(event: InboundEvent): void;
|
|
44
|
+
/**
|
|
45
|
+
* 强制提交窗口内已装配的内容(命令到达、退出前冲刷)。
|
|
46
|
+
* 仍有 in-flight 下载时先等它落定(避免丢掉附件路径)。
|
|
47
|
+
*/
|
|
48
|
+
flush(userId: string, chatId: string, reason?: CommitReason): Promise<void>;
|
|
49
|
+
/** 冲刷全部窗口(/exit、/restart 干净退出前调用)。 */
|
|
50
|
+
flushAll(reason?: CommitReason): Promise<void>;
|
|
51
|
+
/** 进程退出:丢弃定时器,避免悬挂。 */
|
|
52
|
+
dispose(): void;
|
|
53
|
+
private ensureBucket;
|
|
54
|
+
private pushRejected;
|
|
55
|
+
private armTimer;
|
|
56
|
+
/**
|
|
57
|
+
* 登记一次 in-flight 媒体下载:落定后合并结果,并在窗口已到期时触发 commit。
|
|
58
|
+
* 下载异常(含超时)记入 rejected,不阻断本 turn 的其它内容。
|
|
59
|
+
*/
|
|
60
|
+
private trackMediaOutcome;
|
|
61
|
+
private tryCommit;
|
|
62
|
+
private commit;
|
|
63
|
+
}
|