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
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { stat } from "node:fs/promises";
|
|
2
|
+
import { basename, isAbsolute, relative, resolve } from "node:path";
|
|
3
|
+
import { Type } from "typebox";
|
|
4
|
+
import { getFeishuContext } from "../feishu/context.js";
|
|
5
|
+
/**
|
|
6
|
+
* ============================================================
|
|
7
|
+
* send_file_to_chat 工具
|
|
8
|
+
* ------------------------------------------------------------
|
|
9
|
+
* 供 Pi 在生成过程中调用:把本地生成的文件回传到当前飞书聊天。
|
|
10
|
+
*
|
|
11
|
+
* 安全校验:
|
|
12
|
+
* - 必须在飞书请求上下文内(getFeishuContext)才可用
|
|
13
|
+
* - 文件路径必须在 allowedOutputDirs 白名单内
|
|
14
|
+
* - 必须是文件且大小不超过 maxUploadBytes
|
|
15
|
+
* ============================================================
|
|
16
|
+
*/
|
|
17
|
+
/** 工具参数 schema(typebox 描述,Pi 会据此生成函数签名给模型看) */
|
|
18
|
+
const sendFileSchema = Type.Object({
|
|
19
|
+
filePath: Type.String({ description: "Local file path to send to the current Feishu chat" }),
|
|
20
|
+
fileName: Type.Optional(Type.String({ description: "Display file name in Feishu" })),
|
|
21
|
+
});
|
|
22
|
+
/** 判断 target 是否在 root 目录之内(含 root 本身),防止路径穿越 */
|
|
23
|
+
function isUnderPath(target, root) {
|
|
24
|
+
const resolvedTarget = resolve(target);
|
|
25
|
+
const resolvedRoot = resolve(root);
|
|
26
|
+
const relativePath = relative(resolvedRoot, resolvedTarget);
|
|
27
|
+
return relativePath === "" || (!relativePath.startsWith("..") && !isAbsolute(relativePath));
|
|
28
|
+
}
|
|
29
|
+
/** 校验文件:目录白名单 + 是文件 + 大小上限,通过则返回绝对路径 */
|
|
30
|
+
async function validateFile(filePath, config) {
|
|
31
|
+
const resolvedPath = resolve(filePath);
|
|
32
|
+
if (!config.files.allowedOutputDirs.some((dir) => isUnderPath(resolvedPath, dir))) {
|
|
33
|
+
throw new Error("File is outside allowed output directories");
|
|
34
|
+
}
|
|
35
|
+
const statResult = await stat(resolvedPath);
|
|
36
|
+
if (!statResult.isFile()) {
|
|
37
|
+
throw new Error("Path is not a file");
|
|
38
|
+
}
|
|
39
|
+
if (statResult.size > config.files.maxUploadBytes) {
|
|
40
|
+
throw new Error(`File exceeds upload limit (${config.files.maxUploadBytes} bytes)`);
|
|
41
|
+
}
|
|
42
|
+
return resolvedPath;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* 创建 send_file_to_chat 工具。
|
|
46
|
+
* 在 Pi 的运行时中注册后,模型生成完文件即可调用它把文件发回飞书。
|
|
47
|
+
*/
|
|
48
|
+
export function createSendFileToChatTool(config) {
|
|
49
|
+
return {
|
|
50
|
+
name: "send_file_to_chat",
|
|
51
|
+
label: "Send file to Feishu chat",
|
|
52
|
+
description: "Send a local file to the current Feishu chat. Only works while handling a Feishu request.",
|
|
53
|
+
promptGuidelines: [
|
|
54
|
+
"When you create a deliverable file for a Feishu user, call send_file_to_chat with the final local file path.",
|
|
55
|
+
"Only send files that already exist and are relevant to the user's request.",
|
|
56
|
+
],
|
|
57
|
+
parameters: sendFileSchema,
|
|
58
|
+
async execute(_toolCallId, params) {
|
|
59
|
+
// 只能在飞书请求上下文中使用(运行时通过 AsyncLocalStorage 注入)
|
|
60
|
+
const context = getFeishuContext();
|
|
61
|
+
if (!context) {
|
|
62
|
+
return {
|
|
63
|
+
content: [{ type: "text", text: "Not currently handling a Feishu request; cannot send file." }],
|
|
64
|
+
details: undefined,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
const filePath = await validateFile(params.filePath, config);
|
|
69
|
+
await context.channel.sendFile(context.chatId, filePath, params.fileName ?? basename(filePath));
|
|
70
|
+
return {
|
|
71
|
+
content: [{ type: "text", text: `Sent file to Feishu: ${params.fileName ?? basename(filePath)}` }],
|
|
72
|
+
details: undefined,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
77
|
+
return {
|
|
78
|
+
content: [{ type: "text", text: `Failed to send file to Feishu: ${message}` }],
|
|
79
|
+
details: undefined,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=send-file-to-chat.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"send-file-to-chat.js","sourceRoot":"","sources":["../../src/tools/send-file-to-chat.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import type { AgentSessionRuntime, ExtensionUIContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
/**
|
|
3
|
+
* ============================================================
|
|
4
|
+
* pi-remote-feishu 核心类型定义
|
|
5
|
+
* ------------------------------------------------------------
|
|
6
|
+
* 本文件定义了整个 pi-remote-feishu 扩展对外、对内使用的全部类型。
|
|
7
|
+
* 数据流概览:
|
|
8
|
+
*
|
|
9
|
+
* 飞书消息(FeishuMessage) -> 路由(routeConversation) -> 归一化(normalizeMessage)
|
|
10
|
+
* -> Pi 运行时(prompt) -> 流式渲染(stream-renderer) -> 回发到飞书聊天
|
|
11
|
+
*
|
|
12
|
+
* 本文件只包含纯类型,不含任何逻辑。
|
|
13
|
+
* ============================================================
|
|
14
|
+
*/
|
|
15
|
+
/** 聊天类型:私聊 或 群聊 */
|
|
16
|
+
export type ChatType = "private" | "group";
|
|
17
|
+
/** 群聊会话隔离范围:整个群共享一个会话 / 群里每个用户一个会话 */
|
|
18
|
+
export type GroupScope = "shared-chat" | "per-user";
|
|
19
|
+
/** 会话持久化存储类型:JSON 文件 / SQLite(SQLite 暂未实现) */
|
|
20
|
+
export type StoreKind = "json" | "sqlite";
|
|
21
|
+
/** 回复渲染模式:流式卡片 / Markdown / 纯文本 */
|
|
22
|
+
export type RenderingMode = "stream-card" | "markdown" | "text";
|
|
23
|
+
/** 思考过程(thinking)展示方式:隐藏 / 引用块(> 前缀) / 原文输出 */
|
|
24
|
+
export type ThinkingRenderMode = "hide" | "quote" | "plain";
|
|
25
|
+
/** 消息接入策略:谁可以跟机器人对话 */
|
|
26
|
+
export interface FeishuPolicyConfig {
|
|
27
|
+
/** 群聊中是否必须 @机器人 才响应 */
|
|
28
|
+
requireMention: boolean;
|
|
29
|
+
/** 是否允许私聊 */
|
|
30
|
+
dmEnabled: boolean;
|
|
31
|
+
/** 是否允许群聊 */
|
|
32
|
+
groupEnabled: boolean;
|
|
33
|
+
/** 允许的用户 open_id 白名单(缺省表示全部允许) */
|
|
34
|
+
allowUsers?: string[];
|
|
35
|
+
/** 允许的聊天 chat_id 白名单(缺省表示全部允许) */
|
|
36
|
+
allowChats?: string[];
|
|
37
|
+
}
|
|
38
|
+
/** 会话管理配置 */
|
|
39
|
+
export interface FeishuSessionsConfig {
|
|
40
|
+
/** 私聊固定为 per-user(每个用户独立会话),当前不支持其他值 */
|
|
41
|
+
privateScope: "per-user";
|
|
42
|
+
/** 群聊的会话隔离策略 */
|
|
43
|
+
groupScope: GroupScope;
|
|
44
|
+
/** Pi 运行时默认的工作目录 */
|
|
45
|
+
defaultCwd?: string;
|
|
46
|
+
/** 会话映射的存储方式 */
|
|
47
|
+
store: StoreKind;
|
|
48
|
+
/** 空闲会话自动回收的毫秒数(超时后释放 Pi runtime) */
|
|
49
|
+
idleTtlMs: number;
|
|
50
|
+
}
|
|
51
|
+
/** 回复渲染配置 */
|
|
52
|
+
export interface FeishuRenderingConfig {
|
|
53
|
+
/** 渲染模式 */
|
|
54
|
+
mode: RenderingMode;
|
|
55
|
+
/** 思考过程的展示方式 */
|
|
56
|
+
showThinking: ThinkingRenderMode;
|
|
57
|
+
/** 是否把工具调用过程(如 Running tool)输出到聊天里 */
|
|
58
|
+
showToolEvents: boolean;
|
|
59
|
+
}
|
|
60
|
+
/** 文件能力配置 */
|
|
61
|
+
export interface FeishuFilesConfig {
|
|
62
|
+
/** 允许回传给聊天的输出目录白名单(send_file_to_chat 工具会校验) */
|
|
63
|
+
allowedOutputDirs: string[];
|
|
64
|
+
/** 回传文件的最大字节数 */
|
|
65
|
+
maxUploadBytes: number;
|
|
66
|
+
/** 附件临时下载目录(默认系统临时目录下的 pi-remote-feishu) */
|
|
67
|
+
tempDir?: string;
|
|
68
|
+
}
|
|
69
|
+
/** 调试配置 */
|
|
70
|
+
export interface FeishuDebugConfig {
|
|
71
|
+
/** 是否打印收到的消息/卡片事件摘要,便于排查问题 */
|
|
72
|
+
logIncomingEvents: boolean;
|
|
73
|
+
}
|
|
74
|
+
/** Webhook 传输配置(v1 版本暂未实现,仅占位) */
|
|
75
|
+
export interface FeishuWebhookConfig {
|
|
76
|
+
host?: string;
|
|
77
|
+
port: number;
|
|
78
|
+
path: string;
|
|
79
|
+
}
|
|
80
|
+
/** 完整配置:由 load-config 合并 环境变量 + 配置文件 + CLI 参数 后生成 */
|
|
81
|
+
export interface FeishuConfig {
|
|
82
|
+
/** 飞书应用 App ID */
|
|
83
|
+
appId: string;
|
|
84
|
+
/** 飞书应用 App Secret */
|
|
85
|
+
appSecret: string;
|
|
86
|
+
/** 加密密钥(webhook 模式使用) */
|
|
87
|
+
encryptKey?: string;
|
|
88
|
+
/** 验证令牌(webhook 模式使用) */
|
|
89
|
+
verificationToken?: string;
|
|
90
|
+
/** 机器人显示名称 */
|
|
91
|
+
botName?: string;
|
|
92
|
+
/** 传输方式:目前只有 websocket 可用 */
|
|
93
|
+
transport: "websocket" | "webhook";
|
|
94
|
+
webhook?: FeishuWebhookConfig;
|
|
95
|
+
policy: FeishuPolicyConfig;
|
|
96
|
+
sessions: FeishuSessionsConfig;
|
|
97
|
+
rendering: FeishuRenderingConfig;
|
|
98
|
+
files: FeishuFilesConfig;
|
|
99
|
+
debug: FeishuDebugConfig;
|
|
100
|
+
}
|
|
101
|
+
/** 标识"一条消息来自哪里",用于路由和会话隔离 */
|
|
102
|
+
export interface FeishuIdentity {
|
|
103
|
+
appId: string;
|
|
104
|
+
tenantKey?: string;
|
|
105
|
+
chatType: ChatType;
|
|
106
|
+
chatId: string;
|
|
107
|
+
userOpenId: string;
|
|
108
|
+
senderName?: string;
|
|
109
|
+
}
|
|
110
|
+
/** 消息携带的附件资源(图片 / 文件 / 音频 / 视频 / 表情包等) */
|
|
111
|
+
export interface FeishuResource {
|
|
112
|
+
type: "image" | "file" | "audio" | "video" | "sticker" | string;
|
|
113
|
+
/** 飞书侧的文件 key,用于下载 */
|
|
114
|
+
fileKey: string;
|
|
115
|
+
fileName?: string;
|
|
116
|
+
size?: number;
|
|
117
|
+
}
|
|
118
|
+
/** 归一化后的飞书消息(channel 层把 SDK 原始事件转换成这个结构) */
|
|
119
|
+
export interface FeishuMessage {
|
|
120
|
+
messageId: string;
|
|
121
|
+
chatId: string;
|
|
122
|
+
chatType: ChatType;
|
|
123
|
+
userOpenId: string;
|
|
124
|
+
senderName?: string;
|
|
125
|
+
tenantKey?: string;
|
|
126
|
+
/** 消息文本内容 */
|
|
127
|
+
text: string;
|
|
128
|
+
/** 是否 @了机器人 */
|
|
129
|
+
mentionedBot: boolean;
|
|
130
|
+
/** 附件资源列表 */
|
|
131
|
+
resources: FeishuResource[];
|
|
132
|
+
/** 原始事件,便于调试 */
|
|
133
|
+
raw: unknown;
|
|
134
|
+
}
|
|
135
|
+
/** 飞书交互卡片上按钮/下拉菜单被点击后触发的事件 */
|
|
136
|
+
export interface FeishuCardAction {
|
|
137
|
+
messageId?: string;
|
|
138
|
+
chatId: string;
|
|
139
|
+
userOpenId?: string;
|
|
140
|
+
/** 按钮上 value 字段携带的负载(cmd、sessionKey 等都在这里) */
|
|
141
|
+
actionValue: Record<string, unknown>;
|
|
142
|
+
/** 卡片更新令牌,可用它原地更新卡片内容 */
|
|
143
|
+
token?: string;
|
|
144
|
+
raw: unknown;
|
|
145
|
+
}
|
|
146
|
+
/** 回复选项:replyTo 表示回复哪条消息(飞书里会形成引用回复) */
|
|
147
|
+
export interface ReplyOptions {
|
|
148
|
+
replyTo?: string;
|
|
149
|
+
}
|
|
150
|
+
/** 发送结果 */
|
|
151
|
+
export interface SendResult {
|
|
152
|
+
messageId: string;
|
|
153
|
+
}
|
|
154
|
+
/** 流式写入器:渲染过程把增量文本一段段写进来 */
|
|
155
|
+
export interface StreamWriter {
|
|
156
|
+
append(chunk: string): Promise<void>;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* 飞书通道抽象:所有跟飞书 SDK 的交互都封装在这个接口后面。
|
|
160
|
+
* 实现见 src/feishu/channel.ts
|
|
161
|
+
*/
|
|
162
|
+
export interface FeishuChannel {
|
|
163
|
+
connect(): Promise<void>;
|
|
164
|
+
disconnect(): Promise<void>;
|
|
165
|
+
/** 注册消息回调 */
|
|
166
|
+
onMessage(handler: (message: FeishuMessage) => void): void;
|
|
167
|
+
/** 注册卡片事件回调 */
|
|
168
|
+
onCardAction(handler: (event: FeishuCardAction) => void): void;
|
|
169
|
+
onError(handler: (error: Error) => void): void;
|
|
170
|
+
/** 发送纯文本 */
|
|
171
|
+
sendText(chatId: string, text: string, options?: ReplyOptions): Promise<SendResult>;
|
|
172
|
+
/** 发送 Markdown 文本 */
|
|
173
|
+
sendMarkdown(chatId: string, markdown: string, options?: ReplyOptions): Promise<SendResult>;
|
|
174
|
+
/** 发送交互卡片 */
|
|
175
|
+
sendCard(chatId: string, card: JsonObject, options?: ReplyOptions): Promise<SendResult>;
|
|
176
|
+
/** 以流式方式发送 Markdown:producer 回调里不断 append 增量内容 */
|
|
177
|
+
streamMarkdown(chatId: string, producer: (writer: StreamWriter) => Promise<void>, options?: ReplyOptions): Promise<void>;
|
|
178
|
+
/** 按 messageId 更新一张已发送的卡片 */
|
|
179
|
+
updateCard(messageId: string, card: JsonObject): Promise<void>;
|
|
180
|
+
/** 按 token 更新一张卡片(token 来自卡片事件) */
|
|
181
|
+
updateCardByToken(token: string, card: JsonObject): Promise<void>;
|
|
182
|
+
/** 发送本地文件到聊天 */
|
|
183
|
+
sendFile(chatId: string, filePath: string, fileName?: string): Promise<void>;
|
|
184
|
+
/** 发送本地图片到聊天 */
|
|
185
|
+
sendImage(chatId: string, imagePath: string): Promise<void>;
|
|
186
|
+
/** 下载消息中的附件资源,返回 Buffer */
|
|
187
|
+
downloadResource(messageId: string, fileKey: string, type: string): Promise<Buffer>;
|
|
188
|
+
readonly connected: boolean;
|
|
189
|
+
readonly botName?: string | undefined;
|
|
190
|
+
}
|
|
191
|
+
/** 飞书聊天里以 / 开头的机器人命令 */
|
|
192
|
+
export interface FeishuCommand {
|
|
193
|
+
name: "help" | "sessions" | "models" | "new" | "stop" | "reset" | "status";
|
|
194
|
+
args: string;
|
|
195
|
+
}
|
|
196
|
+
/** 传给 Pi 的图片输入(base64 编码) */
|
|
197
|
+
export interface ImageInput {
|
|
198
|
+
type: "image";
|
|
199
|
+
data: string;
|
|
200
|
+
mimeType: string;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* 归一化后的 Pi 输入:把飞书消息、路由结果、附件信息统一打包,
|
|
204
|
+
* 供 runtime-host 直接调用 Pi 会话。
|
|
205
|
+
*/
|
|
206
|
+
export interface NormalizedPiInput {
|
|
207
|
+
/** 会话 key(dm:xxx / group:xxx / group-user:xxx:xxx) */
|
|
208
|
+
sessionKey: string;
|
|
209
|
+
chatId: string;
|
|
210
|
+
messageId: string;
|
|
211
|
+
userId: string;
|
|
212
|
+
tenantKey?: string;
|
|
213
|
+
chatType: ChatType;
|
|
214
|
+
senderName?: string;
|
|
215
|
+
/** 拼装好的提示词文本(含群聊上下文、附件说明等) */
|
|
216
|
+
text: string;
|
|
217
|
+
images: ImageInput[];
|
|
218
|
+
/** 附件处理后的文字说明(文本内容 / 已保存路径 / 失败原因) */
|
|
219
|
+
attachmentNotes: string[];
|
|
220
|
+
/** 若消息是 /命令 则解析出命令 */
|
|
221
|
+
command?: FeishuCommand;
|
|
222
|
+
}
|
|
223
|
+
/** 会话映射:把 sessionKey 与 Pi 的会话文件、工作目录关联起来,实现跨进程持久化 */
|
|
224
|
+
export interface SessionMapping {
|
|
225
|
+
sessionKey: string;
|
|
226
|
+
appId: string;
|
|
227
|
+
tenantKey?: string;
|
|
228
|
+
chatType: ChatType;
|
|
229
|
+
chatId: string;
|
|
230
|
+
userId?: string;
|
|
231
|
+
/** Pi 会话的工作目录 */
|
|
232
|
+
cwd: string;
|
|
233
|
+
/** Pi 会话文件路径(jsonl),可用 SessionManager.open 恢复会话 */
|
|
234
|
+
sessionFile: string;
|
|
235
|
+
createdAt: string;
|
|
236
|
+
updatedAt: string;
|
|
237
|
+
}
|
|
238
|
+
/** 查询会话映射的过滤条件 */
|
|
239
|
+
export interface SessionFilter {
|
|
240
|
+
chatId?: string;
|
|
241
|
+
userId?: string;
|
|
242
|
+
chatType?: ChatType;
|
|
243
|
+
}
|
|
244
|
+
/** 会话映射存储抽象(实现:JSON 文件存储) */
|
|
245
|
+
export interface FeishuStore {
|
|
246
|
+
getSessionMapping(sessionKey: string): Promise<SessionMapping | undefined>;
|
|
247
|
+
setSessionMapping(mapping: SessionMapping): Promise<void>;
|
|
248
|
+
deleteSessionMapping(sessionKey: string): Promise<void>;
|
|
249
|
+
listSessionMappings(filter?: SessionFilter): Promise<SessionMapping[]>;
|
|
250
|
+
}
|
|
251
|
+
/** 路由成功:消息被接受,得到会话 key 和身份信息 */
|
|
252
|
+
export interface ConversationRoute {
|
|
253
|
+
accepted: true;
|
|
254
|
+
sessionKey: string;
|
|
255
|
+
identity: FeishuIdentity;
|
|
256
|
+
}
|
|
257
|
+
/** 路由拒绝:附上拒绝原因(如"未 @机器人"、"用户不在白名单") */
|
|
258
|
+
export interface RejectedConversationRoute {
|
|
259
|
+
accepted: false;
|
|
260
|
+
reason: string;
|
|
261
|
+
}
|
|
262
|
+
/** 路由结果:成功或拒绝二选一 */
|
|
263
|
+
export type ConversationRouteResult = ConversationRoute | RejectedConversationRoute;
|
|
264
|
+
/** 一次正在进行的生成运行(用于 /stop 中止) */
|
|
265
|
+
export interface ActiveRun {
|
|
266
|
+
sessionKey: string;
|
|
267
|
+
chatId: string;
|
|
268
|
+
messageId: string;
|
|
269
|
+
startedAt: Date;
|
|
270
|
+
abort(): Promise<void>;
|
|
271
|
+
}
|
|
272
|
+
/** 一个会话宿主:包含 Pi 运行时 + 消息队列 + 最近使用时间 */
|
|
273
|
+
export interface SessionHost {
|
|
274
|
+
sessionKey: string;
|
|
275
|
+
runtime: AgentSessionRuntime;
|
|
276
|
+
queue: PromptQueueLike;
|
|
277
|
+
activeRun?: ActiveRun;
|
|
278
|
+
lastUsedAt: Date;
|
|
279
|
+
}
|
|
280
|
+
/** 队列接口:保证同一会话内的提示词串行执行 */
|
|
281
|
+
export interface PromptQueueLike {
|
|
282
|
+
enqueue<T>(run: () => Promise<T>): Promise<T>;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* 飞书运行上下文:在一次"飞书请求 -> Pi 生成"的过程中,
|
|
286
|
+
* 通过 AsyncLocalStorage 传递,供 send_file_to_chat、ui.select 等能力读取。
|
|
287
|
+
*/
|
|
288
|
+
export interface FeishuRunContext {
|
|
289
|
+
sessionKey: string;
|
|
290
|
+
chatId: string;
|
|
291
|
+
messageId?: string;
|
|
292
|
+
channel: FeishuChannel;
|
|
293
|
+
uiContext: ExtensionUIContext;
|
|
294
|
+
config: FeishuConfig;
|
|
295
|
+
}
|
|
296
|
+
export type JsonPrimitive = string | number | boolean | null;
|
|
297
|
+
export type JsonValue = JsonPrimitive | JsonObject | JsonValue[];
|
|
298
|
+
export interface JsonObject {
|
|
299
|
+
[key: string]: JsonValue;
|
|
300
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-remote-feishu",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Remote control Pi Agent via Feishu (飞书) chat: WebSocket bridge, cards, attachments and file sending.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"pi",
|
|
9
|
+
"pi-extension",
|
|
10
|
+
"pi-package",
|
|
11
|
+
"feishu",
|
|
12
|
+
"lark",
|
|
13
|
+
"bot",
|
|
14
|
+
"chat-bridge",
|
|
15
|
+
"websocket"
|
|
16
|
+
],
|
|
17
|
+
"bin": {
|
|
18
|
+
"pi-remote-feishu": "./dist/bin/pi-remote-feishu.js"
|
|
19
|
+
},
|
|
20
|
+
"main": "./dist/index.js",
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"import": "./dist/index.js"
|
|
26
|
+
},
|
|
27
|
+
"./extension": {
|
|
28
|
+
"types": "./dist/extensions/index.d.ts",
|
|
29
|
+
"import": "./dist/extensions/index.js"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"pi": {
|
|
33
|
+
"extensions": [
|
|
34
|
+
"./dist/extensions/index.js"
|
|
35
|
+
],
|
|
36
|
+
"skills": [
|
|
37
|
+
"./skills"
|
|
38
|
+
]
|
|
39
|
+
},
|
|
40
|
+
"files": [
|
|
41
|
+
"dist",
|
|
42
|
+
"skills",
|
|
43
|
+
"ARCHITECTURE.md",
|
|
44
|
+
"ARCHITECTURE.zh-CN.md",
|
|
45
|
+
"README.md"
|
|
46
|
+
],
|
|
47
|
+
"scripts": {
|
|
48
|
+
"build": "tsc -p tsconfig.build.json",
|
|
49
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
50
|
+
"test": "vitest run",
|
|
51
|
+
"clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
|
|
52
|
+
"prepublishOnly": "npm run typecheck && npm test && npm run clean && npm run build"
|
|
53
|
+
},
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"@earendil-works/pi-coding-agent": "0.83.0",
|
|
56
|
+
"@larksuiteoapi/node-sdk": "1.66.0",
|
|
57
|
+
"typebox": "1.3.7"
|
|
58
|
+
},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"@types/node": "24.12.4",
|
|
61
|
+
"typescript": "5.9.3",
|
|
62
|
+
"vitest": "4.1.9"
|
|
63
|
+
},
|
|
64
|
+
"engines": {
|
|
65
|
+
"node": ">=22.19.0"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: lark-doc-cli
|
|
3
|
+
description: Use this skill for Feishu/Lark document work through lark-cli: fetch documents, create docs, append or update document content, and insert local images or files into documents. Do not use this skill to send chat messages.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Lark Doc CLI
|
|
7
|
+
|
|
8
|
+
Use this skill when the user asks to read, create, update, or enrich a Feishu/Lark document.
|
|
9
|
+
|
|
10
|
+
## Preconditions
|
|
11
|
+
|
|
12
|
+
The machine must have `lark-cli` installed and authenticated:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
lark-cli auth login
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
If authentication is missing or expired, ask the user to authenticate before continuing.
|
|
19
|
+
|
|
20
|
+
## Core Commands
|
|
21
|
+
|
|
22
|
+
Fetch a document:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
lark-cli docs +fetch --doc "<doc-url-or-token>"
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Create a document:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
lark-cli docs +create --content "<title>Title</title><p>Content</p>"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Append content to a document:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
lark-cli docs +update --doc "<doc-url-or-token>" --command append --content "<p>Content</p>"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Insert a local image or file into a document:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
lark-cli docs +media-insert --doc "<doc-url-or-token>" --file "<local-path>"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Workflow
|
|
47
|
+
|
|
48
|
+
1. Identify the document URL or token from the user request.
|
|
49
|
+
2. Fetch before modifying unless the user explicitly asks to create a new document.
|
|
50
|
+
3. Preserve document structure when summarizing or updating.
|
|
51
|
+
4. For generated images or files that belong inside a document, use document media insertion.
|
|
52
|
+
5. For generated files that should be returned to the current Feishu chat, use `send_file_to_chat` instead.
|
|
53
|
+
|
|
54
|
+
## Safety
|
|
55
|
+
|
|
56
|
+
Do not use IM send or reply commands from this skill. The Feishu chat response is handled by the pi-remote-feishu transport.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: lark-im-readonly
|
|
3
|
+
description: Use this skill for read-only Feishu/Lark IM inspection through lark-cli: find chats, list members, search existing messages, and download message resources. This skill must not send, reply, forward, delete, update, receive, or listen for messages.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Lark IM Readonly
|
|
7
|
+
|
|
8
|
+
Use this skill when the user asks to inspect Feishu/Lark IM data without sending new messages.
|
|
9
|
+
|
|
10
|
+
## Preconditions
|
|
11
|
+
|
|
12
|
+
The machine must have `lark-cli` installed and authenticated:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
lark-cli auth login
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
If authentication is missing or expired, ask the user to authenticate before continuing.
|
|
19
|
+
|
|
20
|
+
## Allowed Use
|
|
21
|
+
|
|
22
|
+
Use read-only `lark-cli im` commands for:
|
|
23
|
+
|
|
24
|
+
- Finding or inspecting chats
|
|
25
|
+
- Listing chat members
|
|
26
|
+
- Searching existing messages
|
|
27
|
+
- Downloading message resources such as files or images
|
|
28
|
+
- Inspecting message metadata needed to answer the user
|
|
29
|
+
|
|
30
|
+
Examples:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
lark-cli im +chat-members-list --chat-id "<chat-id>"
|
|
34
|
+
lark-cli im +messages-resources-download --message-id "<message-id>" --file-key "<file-key>" --type file
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Forbidden Use
|
|
38
|
+
|
|
39
|
+
Do not run `lark-cli im` commands that send, reply, forward, delete, update, receive, or listen for messages.
|
|
40
|
+
|
|
41
|
+
Forbidden examples:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
lark-cli im +messages-send ...
|
|
45
|
+
lark-cli im +messages-reply ...
|
|
46
|
+
lark-cli im +messages-receive ...
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
In a Feishu-triggered Pi run, the pi-remote-feishu transport is the only normal channel that sends the final answer back to the current chat. For local files generated by the agent, use `send_file_to_chat`.
|