pi-remote-feishu 0.1.0
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/ARCHITECTURE.md +837 -0
- package/ARCHITECTURE.zh-CN.md +704 -0
- package/README.md +118 -0
- package/dist/attachments/mime.d.ts +11 -0
- package/dist/attachments/mime.js +27 -0
- package/dist/attachments/mime.js.map +1 -0
- package/dist/attachments/processor.d.ts +29 -0
- package/dist/attachments/processor.js +48 -0
- package/dist/attachments/processor.js.map +1 -0
- package/dist/attachments/temp-files.d.ts +16 -0
- package/dist/attachments/temp-files.js +32 -0
- package/dist/attachments/temp-files.js.map +1 -0
- package/dist/bin/pi-remote-feishu.d.ts +2 -0
- package/dist/bin/pi-remote-feishu.js +359 -0
- package/dist/bin/pi-remote-feishu.js.map +1 -0
- package/dist/bridge/card-actions.d.ts +13 -0
- package/dist/bridge/card-actions.js +156 -0
- package/dist/bridge/card-actions.js.map +1 -0
- package/dist/bridge/conversation-router.d.ts +21 -0
- package/dist/bridge/conversation-router.js +65 -0
- package/dist/bridge/conversation-router.js.map +1 -0
- package/dist/bridge/message-handler.d.ts +22 -0
- package/dist/bridge/message-handler.js +218 -0
- package/dist/bridge/message-handler.js.map +1 -0
- package/dist/bridge/message-normalizer.d.ts +17 -0
- package/dist/bridge/message-normalizer.js +81 -0
- package/dist/bridge/message-normalizer.js.map +1 -0
- package/dist/bridge/prompt-queue.d.ts +20 -0
- package/dist/bridge/prompt-queue.js +39 -0
- package/dist/bridge/prompt-queue.js.map +1 -0
- package/dist/bridge/runtime-host.d.ts +45 -0
- package/dist/bridge/runtime-host.js +133 -0
- package/dist/bridge/runtime-host.js.map +1 -0
- package/dist/bridge/session-host-manager.d.ts +59 -0
- package/dist/bridge/session-host-manager.js +143 -0
- package/dist/bridge/session-host-manager.js.map +1 -0
- package/dist/bridge/stream-renderer.d.ts +50 -0
- package/dist/bridge/stream-renderer.js +108 -0
- package/dist/bridge/stream-renderer.js.map +1 -0
- package/dist/bridge/ui-context.d.ts +19 -0
- package/dist/bridge/ui-context.js +150 -0
- package/dist/bridge/ui-context.js.map +1 -0
- package/dist/cards/common.d.ts +33 -0
- package/dist/cards/common.js +84 -0
- package/dist/cards/common.js.map +1 -0
- package/dist/cards/help.d.ts +11 -0
- package/dist/cards/help.js +31 -0
- package/dist/cards/help.js.map +1 -0
- package/dist/cards/models.d.ts +24 -0
- package/dist/cards/models.js +46 -0
- package/dist/cards/models.js.map +1 -0
- package/dist/cards/permission.d.ts +9 -0
- package/dist/cards/permission.js +17 -0
- package/dist/cards/permission.js.map +1 -0
- package/dist/cards/sessions.d.ts +11 -0
- package/dist/cards/sessions.js +54 -0
- package/dist/cards/sessions.js.map +1 -0
- package/dist/cards/status.d.ts +15 -0
- package/dist/cards/status.js +20 -0
- package/dist/cards/status.js.map +1 -0
- package/dist/cards/stop.d.ts +16 -0
- package/dist/cards/stop.js +24 -0
- package/dist/cards/stop.js.map +1 -0
- package/dist/config/load-config.d.ts +19 -0
- package/dist/config/load-config.js +51 -0
- package/dist/config/load-config.js.map +1 -0
- package/dist/config/schema.d.ts +52 -0
- package/dist/config/schema.js +260 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/extensions/index.d.ts +7 -0
- package/dist/extensions/index.js +71 -0
- package/dist/extensions/index.js.map +1 -0
- package/dist/extensions/lark-cli-guard.d.ts +12 -0
- package/dist/extensions/lark-cli-guard.js +75 -0
- package/dist/extensions/lark-cli-guard.js.map +1 -0
- package/dist/feishu/channel.d.ts +28 -0
- package/dist/feishu/channel.js +191 -0
- package/dist/feishu/channel.js.map +1 -0
- package/dist/feishu/context.d.ts +5 -0
- package/dist/feishu/context.js +19 -0
- package/dist/feishu/context.js.map +1 -0
- package/dist/feishu/webhook.d.ts +5 -0
- package/dist/feishu/webhook.js +8 -0
- package/dist/feishu/webhook.js.map +1 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/dist/store/json-store.d.ts +25 -0
- package/dist/store/json-store.js +105 -0
- package/dist/store/json-store.js.map +1 -0
- package/dist/store/store.d.ts +7 -0
- package/dist/store/store.js +15 -0
- package/dist/store/store.js.map +1 -0
- package/dist/tools/send-file-to-chat.d.ts +26 -0
- package/dist/tools/send-file-to-chat.js +85 -0
- package/dist/tools/send-file-to-chat.js.map +1 -0
- package/dist/types.d.ts +300 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +67 -0
- package/skills/lark-doc-cli/SKILL.md +56 -0
- package/skills/lark-im-readonly/SKILL.md +49 -0
package/README.md
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# pi-remote-feishu
|
|
2
|
+
|
|
3
|
+
Remote control Pi Agent via Feishu (飞书) chat. WebSocket bridge, interactive cards, attachments, and file sending for the Pi Agent.
|
|
4
|
+
|
|
5
|
+
This is an independent Pi extension project. It does not modify `pi-main`.
|
|
6
|
+
|
|
7
|
+
Current scope:
|
|
8
|
+
|
|
9
|
+
- Feishu WebSocket message transport
|
|
10
|
+
- Private chat and group chat session isolation
|
|
11
|
+
- One Pi runtime per Feishu session key
|
|
12
|
+
- Feishu card commands
|
|
13
|
+
- Attachment processing
|
|
14
|
+
- `send_file_to_chat` for returning local files to the current Feishu chat
|
|
15
|
+
- Optional `lark-doc-cli` and `lark-im-readonly` skills when `lark-cli` is installed
|
|
16
|
+
- Guardrails that block `lark-cli im` send/reply/receive commands
|
|
17
|
+
|
|
18
|
+
## Quick Start
|
|
19
|
+
|
|
20
|
+
Install and build:
|
|
21
|
+
|
|
22
|
+
```powershell
|
|
23
|
+
npm install --ignore-scripts
|
|
24
|
+
npm run build
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Create a config template:
|
|
28
|
+
|
|
29
|
+
```powershell
|
|
30
|
+
node .\dist\bin\pi-remote-feishu.js init --cwd D:\develop\pi-main
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Edit:
|
|
34
|
+
|
|
35
|
+
```text
|
|
36
|
+
D:\develop\pi-main\.pi\feishu.json
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Start the bridge:
|
|
40
|
+
|
|
41
|
+
```powershell
|
|
42
|
+
node .\dist\bin\pi-remote-feishu.js serve --cwd D:\develop\pi-main
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
You can also pass credentials through environment variables:
|
|
46
|
+
|
|
47
|
+
```powershell
|
|
48
|
+
$env:FEISHU_APP_ID="cli_xxx"
|
|
49
|
+
$env:FEISHU_APP_SECRET="xxx"
|
|
50
|
+
node .\dist\bin\pi-remote-feishu.js serve --cwd D:\develop\pi-main
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## CLI Commands
|
|
54
|
+
|
|
55
|
+
Check config and WebSocket connectivity:
|
|
56
|
+
|
|
57
|
+
```powershell
|
|
58
|
+
node .\dist\bin\pi-remote-feishu.js doctor --cwd D:\develop\pi-main
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Send a test message when you know the chat id:
|
|
62
|
+
|
|
63
|
+
```powershell
|
|
64
|
+
node .\dist\bin\pi-remote-feishu.js send-test --chat-id <chatId> --text "hello"
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Log incoming event summaries:
|
|
68
|
+
|
|
69
|
+
```powershell
|
|
70
|
+
node .\dist\bin\pi-remote-feishu.js serve --cwd D:\develop\pi-main --debug-events
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
or configure:
|
|
74
|
+
|
|
75
|
+
```json
|
|
76
|
+
{
|
|
77
|
+
"debug": {
|
|
78
|
+
"logIncomingEvents": true
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Feishu Chat Commands
|
|
84
|
+
|
|
85
|
+
- `/help`
|
|
86
|
+
- `/sessions`
|
|
87
|
+
- `/models`
|
|
88
|
+
- `/new`
|
|
89
|
+
- `/stop`
|
|
90
|
+
- `/reset`
|
|
91
|
+
- `/status`
|
|
92
|
+
|
|
93
|
+
Group chats require mentioning the bot by default.
|
|
94
|
+
|
|
95
|
+
## Skills And Message Output
|
|
96
|
+
|
|
97
|
+
The extension exposes two skills to Pi only when `lark-cli` is installed and available on `PATH`:
|
|
98
|
+
|
|
99
|
+
- `lark-doc-cli`: Feishu/Lark document workflows through `lark-cli`, including fetch, create, update, and media insertion.
|
|
100
|
+
- `lark-im-readonly`: read-only IM inspection through `lark-cli`, including chat/member lookup, message search, and resource download.
|
|
101
|
+
|
|
102
|
+
If `lark-cli` is not installed, these CLI skills are not loaded. The core Feishu transport, cards, attachments, and `send_file_to_chat` still work through the Feishu SDK.
|
|
103
|
+
|
|
104
|
+
Loading a skill must not send Feishu messages by itself. In a Feishu-triggered run, the final chat reply is sent by the `pi-remote-feishu serve` transport. Generated local files should be returned through `send_file_to_chat`.
|
|
105
|
+
|
|
106
|
+
The extension blocks these IM write or receive commands when the model tries to run them through bash:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
lark-cli im +messages-send
|
|
110
|
+
lark-cli im +messages-reply
|
|
111
|
+
lark-cli im +messages-receive
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Docs
|
|
115
|
+
|
|
116
|
+
- `ARCHITECTURE.md`
|
|
117
|
+
- `ARCHITECTURE.zh-CN.md`
|
|
118
|
+
- `docs/`
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 附件 MIME 类型推断工具。
|
|
3
|
+
*/
|
|
4
|
+
/** 根据文件名后缀推断 MIME 类型;未知扩展名返回通用二进制类型 */
|
|
5
|
+
export declare function inferMimeType(fileName: string): string;
|
|
6
|
+
/**
|
|
7
|
+
* 粗略判断一段 Buffer 是否像文本:
|
|
8
|
+
* 前 4KB 里只要出现字节 0(NUL),就认为不是文本。
|
|
9
|
+
* 用于决定小文件是内联内容还是落盘保存。
|
|
10
|
+
*/
|
|
11
|
+
export declare function isLikelyText(buffer: Buffer): boolean;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 附件 MIME 类型推断工具。
|
|
3
|
+
*/
|
|
4
|
+
/** 常见图片扩展名 -> MIME 类型 */
|
|
5
|
+
const MIME_BY_EXTENSION = {
|
|
6
|
+
png: "image/png",
|
|
7
|
+
jpg: "image/jpeg",
|
|
8
|
+
jpeg: "image/jpeg",
|
|
9
|
+
gif: "image/gif",
|
|
10
|
+
webp: "image/webp",
|
|
11
|
+
bmp: "image/bmp",
|
|
12
|
+
tiff: "image/tiff",
|
|
13
|
+
};
|
|
14
|
+
/** 根据文件名后缀推断 MIME 类型;未知扩展名返回通用二进制类型 */
|
|
15
|
+
export function inferMimeType(fileName) {
|
|
16
|
+
const extension = fileName.split(".").pop()?.toLowerCase() ?? "";
|
|
17
|
+
return MIME_BY_EXTENSION[extension] ?? "application/octet-stream";
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* 粗略判断一段 Buffer 是否像文本:
|
|
21
|
+
* 前 4KB 里只要出现字节 0(NUL),就认为不是文本。
|
|
22
|
+
* 用于决定小文件是内联内容还是落盘保存。
|
|
23
|
+
*/
|
|
24
|
+
export function isLikelyText(buffer) {
|
|
25
|
+
return !buffer.subarray(0, 4096).includes(0);
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=mime.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mime.js","sourceRoot":"","sources":["../../src/attachments/mime.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,yBAAyB;AACzB,MAAM,iBAAiB,GAA2B;IAChD,GAAG,EAAE,WAAW;IAChB,GAAG,EAAE,YAAY;IACjB,IAAI,EAAE,YAAY;IAClB,GAAG,EAAE,WAAW;IAChB,IAAI,EAAE,YAAY;IAClB,GAAG,EAAE,WAAW;IAChB,IAAI,EAAE,YAAY;CACnB,CAAC;AAEF,uCAAuC;AACvC,MAAM,UAAU,aAAa,CAAC,QAAgB;IAC5C,MAAM,SAAS,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC;IACjE,OAAO,iBAAiB,CAAC,SAAS,CAAC,IAAI,0BAA0B,CAAC;AACpE,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,MAAc;IACzC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;AAC/C,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { FeishuChannel, FeishuConfig, FeishuMessage, ImageInput } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* ============================================================
|
|
4
|
+
* 附件处理
|
|
5
|
+
* ------------------------------------------------------------
|
|
6
|
+
* 对消息携带的附件资源分类处理:
|
|
7
|
+
* 1. 表情包(sticker):直接跳过
|
|
8
|
+
* 2. 图片 + 模型支持图片输入:转 base64 图片传给 Pi
|
|
9
|
+
* 3. 小型文本文件(非音视频、≤32KB、内容像文本):内容内联到提示词
|
|
10
|
+
* 4. 其余文件:下载保存到临时目录,提示词里记录保存路径
|
|
11
|
+
* 5. 下载失败:提示词里记录失败原因
|
|
12
|
+
* ============================================================
|
|
13
|
+
*/
|
|
14
|
+
/** 附件处理结果:图片列表 + 文字说明列表 */
|
|
15
|
+
export interface ProcessedAttachments {
|
|
16
|
+
images: ImageInput[];
|
|
17
|
+
notes: string[];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* 处理一条消息的所有附件。
|
|
21
|
+
* 每个资源单独 try/catch:单个失败不影响其他资源。
|
|
22
|
+
*/
|
|
23
|
+
export declare function processAttachments(options: {
|
|
24
|
+
channel: Pick<FeishuChannel, "downloadResource">;
|
|
25
|
+
config: FeishuConfig;
|
|
26
|
+
message: FeishuMessage;
|
|
27
|
+
sessionKey: string;
|
|
28
|
+
supportsImages: boolean;
|
|
29
|
+
}): Promise<ProcessedAttachments>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { inferMimeType, isLikelyText } from "./mime.js";
|
|
2
|
+
import { writeTempAttachment } from "./temp-files.js";
|
|
3
|
+
/** 文本文件大小上限:超过则视为不可内联 */
|
|
4
|
+
const TEXT_SIZE_LIMIT = 32 * 1024;
|
|
5
|
+
/** 附件落盘文件名:优先用原始文件名,没有则用 fileKey.bin */
|
|
6
|
+
function resourceFileName(fileKey, fileName) {
|
|
7
|
+
return fileName ?? `${fileKey}.bin`;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* 处理一条消息的所有附件。
|
|
11
|
+
* 每个资源单独 try/catch:单个失败不影响其他资源。
|
|
12
|
+
*/
|
|
13
|
+
export async function processAttachments(options) {
|
|
14
|
+
const images = [];
|
|
15
|
+
const notes = [];
|
|
16
|
+
for (const resource of options.message.resources) {
|
|
17
|
+
if (resource.type === "sticker")
|
|
18
|
+
continue;
|
|
19
|
+
const fileName = resourceFileName(resource.fileKey, resource.fileName);
|
|
20
|
+
try {
|
|
21
|
+
const buffer = await options.channel.downloadResource(options.message.messageId, resource.fileKey, resource.type);
|
|
22
|
+
// 图片且模型支持:转 base64 作为图像输入
|
|
23
|
+
if (resource.type === "image" && options.supportsImages) {
|
|
24
|
+
images.push({
|
|
25
|
+
type: "image",
|
|
26
|
+
data: buffer.toString("base64"),
|
|
27
|
+
mimeType: inferMimeType(fileName),
|
|
28
|
+
});
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
// 小型文本(非音视频):内容直接内联进提示词
|
|
32
|
+
if (resource.type !== "audio" && resource.type !== "video" && buffer.length <= TEXT_SIZE_LIMIT && isLikelyText(buffer)) {
|
|
33
|
+
notes.push(`[Attachment text: ${fileName}]\n${buffer.toString("utf-8")}`);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
// 其他文件:下载到临时目录,提示词记录路径(模型可用它读取)
|
|
37
|
+
const filePath = await writeTempAttachment(options.config.files.tempDir ?? ".", options.sessionKey, fileName, buffer);
|
|
38
|
+
notes.push(`[Attachment saved: ${fileName} -> ${filePath}]`);
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
// 下载/处理失败:提示词里记录原因
|
|
42
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
43
|
+
notes.push(`[Attachment failed: ${fileName}: ${message}]`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return { images, notes };
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=processor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"processor.js","sourceRoot":"","sources":["../../src/attachments/processor.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACxD,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAqBtD,yBAAyB;AACzB,MAAM,eAAe,GAAG,EAAE,GAAG,IAAI,CAAC;AAElC,wCAAwC;AACxC,SAAS,gBAAgB,CAAC,OAAe,EAAE,QAA4B;IACrE,OAAO,QAAQ,IAAI,GAAG,OAAO,MAAM,CAAC;AACtC,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,OAMxC;IACC,MAAM,MAAM,GAAiB,EAAE,CAAC;IAChC,MAAM,KAAK,GAAa,EAAE,CAAC;IAE3B,KAAK,MAAM,QAAQ,IAAI,OAAO,CAAC,OAAO,CAAC,SAAS,EAAE,CAAC;QACjD,IAAI,QAAQ,CAAC,IAAI,KAAK,SAAS;YAAE,SAAS;QAC1C,MAAM,QAAQ,GAAG,gBAAgB,CAAC,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,QAAQ,CAAC,CAAC;QAEvE,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,gBAAgB,CAAC,OAAO,CAAC,OAAO,CAAC,SAAS,EAAE,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC;YAElH,0BAA0B;YAC1B,IAAI,QAAQ,CAAC,IAAI,KAAK,OAAO,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;gBACxD,MAAM,CAAC,IAAI,CAAC;oBACV,IAAI,EAAE,OAAO;oBACb,IAAI,EAAE,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC;oBAC/B,QAAQ,EAAE,aAAa,CAAC,QAAQ,CAAC;iBAClC,CAAC,CAAC;gBACH,SAAS;YACX,CAAC;YAED,wBAAwB;YACxB,IAAI,QAAQ,CAAC,IAAI,KAAK,OAAO,IAAI,QAAQ,CAAC,IAAI,KAAK,OAAO,IAAI,MAAM,CAAC,MAAM,IAAI,eAAe,IAAI,YAAY,CAAC,MAAM,CAAC,EAAE,CAAC;gBACvH,KAAK,CAAC,IAAI,CAAC,qBAAqB,QAAQ,MAAM,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;gBAC1E,SAAS;YACX,CAAC;YAED,gCAAgC;YAChC,MAAM,QAAQ,GAAG,MAAM,mBAAmB,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,IAAI,GAAG,EAAE,OAAO,CAAC,UAAU,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC;YACtH,KAAK,CAAC,IAAI,CAAC,sBAAsB,QAAQ,OAAO,QAAQ,GAAG,CAAC,CAAC;QAC/D,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,mBAAmB;YACnB,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YACvE,KAAK,CAAC,IAAI,CAAC,uBAAuB,QAAQ,KAAK,OAAO,GAAG,CAAC,CAAC;QAC7D,CAAC;IACH,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;AAC3B,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 附件临时文件管理:
|
|
3
|
+
* 每个会话一个子目录(目录名由 sessionKey 清洗而来),
|
|
4
|
+
* 附件下载后写到这里,生成结束后整体清理。
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* 把附件 Buffer 写入会话专属临时目录。
|
|
8
|
+
* 返回写入后的完整文件路径。
|
|
9
|
+
* 文件名会做安全清洗,去掉路径分隔符等危险字符。
|
|
10
|
+
*/
|
|
11
|
+
export declare function writeTempAttachment(tempDir: string, sessionKey: string, fileName: string, buffer: Buffer): Promise<string>;
|
|
12
|
+
/**
|
|
13
|
+
* 清理某个会话的临时附件目录(生成结束/命令处理完后调用)。
|
|
14
|
+
* force: true 表示目录不存在也不报错。
|
|
15
|
+
*/
|
|
16
|
+
export declare function cleanupTempAttachments(tempDir: string, sessionKey: string): Promise<void>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* 附件临时文件管理:
|
|
5
|
+
* 每个会话一个子目录(目录名由 sessionKey 清洗而来),
|
|
6
|
+
* 附件下载后写到这里,生成结束后整体清理。
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* 把附件 Buffer 写入会话专属临时目录。
|
|
10
|
+
* 返回写入后的完整文件路径。
|
|
11
|
+
* 文件名会做安全清洗,去掉路径分隔符等危险字符。
|
|
12
|
+
*/
|
|
13
|
+
export async function writeTempAttachment(tempDir, sessionKey, fileName, buffer) {
|
|
14
|
+
// 清洗文件名:替换 Windows/Unix 路径分隔符与控制字符
|
|
15
|
+
const sanitizedName = fileName.replace(/[<>:"/\\|?*\u0000-\u001f]/g, "_");
|
|
16
|
+
const safeName = sanitizedName.length > 0 && sanitizedName !== "." && sanitizedName !== ".." ? sanitizedName : "attachment.bin";
|
|
17
|
+
// 会话目录名:只保留安全字符
|
|
18
|
+
const dir = join(tempDir, sessionKey.replace(/[^a-zA-Z0-9_.-]/g, "_"));
|
|
19
|
+
await mkdir(dir, { recursive: true });
|
|
20
|
+
const filePath = join(dir, safeName);
|
|
21
|
+
await writeFile(filePath, buffer);
|
|
22
|
+
return filePath;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* 清理某个会话的临时附件目录(生成结束/命令处理完后调用)。
|
|
26
|
+
* force: true 表示目录不存在也不报错。
|
|
27
|
+
*/
|
|
28
|
+
export async function cleanupTempAttachments(tempDir, sessionKey) {
|
|
29
|
+
const dir = join(tempDir, sessionKey.replace(/[^a-zA-Z0-9_.-]/g, "_"));
|
|
30
|
+
await rm(dir, { recursive: true, force: true });
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=temp-files.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"temp-files.js","sourceRoot":"","sources":["../../src/attachments/temp-files.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACxD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC;;;;GAIG;AAEH;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,OAAe,EAAE,UAAkB,EAAE,QAAgB,EAAE,MAAc;IAC7G,mCAAmC;IACnC,MAAM,aAAa,GAAG,QAAQ,CAAC,OAAO,CAAC,4BAA4B,EAAE,GAAG,CAAC,CAAC;IAC1E,MAAM,QAAQ,GAAG,aAAa,CAAC,MAAM,GAAG,CAAC,IAAI,aAAa,KAAK,GAAG,IAAI,aAAa,KAAK,IAAI,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,gBAAgB,CAAC;IAChI,gBAAgB;IAChB,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,EAAE,UAAU,CAAC,OAAO,CAAC,kBAAkB,EAAE,GAAG,CAAC,CAAC,CAAC;IACvE,MAAM,KAAK,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACtC,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;IACrC,MAAM,SAAS,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IAClC,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAAC,OAAe,EAAE,UAAkB;IAC9E,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,EAAE,UAAU,CAAC,OAAO,CAAC,kBAAkB,EAAE,GAAG,CAAC,CAAC,CAAC;IACvE,MAAM,EAAE,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;AAClD,CAAC"}
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* ============================================================
|
|
4
|
+
* pi-remote-feishu 命令行入口(bin)
|
|
5
|
+
* ------------------------------------------------------------
|
|
6
|
+
* 支持子命令:
|
|
7
|
+
* serve 启动 WebSocket 桥接,把飞书消息转发给 Pi 运行时(主命令)
|
|
8
|
+
* init 生成一份 feishu.json 配置模板
|
|
9
|
+
* doctor 检查配置与飞书 WebSocket 连通性
|
|
10
|
+
* send-test 向指定聊天发送一条测试消息
|
|
11
|
+
* help 打印帮助
|
|
12
|
+
*
|
|
13
|
+
* 参数来源优先级:环境变量 < 配置文件 < CLI 参数(见 config/load-config.ts)
|
|
14
|
+
* ============================================================
|
|
15
|
+
*/
|
|
16
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
17
|
+
import { dirname, join, resolve } from "node:path";
|
|
18
|
+
import { handleCardAction } from "../bridge/card-actions.js";
|
|
19
|
+
import { handleFeishuMessage } from "../bridge/message-handler.js";
|
|
20
|
+
import { SessionHostManager } from "../bridge/session-host-manager.js";
|
|
21
|
+
import { loadConfig, resolveConfigFilePath } from "../config/load-config.js";
|
|
22
|
+
import { createFeishuChannel } from "../feishu/channel.js";
|
|
23
|
+
import { createWebhookNotImplementedError } from "../feishu/webhook.js";
|
|
24
|
+
import { createStore } from "../store/store.js";
|
|
25
|
+
/** 飞书 WebSocket 连接/断开的超时时间(毫秒) */
|
|
26
|
+
const CONNECT_TIMEOUT_MS = 20_000;
|
|
27
|
+
const DISCONNECT_TIMEOUT_MS = 5_000;
|
|
28
|
+
/** 读取某个 flag 后面的参数值,缺失则报错 */
|
|
29
|
+
function readFlag(args, index) {
|
|
30
|
+
const value = args[index + 1];
|
|
31
|
+
if (!value) {
|
|
32
|
+
throw new Error(`Missing value for ${args[index]}`);
|
|
33
|
+
}
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* 解析命令行参数。
|
|
38
|
+
* 第一个参数如果是 serve/init/doctor/send-test/help 则作为子命令,
|
|
39
|
+
* 否则默认为 serve(方便直接运行 `pi-remote-feishu --app-id ...`)。
|
|
40
|
+
*/
|
|
41
|
+
function parseArgs(args) {
|
|
42
|
+
const command = args[0] === "serve" || args[0] === "init" || args[0] === "doctor" || args[0] === "send-test"
|
|
43
|
+
? args[0]
|
|
44
|
+
: args[0] === "help" || args[0] === "--help" || args[0] === "-h"
|
|
45
|
+
? "help"
|
|
46
|
+
: "serve";
|
|
47
|
+
const config = {};
|
|
48
|
+
let logLevel;
|
|
49
|
+
let initPath;
|
|
50
|
+
let force = false;
|
|
51
|
+
let chatId;
|
|
52
|
+
let text;
|
|
53
|
+
for (let index = command === "serve" || command === "init" || command === "doctor" || command === "send-test" ? 1 : 0; index < args.length; index += 1) {
|
|
54
|
+
const arg = args[index];
|
|
55
|
+
if (arg === "--app-id") {
|
|
56
|
+
config.appId = readFlag(args, index);
|
|
57
|
+
index += 1;
|
|
58
|
+
}
|
|
59
|
+
else if (arg === "--app-secret") {
|
|
60
|
+
config.appSecret = readFlag(args, index);
|
|
61
|
+
index += 1;
|
|
62
|
+
}
|
|
63
|
+
else if (arg === "--config") {
|
|
64
|
+
config.configPath = readFlag(args, index);
|
|
65
|
+
initPath = resolve(config.configPath);
|
|
66
|
+
index += 1;
|
|
67
|
+
}
|
|
68
|
+
else if (arg === "--cwd") {
|
|
69
|
+
config.cwd = resolve(readFlag(args, index));
|
|
70
|
+
index += 1;
|
|
71
|
+
}
|
|
72
|
+
else if (arg === "--log-level") {
|
|
73
|
+
logLevel = readFlag(args, index);
|
|
74
|
+
index += 1;
|
|
75
|
+
}
|
|
76
|
+
else if (arg === "--group-scope") {
|
|
77
|
+
// 群聊会话隔离范围:shared-chat(共享)/ per-user(按用户隔离)
|
|
78
|
+
const groupScope = readFlag(args, index);
|
|
79
|
+
if (groupScope !== "shared-chat" && groupScope !== "per-user") {
|
|
80
|
+
throw new Error("--group-scope must be shared-chat or per-user");
|
|
81
|
+
}
|
|
82
|
+
config.sessions = { ...config.sessions, groupScope };
|
|
83
|
+
index += 1;
|
|
84
|
+
}
|
|
85
|
+
else if (arg === "--debug-events") {
|
|
86
|
+
// 打印收到的消息/卡片事件摘要
|
|
87
|
+
config.debug = { ...config.debug, logIncomingEvents: true };
|
|
88
|
+
}
|
|
89
|
+
else if (arg === "--force") {
|
|
90
|
+
force = true;
|
|
91
|
+
}
|
|
92
|
+
else if (arg === "--chat-id") {
|
|
93
|
+
chatId = readFlag(args, index);
|
|
94
|
+
index += 1;
|
|
95
|
+
}
|
|
96
|
+
else if (arg === "--text") {
|
|
97
|
+
text = readFlag(args, index);
|
|
98
|
+
index += 1;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
command,
|
|
103
|
+
config,
|
|
104
|
+
...(logLevel ? { logLevel } : {}),
|
|
105
|
+
...(initPath ? { initPath } : {}),
|
|
106
|
+
...(force ? { force } : {}),
|
|
107
|
+
...(chatId ? { chatId } : {}),
|
|
108
|
+
...(text ? { text } : {}),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/** 打印命令行帮助 */
|
|
112
|
+
function printHelp() {
|
|
113
|
+
console.log(`Usage:
|
|
114
|
+
pi-remote-feishu serve [options]
|
|
115
|
+
pi-remote-feishu init [--config <path>] [--force]
|
|
116
|
+
pi-remote-feishu doctor [options]
|
|
117
|
+
pi-remote-feishu send-test --chat-id <chatId> --text <text> [options]
|
|
118
|
+
|
|
119
|
+
Options:
|
|
120
|
+
--app-id <id> Feishu app id
|
|
121
|
+
--app-secret <secret> Feishu app secret
|
|
122
|
+
--config <path> Config file path
|
|
123
|
+
--cwd <path> Default Pi working directory
|
|
124
|
+
--log-level <level> fatal|error|warn|info|debug|trace
|
|
125
|
+
--group-scope <scope> shared-chat|per-user
|
|
126
|
+
--debug-events Log incoming message/card action summaries
|
|
127
|
+
--force Overwrite config when used with init
|
|
128
|
+
--chat-id <id> Target chat id for send-test
|
|
129
|
+
--text <text> Text for send-test
|
|
130
|
+
`);
|
|
131
|
+
}
|
|
132
|
+
/** 脱敏 App Secret:只保留头尾各 4 位,中间用 **** 代替 */
|
|
133
|
+
function maskSecret(value) {
|
|
134
|
+
if (value.length <= 8)
|
|
135
|
+
return "****";
|
|
136
|
+
return `${value.slice(0, 4)}****${value.slice(-4)}`;
|
|
137
|
+
}
|
|
138
|
+
/** 给 Promise 加超时:超时后 reject,并在结束时清理定时器 */
|
|
139
|
+
async function withTimeout(promise, timeoutMs, label) {
|
|
140
|
+
let timer;
|
|
141
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
142
|
+
timer = setTimeout(() => {
|
|
143
|
+
reject(new Error(`${label} timed out after ${timeoutMs}ms`));
|
|
144
|
+
}, timeoutMs);
|
|
145
|
+
});
|
|
146
|
+
try {
|
|
147
|
+
return await Promise.race([promise, timeout]);
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
if (timer) {
|
|
151
|
+
clearTimeout(timer);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/** 生成一份默认配置模板(pi-remote-feishu init 用),已存在且未带 --force 时拒绝覆盖 */
|
|
156
|
+
function writeConfigTemplate(path, force) {
|
|
157
|
+
if (existsSync(path) && !force) {
|
|
158
|
+
throw new Error(`Config already exists: ${path}. Use --force to overwrite.`);
|
|
159
|
+
}
|
|
160
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
161
|
+
const template = {
|
|
162
|
+
appId: "cli_xxx",
|
|
163
|
+
appSecret: "replace-with-app-secret",
|
|
164
|
+
botName: "Pi Agent",
|
|
165
|
+
transport: "websocket",
|
|
166
|
+
policy: {
|
|
167
|
+
requireMention: true,
|
|
168
|
+
dmEnabled: true,
|
|
169
|
+
groupEnabled: true,
|
|
170
|
+
allowUsers: [],
|
|
171
|
+
allowChats: [],
|
|
172
|
+
},
|
|
173
|
+
sessions: {
|
|
174
|
+
groupScope: "shared-chat",
|
|
175
|
+
defaultCwd: process.cwd(),
|
|
176
|
+
store: "json",
|
|
177
|
+
idleTtlMs: 1_800_000,
|
|
178
|
+
},
|
|
179
|
+
rendering: {
|
|
180
|
+
mode: "stream-card",
|
|
181
|
+
showThinking: "quote",
|
|
182
|
+
showToolEvents: true,
|
|
183
|
+
},
|
|
184
|
+
files: {
|
|
185
|
+
allowedOutputDirs: [process.cwd()],
|
|
186
|
+
maxUploadBytes: 20_971_520,
|
|
187
|
+
},
|
|
188
|
+
debug: {
|
|
189
|
+
logIncomingEvents: false,
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
writeFileSync(path, `${JSON.stringify(template, null, 2)}\n`, "utf-8");
|
|
193
|
+
}
|
|
194
|
+
/** 生成收到的消息摘要(--debug-events 时打印到 stderr) */
|
|
195
|
+
function incomingMessageSummary(message) {
|
|
196
|
+
return JSON.stringify({
|
|
197
|
+
event: "message",
|
|
198
|
+
messageId: message.messageId,
|
|
199
|
+
chatId: message.chatId,
|
|
200
|
+
chatType: message.chatType,
|
|
201
|
+
userOpenId: message.userOpenId,
|
|
202
|
+
senderName: message.senderName ?? "",
|
|
203
|
+
mentionedBot: message.mentionedBot,
|
|
204
|
+
textLength: message.text.length,
|
|
205
|
+
resources: message.resources.length,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
/** 生成收到的卡片事件摘要(--debug-events 时打印到 stderr) */
|
|
209
|
+
function incomingCardActionSummary(event) {
|
|
210
|
+
return JSON.stringify({
|
|
211
|
+
event: "cardAction",
|
|
212
|
+
messageId: event.messageId ?? "",
|
|
213
|
+
chatId: event.chatId,
|
|
214
|
+
userOpenId: event.userOpenId ?? "",
|
|
215
|
+
cmd: typeof event.actionValue.cmd === "string" ? event.actionValue.cmd : "",
|
|
216
|
+
hasToken: event.token !== undefined,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
/** 根据配置创建飞书通道 */
|
|
220
|
+
function createChannel(config, logLevel) {
|
|
221
|
+
return createFeishuChannel({
|
|
222
|
+
appId: config.appId,
|
|
223
|
+
appSecret: config.appSecret,
|
|
224
|
+
requireMention: config.policy.requireMention,
|
|
225
|
+
...(logLevel ? { logLevel } : {}),
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
/** doctor 子命令:打印配置摘要,并实际连一次飞书 WebSocket 验证连通性 */
|
|
229
|
+
async function runDoctor(cli) {
|
|
230
|
+
const cwd = resolve(cli.config.cwd ?? process.cwd());
|
|
231
|
+
const configPath = resolveConfigFilePath(cwd, cli.config.configPath);
|
|
232
|
+
const config = loadConfig(cli.config);
|
|
233
|
+
console.log("Pi Feishu Doctor");
|
|
234
|
+
console.log(`- cwd: ${cwd}`);
|
|
235
|
+
console.log(`- config: ${configPath ?? "env/cli only"}`);
|
|
236
|
+
console.log(`- appId: ${config.appId}`);
|
|
237
|
+
console.log(`- appSecret: ${maskSecret(config.appSecret)}`);
|
|
238
|
+
console.log(`- transport: ${config.transport}`);
|
|
239
|
+
console.log(`- dmEnabled: ${config.policy.dmEnabled}`);
|
|
240
|
+
console.log(`- groupEnabled: ${config.policy.groupEnabled}`);
|
|
241
|
+
console.log(`- requireMention: ${config.policy.requireMention}`);
|
|
242
|
+
console.log(`- groupScope: ${config.sessions.groupScope}`);
|
|
243
|
+
console.log(`- debug.logIncomingEvents: ${config.debug.logIncomingEvents}`);
|
|
244
|
+
if (config.transport === "webhook") {
|
|
245
|
+
console.log("- websocket: skipped, webhook transport is not implemented in v1");
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
const channel = createChannel(config, cli.logLevel);
|
|
249
|
+
try {
|
|
250
|
+
await withTimeout(channel.connect(), CONNECT_TIMEOUT_MS, "Feishu websocket connect");
|
|
251
|
+
console.log(`- websocket: connected${channel.botName ? ` as ${channel.botName}` : ""}`);
|
|
252
|
+
}
|
|
253
|
+
finally {
|
|
254
|
+
await withTimeout(channel.disconnect(), DISCONNECT_TIMEOUT_MS, "Feishu websocket disconnect").catch(() => { });
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
/** send-test 子命令:连接飞书并发送一条测试文本 */
|
|
258
|
+
async function runSendTest(cli) {
|
|
259
|
+
if (!cli.chatId) {
|
|
260
|
+
throw new Error("Missing --chat-id");
|
|
261
|
+
}
|
|
262
|
+
if (!cli.text) {
|
|
263
|
+
throw new Error("Missing --text");
|
|
264
|
+
}
|
|
265
|
+
const config = loadConfig(cli.config);
|
|
266
|
+
if (config.transport === "webhook") {
|
|
267
|
+
throw createWebhookNotImplementedError();
|
|
268
|
+
}
|
|
269
|
+
const channel = createChannel(config, cli.logLevel);
|
|
270
|
+
try {
|
|
271
|
+
await withTimeout(channel.connect(), CONNECT_TIMEOUT_MS, "Feishu websocket connect");
|
|
272
|
+
const result = await channel.sendText(cli.chatId, cli.text);
|
|
273
|
+
console.log(`Sent test message: ${result.messageId}`);
|
|
274
|
+
}
|
|
275
|
+
finally {
|
|
276
|
+
await withTimeout(channel.disconnect(), DISCONNECT_TIMEOUT_MS, "Feishu websocket disconnect").catch(() => { });
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* serve 主流程:
|
|
281
|
+
* 1. 加载配置、创建通道和存储
|
|
282
|
+
* 2. 创建 SessionHostManager(负责按 sessionKey 管理 Pi 运行时)
|
|
283
|
+
* 3. 注册消息/卡片/错误三个回调
|
|
284
|
+
* 4. 连接飞书,阻塞直到收到 SIGINT/SIGTERM
|
|
285
|
+
*/
|
|
286
|
+
async function main() {
|
|
287
|
+
const cli = parseArgs(process.argv.slice(2));
|
|
288
|
+
if (cli.command === "help") {
|
|
289
|
+
printHelp();
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
if (cli.command === "init") {
|
|
293
|
+
const cwd = resolve(cli.config.cwd ?? process.cwd());
|
|
294
|
+
const configPath = cli.initPath ?? join(cwd, ".pi", "feishu.json");
|
|
295
|
+
writeConfigTemplate(configPath, cli.force ?? false);
|
|
296
|
+
console.log(`Created Feishu config: ${configPath}`);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (cli.command === "doctor") {
|
|
300
|
+
await runDoctor(cli);
|
|
301
|
+
process.exit(0);
|
|
302
|
+
}
|
|
303
|
+
if (cli.command === "send-test") {
|
|
304
|
+
await runSendTest(cli);
|
|
305
|
+
process.exit(0);
|
|
306
|
+
}
|
|
307
|
+
// ---- serve 子命令 ----
|
|
308
|
+
const config = loadConfig(cli.config);
|
|
309
|
+
if (config.transport === "webhook") {
|
|
310
|
+
throw createWebhookNotImplementedError();
|
|
311
|
+
}
|
|
312
|
+
const channel = createChannel(config, cli.logLevel);
|
|
313
|
+
const store = createStore(config);
|
|
314
|
+
const manager = new SessionHostManager({ config, store, channel });
|
|
315
|
+
// 收到飞书消息:可选的调试摘要 -> 交给 handleFeishuMessage 处理
|
|
316
|
+
channel.onMessage((message) => {
|
|
317
|
+
if (config.debug.logIncomingEvents) {
|
|
318
|
+
console.error(incomingMessageSummary(message));
|
|
319
|
+
}
|
|
320
|
+
handleFeishuMessage({ config, channel, store, manager, message }).catch((error) => {
|
|
321
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
// 收到卡片按钮事件:交给 handleCardAction 处理
|
|
325
|
+
channel.onCardAction((event) => {
|
|
326
|
+
if (config.debug.logIncomingEvents) {
|
|
327
|
+
console.error(incomingCardActionSummary(event));
|
|
328
|
+
}
|
|
329
|
+
handleCardAction({
|
|
330
|
+
config,
|
|
331
|
+
store,
|
|
332
|
+
manager,
|
|
333
|
+
event,
|
|
334
|
+
updateCardByToken: async (token, card) => {
|
|
335
|
+
await channel.updateCardByToken(token, card);
|
|
336
|
+
},
|
|
337
|
+
}).catch((error) => {
|
|
338
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
channel.onError((error) => {
|
|
342
|
+
console.error(error.message);
|
|
343
|
+
});
|
|
344
|
+
await channel.connect();
|
|
345
|
+
console.error(`Feishu bot connected${channel.botName ? ` as ${channel.botName}` : ""}.`);
|
|
346
|
+
// 阻塞等待退出信号,收到后断开飞书连接
|
|
347
|
+
await new Promise((resolvePromise) => {
|
|
348
|
+
const shutdown = () => {
|
|
349
|
+
channel.disconnect().finally(resolvePromise);
|
|
350
|
+
};
|
|
351
|
+
process.once("SIGINT", shutdown);
|
|
352
|
+
process.once("SIGTERM", shutdown);
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
main().catch((error) => {
|
|
356
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
357
|
+
process.exit(1);
|
|
358
|
+
});
|
|
359
|
+
//# sourceMappingURL=pi-remote-feishu.js.map
|