@sidleo3/dsh-chat-feishu 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/index.js +1190 -0
- package/cordis.patch.yml +5 -0
- package/host/bridge.mjs +1605 -0
- package/host/config-store.mjs +240 -0
- package/host/controller.mjs +1514 -0
- package/host/index.mjs +227 -0
- package/host/lark-cli.mjs +543 -0
- package/host/lark-gateway.mjs +1362 -0
- package/host/lark-guard.mjs +200 -0
- package/host/panel-card.mjs +681 -0
- package/host/provision.mjs +247 -0
- package/host/state-store.mjs +160 -0
- package/host/turn-presenter.mjs +778 -0
- package/lib/client.js +1132 -0
- package/lib/index.js +132126 -0
- package/package.json +54 -0
package/host/index.mjs
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-chat-feishu(host 侧):把飞书渠道注册进 hub。
|
|
3
|
+
*
|
|
4
|
+
* 本包**不 import hub 包**,只依赖运行期契约(见仓库 CONTRACT.md)。
|
|
5
|
+
*
|
|
6
|
+
* 除了注册渠道,这里还接三个 DSH 层的能力,**只针对本渠道的聊天会话**:
|
|
7
|
+
* ① `tools/pre-execute` 门禁:会话里模型自己跑的 lark-cli 必须绑定本机器人的 profile、
|
|
8
|
+
* 并按身份策略显式写 `--as`(否则"只用应用身份"那个开关对模型毫无约束力);
|
|
9
|
+
* ② `shellEnv` 事实:把本机器人的 profile 名与身份策略暴露成 `DSH_CHAT_LARK_*`;
|
|
10
|
+
* ③ 系统提示词段:让模型一上手就带对参数(门禁是兜底,不是主要交互)。
|
|
11
|
+
*
|
|
12
|
+
* @module dsh-chat-feishu/host
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { createFeishuController } from './controller.mjs';
|
|
16
|
+
|
|
17
|
+
/** 渠道包版本:设置页的「版本与更新」面板用它,`npm run check` 会与 package.json 对账。 */
|
|
18
|
+
const CHANNEL_VERSION = '0.0.4';
|
|
19
|
+
|
|
20
|
+
export const name = 'dsh-chat-feishu-host';
|
|
21
|
+
|
|
22
|
+
/** 只依赖 hub 服务;hub 未就绪时 Cordis 会自动挂起等待。 */
|
|
23
|
+
export const inject = ['dshChat'];
|
|
24
|
+
|
|
25
|
+
/** 本渠道编译期声明的契约版本,激活时与 hub 对账。 */
|
|
26
|
+
const EXPECTED_CONTRACT = 1;
|
|
27
|
+
|
|
28
|
+
const CHANNEL_ID = 'feishu';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Cordis host 插件入口。
|
|
32
|
+
*
|
|
33
|
+
* @param ctx - host 上下文。
|
|
34
|
+
*/
|
|
35
|
+
export function apply(ctx) {
|
|
36
|
+
const service = ctx.dshChat;
|
|
37
|
+
const actual = service?.contractVersion;
|
|
38
|
+
if (actual !== EXPECTED_CONTRACT) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
`dsh-chat-feishu 需要 dsh-chat 契约 v${EXPECTED_CONTRACT},当前 hub 提供 v${String(actual)};`
|
|
41
|
+
+ '请升级 dsh-chat 或安装匹配版本的渠道插件(见 CONTRACT.md)。',
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 渠道实例起来后才有的两个查询:会话归属与门禁。
|
|
47
|
+
* 事件监听在 apply 时就注册好(渠道还没起来时直接放行),避免"插件加载顺序"决定有没有门禁。
|
|
48
|
+
*/
|
|
49
|
+
let chatOwnership = null;
|
|
50
|
+
let larkGuard = null;
|
|
51
|
+
/** 提示词段的补装入口(真正的安装在本函数末尾;先声明再注册监听,避免顺序上的坑)。 */
|
|
52
|
+
let ensureIdentitySection = () => false;
|
|
53
|
+
|
|
54
|
+
ctx.on('tools/pre-execute', async (exec, next) => {
|
|
55
|
+
// 提示词段要是当初没装上(systemPrompt 服务晚到),这里顺手补一次。
|
|
56
|
+
ensureIdentitySection?.();
|
|
57
|
+
if (!larkGuard) return next();
|
|
58
|
+
let decision = null;
|
|
59
|
+
try {
|
|
60
|
+
decision = await larkGuard.evaluate(exec);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
// 门禁自己出错时**放行**并记日志:它是行为绊线,不是安全沙箱;
|
|
63
|
+
// 但绝不静默——出错这件事必须留下痕迹。
|
|
64
|
+
ctx.logger?.warn?.(`[dsh-chat-feishu] lark-cli 门禁判断失败,已放行:${error?.message ?? error}`);
|
|
65
|
+
}
|
|
66
|
+
if (decision) return decision;
|
|
67
|
+
return next();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// 会话级环境事实:模型在聊天会话里直接能拿到"该用哪个 profile、什么身份策略"。
|
|
71
|
+
registerShellFacts(ctx, () => chatOwnership);
|
|
72
|
+
|
|
73
|
+
ensureIdentitySection = installLarkIdentitySection(ctx, () => chatOwnership);
|
|
74
|
+
|
|
75
|
+
ctx.effect(() => service.registerChannel({
|
|
76
|
+
id: CHANNEL_ID,
|
|
77
|
+
label: '飞书',
|
|
78
|
+
version: CHANNEL_VERSION,
|
|
79
|
+
order: 20,
|
|
80
|
+
legacy: { dir: 'dsh-feishu' },
|
|
81
|
+
async createChannel(deps) {
|
|
82
|
+
const controller = createFeishuController({ deps, logger: deps.logger });
|
|
83
|
+
// 启动放到后台:一个机器人连不上不该拖住整个 Host 启动。
|
|
84
|
+
void controller.start().catch((error) => {
|
|
85
|
+
deps.reportStatus('failed', error);
|
|
86
|
+
deps.logger.error?.(`[dsh-chat-feishu] 启动失败:${error?.message ?? error}`);
|
|
87
|
+
});
|
|
88
|
+
chatOwnership = controller.chatOwnership;
|
|
89
|
+
larkGuard = controller.larkGuard;
|
|
90
|
+
return {
|
|
91
|
+
async stop() {
|
|
92
|
+
await controller.stop();
|
|
93
|
+
},
|
|
94
|
+
endpoints: controller.endpoints,
|
|
95
|
+
// hub 用它把"主动投递"接到该渠道上。
|
|
96
|
+
delivery: controller.delivery,
|
|
97
|
+
};
|
|
98
|
+
},
|
|
99
|
+
}), 'dsh-chat-feishu: 注册渠道');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 注册「本机器人的 lark-cli 身份策略」这段系统提示词。
|
|
104
|
+
*
|
|
105
|
+
* 为什么要有:门禁能把错的挡下来,但每挡一次模型就白跑一步。先说清楚规矩,
|
|
106
|
+
* 模型一上手就写对(`--profile <name> … --as bot`),门禁只当兜底。
|
|
107
|
+
*
|
|
108
|
+
* 没有 `systemPrompt` 服务的部署(或服务晚到)只告警一次,功能不丢——与 hub 的
|
|
109
|
+
* `prompt-context.mjs` 同一条口径:服务晚到会在下次请求前重试装上。
|
|
110
|
+
*
|
|
111
|
+
* @param ctx - host 上下文。
|
|
112
|
+
* @param ownershipOf - 返回 `chatOwnership` 的取值函数(渠道实例起来前为 null)。
|
|
113
|
+
* @returns 无。
|
|
114
|
+
*/
|
|
115
|
+
export function installLarkIdentitySection(ctx, ownershipOf) {
|
|
116
|
+
let installed = false;
|
|
117
|
+
let warned = false;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 段文本按会话现算:不是聊天会话就返回空串(空段在渲染时被丢掉),
|
|
121
|
+
* 所以这段只在"本渠道的聊天会话"里出现。
|
|
122
|
+
*
|
|
123
|
+
* 必须**同步**:`dsh-system-prompt` 的 `text` 类型就是 `string | ((ctx) => string)`。
|
|
124
|
+
*/
|
|
125
|
+
const text = (context) => {
|
|
126
|
+
const agent = context?.agent;
|
|
127
|
+
const sessionId = agent?.id ?? agent?.session?.id;
|
|
128
|
+
const lookup = ownershipOf();
|
|
129
|
+
if (typeof sessionId !== 'string' || !sessionId || typeof lookup !== 'function') return '';
|
|
130
|
+
let owner = null;
|
|
131
|
+
try {
|
|
132
|
+
owner = lookup(sessionId);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
ctx.logger?.warn?.(`[dsh-chat-feishu] 读会话归属失败:${error?.message ?? error}`);
|
|
135
|
+
return '';
|
|
136
|
+
}
|
|
137
|
+
if (!owner) return '';
|
|
138
|
+
const userAllowed = owner.mode === 'user-allowed';
|
|
139
|
+
return [
|
|
140
|
+
`本会话属于飞书机器人「${owner.botName ?? owner.botId}」,它在 lark-cli 里的身份策略是`
|
|
141
|
+
+ `${userAllowed ? '「允许用户身份」' : '「只用应用身份」'}。`,
|
|
142
|
+
'在这个会话里运行 lark-cli 的硬规矩(门禁会检查,违反直接拒绝):',
|
|
143
|
+
`1. 必须带 \`--profile ${owner.profileName}\`——这是这台机器人自己的 profile。`,
|
|
144
|
+
' 不带 profile 时 lark-cli 会用这台机器上"当前生效"的那份授权,可能是别的应用甚至别人的账号。',
|
|
145
|
+
`2. 必须显式写身份:\`--as bot\`(代表这台应用自己)${userAllowed
|
|
146
|
+
? ';要代表某个人的身份操作时才用 `--as user`。'
|
|
147
|
+
: ';本机器人只允许应用身份,`--as user` 会被拒绝。'}`,
|
|
148
|
+
'3. 不要用 `profile use` / `--use` / `config strict-mode --global` / `auth logout`——',
|
|
149
|
+
' 它们会改这台机器上 lark-cli 的全局状态,影响别人的用法。',
|
|
150
|
+
`profile 名也在环境变量 \`DSH_CHAT_LARK_PROFILE\` 里(身份策略在 \`DSH_CHAT_LARK_IDENTITY\`)。`,
|
|
151
|
+
].join('\n');
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const tryInstall = () => {
|
|
155
|
+
if (installed) return true;
|
|
156
|
+
const systemPrompt = typeof ctx.get === 'function' ? ctx.get('systemPrompt') : ctx.systemPrompt;
|
|
157
|
+
if (!systemPrompt || typeof systemPrompt.section !== 'function') return false;
|
|
158
|
+
const register = () => systemPrompt.section({
|
|
159
|
+
name: 'dsh-chat-feishu:lark-cli-identity',
|
|
160
|
+
order: 410,
|
|
161
|
+
text,
|
|
162
|
+
});
|
|
163
|
+
try {
|
|
164
|
+
if (typeof ctx.effect === 'function') ctx.effect(register, 'dsh-chat-feishu: lark-cli 身份策略段');
|
|
165
|
+
else register();
|
|
166
|
+
installed = true;
|
|
167
|
+
ctx.logger?.info?.('[dsh-chat-feishu] 已注册 lark-cli 身份策略提示词段(按会话生效)。');
|
|
168
|
+
return true;
|
|
169
|
+
} catch (error) {
|
|
170
|
+
// 同名段已存在 / ctx 已销毁:当作没装上,只告警一次,绝不影响其它功能。
|
|
171
|
+
ctx.logger?.warn?.(`[dsh-chat-feishu] 注册 lark-cli 身份策略提示词段失败:${error?.message ?? error}`);
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
if (!tryInstall() && !warned) {
|
|
177
|
+
warned = true;
|
|
178
|
+
ctx.logger?.warn?.('[dsh-chat-feishu] 当前 Host 没有可用的 systemPrompt 服务:'
|
|
179
|
+
+ 'lark-cli 身份策略只会以门禁方式生效(模型不会被提前告知)——服务晚到会在下一次工具调用前补装。');
|
|
180
|
+
}
|
|
181
|
+
return tryInstall;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* 注册会话级的 `DSH_CHAT_LARK_*` 环境事实(只有本渠道的聊天会话才拿得到)。
|
|
186
|
+
*
|
|
187
|
+
* 为什么要有:模型在会话里要知道"该用哪个 profile、身份策略是什么",才能一次写对命令。
|
|
188
|
+
* 环境事实与提示词段两条路都给,是因为模型既可能读提示词、也可能直接 `echo $DSH_CHAT_LARK_PROFILE`。
|
|
189
|
+
*
|
|
190
|
+
* 导出是为了让单测能在没有渠道实例的情况下验证取值(不需要真控制器)。
|
|
191
|
+
*
|
|
192
|
+
* @param ctx - host 上下文。
|
|
193
|
+
* @param ownershipOf - 返回 `chatOwnership(sessionId)` 的取值函数。
|
|
194
|
+
* @returns 无。
|
|
195
|
+
*/
|
|
196
|
+
export function registerShellFacts(ctx, ownershipOf) {
|
|
197
|
+
ctx.inject(['shellEnv'], (shellCtx) => {
|
|
198
|
+
shellCtx.shellEnv.register({
|
|
199
|
+
name: 'dsh-chat-feishu',
|
|
200
|
+
variables: {
|
|
201
|
+
DSH_CHAT_LARK_PROFILE: {
|
|
202
|
+
description: '这台飞书机器人在 lark-cli 里的专用 profile 名;调 lark-cli 时必须用 --profile 指定它。',
|
|
203
|
+
},
|
|
204
|
+
DSH_CHAT_LARK_IDENTITY: {
|
|
205
|
+
description: '这台机器人的 lark-cli 身份策略:bot-only(只允许应用身份)或 user-allowed(允许以该 profile 登录的用户身份)。',
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
resolve: (execution) => {
|
|
209
|
+
const sessionId = execution?.agent?.session?.header?.id;
|
|
210
|
+
const lookup = ownershipOf();
|
|
211
|
+
if (!sessionId || typeof lookup !== 'function') return {};
|
|
212
|
+
let owner = null;
|
|
213
|
+
try {
|
|
214
|
+
owner = lookup(sessionId);
|
|
215
|
+
} catch (error) {
|
|
216
|
+
ctx.logger?.warn?.(`[dsh-chat-feishu] 读会话归属失败(会话环境事实):${error?.message ?? error}`);
|
|
217
|
+
return {};
|
|
218
|
+
}
|
|
219
|
+
if (!owner) return {};
|
|
220
|
+
return {
|
|
221
|
+
DSH_CHAT_LARK_PROFILE: owner.profileName,
|
|
222
|
+
DSH_CHAT_LARK_IDENTITY: owner.mode,
|
|
223
|
+
};
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
}
|