@hwj123weijian/pi-feishu 0.10.2 → 0.11.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/README.md CHANGED
@@ -155,12 +155,13 @@ pi -e D:\ai_study\pi-feishu
155
155
  ### 群聊使用
156
156
 
157
157
  - **绑定已有群**:把机器人加进现有项目群后,Owner 在群里发送 `/bind` 即可绑定;之后该群获得独立 Pi session,与其他群和主对话互不串上下文。机器人被拉进新群时也会私聊 Owner 提醒绑定。
158
+ - **绑定项目目录**(推荐):发送 `/bind /path/to/project` 把群锚定到项目目录——群会话的工作目录会切换到该目录,bot 直接读写这个项目;Owner 显式绑定的目录会自动加入 Pi 的项目信任。私聊里说“帮我拉个群 /path/to/project”也可以在建群时一并锚定。
158
159
  - **触发方式**:默认在群里 `@机器人 你的问题`。飞书只向 bot 推送 @bot 的群消息;普通消息直接触发需要敏感权限 `im:message.group_msg`。
159
160
  - **授权成员**:默认只有 Owner 能使用。Owner 私聊发送 `/allow @张三` 后,张三即可在私聊和已绑定群里与机器人协作;`/deny @张三` 撤销。
160
161
  - **分寸感**:未绑定的群里被 @ 时回复绑定引导;非授权成员被 @ 时回复一次“未授权”;其余群内消息完全静默,不会刷屏。
161
162
  - **说话人上下文**:群消息发给 Pi 前会加上 `[飞书群聊] 说话人:` 前缀,@ 其他人会被改写成 `@名字`,模型知道在和谁对话。
162
163
 
163
- 已知限制:Pi 是单会话进程,多个聊天的任务仍全局串行(排队提示会如实显示位数);远程命令作用于最近活跃的会话,`/model`、`/thinking`、`/compact` 建议在对应聊天的上一条消息之后紧接着发送。
164
+ 已知限制:Pi 是单会话进程,群与群、群与私聊之间共享一个执行引擎,**跨聊天**的任务仍全局串行(排队提示会如实显示位数)。同一聊天内任务运行中继续发消息,会直接并入当前对话(steer),无需排队。远程命令作用于最近活跃的会话,`/model`、`/thinking`、`/compact` 建议在对应聊天的上一条消息之后紧接着发送。
164
165
 
165
166
  未知命令会返回提示,不会发给 Pi。`/model` 的候选来自本地 Pi 的 scoped models(未配置时为全部已授权模型);`/model` 无参数时展示当前模型。注意:任何以 `/` 开头的消息都会先被当作命令解析,想发给 Pi 的提问请勿以 `/` 开头。
166
167
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hwj123weijian/pi-feishu",
3
- "version": "0.10.2",
3
+ "version": "0.11.0",
4
4
  "description": "Feishu private-chat and managed-group bridge for Pi",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/contracts.ts CHANGED
@@ -9,6 +9,8 @@ export interface FeishuCredentials {
9
9
  allowlist?: string[];
10
10
  /** Display names for allowlisted open IDs, captured from @mentions when authorizing. */
11
11
  allowlistNames?: Record<string, string>;
12
+ /** Per managed group working directory (easycodeclient-style project binding). */
13
+ groupDirs?: Record<string, string>;
12
14
  }
13
15
 
14
16
  export interface CredentialStore {
@@ -153,6 +155,11 @@ export interface AgentRunOptions {
153
155
  export interface AgentBridge {
154
156
  run(text: string, observer?: AgentProgressObserver, options?: AgentRunOptions): Promise<string>;
155
157
  cancel(reason?: string): void;
158
+ /**
159
+ * 并入当前正在运行的轮次(同聊天追加消息时使用)。
160
+ * 仅当确有轮次在跑时返回 true;false 表示当前空闲,调用方应回退到正常排队。
161
+ */
162
+ steer?(text: string): boolean;
156
163
  }
157
164
 
158
165
  export type FeishuGatewayFactory = (credentials: FeishuCredentials) => FeishuGateway;
package/src/controller.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { stat } from "node:fs/promises";
2
+ import { resolve } from "node:path";
1
3
  import type {
2
4
  AgentBridge,
3
5
  CredentialStore,
@@ -202,17 +204,36 @@ export class FeishuController {
202
204
  await gateway.sendText(ownerOpenId, welcome).catch(() => undefined);
203
205
  }
204
206
 
205
- async createGroupChat(name: string): Promise<string> {
207
+ async createGroupChat(name: string, directory?: string): Promise<string> {
206
208
  const gateway = this.gateway;
207
209
  const ownerOpenId = this.credentials?.ownerOpenId;
208
210
  if (!gateway) throw new CredentialError("飞书尚未连接,请先执行 /feishu start。");
209
211
  if (!ownerOpenId) throw new CredentialError("飞书尚未绑定 Owner,无法创建群聊。");
210
212
  const chatId = await gateway.createGroupChat(name, ownerOpenId);
211
213
  this.managedGroupIds.add(chatId);
212
- await this.saveCredentials({ managedGroupIds: [...this.managedGroupIds] });
214
+ let anchoredLine = "";
215
+ if (directory) {
216
+ const resolved = resolve(directory);
217
+ let isDirectory = false;
218
+ try {
219
+ isDirectory = (await stat(resolved)).isDirectory();
220
+ } catch {
221
+ isDirectory = false;
222
+ }
223
+ if (isDirectory) {
224
+ await this.saveCredentials({
225
+ managedGroupIds: [...this.managedGroupIds],
226
+ groupDirs: { ...(this.credentials?.groupDirs ?? {}), [chatId]: resolved },
227
+ });
228
+ anchoredLine = `\n📂 本群已锚定到目录:${resolved}`;
229
+ }
230
+ }
231
+ if (!this.credentials?.groupDirs?.[chatId]) {
232
+ await this.saveCredentials({ managedGroupIds: [...this.managedGroupIds] });
233
+ }
213
234
  await gateway.sendText(
214
235
  chatId,
215
- `👋 群聊「${name}」已创建,当前绑定的 Pi 飞书机器人已就绪。直接在群内 @机器人即可开始协作。若群内普通消息没有响应,请先 @ 机器人;开通免 @ 权限后可直接发消息。`,
236
+ `👋 群聊「${name}」已创建,当前绑定的 Pi 飞书机器人已就绪。直接在群内 @机器人即可开始协作。若群内普通消息没有响应,请先 @ 机器人;开通免 @ 权限后可直接发消息。${anchoredLine}`,
216
237
  );
217
238
  await this.sendGroupPermissionReminder(gateway, ownerOpenId, name);
218
239
  return chatId;
@@ -379,9 +400,37 @@ export class FeishuController {
379
400
  await this.runRemoteCommand(gateway, message, reactionId, authorizedText);
380
401
  return;
381
402
  }
403
+ // 对齐 easycodeclient 的 mid-turn 注入:同聊天的追加消息直接并入正在运行的轮次,
404
+ // 不排队(跨聊天仍全局串行——Pi 是单会话进程)。
405
+ if (this.trySteerIntoRunningTurn(gateway, message, reactionId, authorizedText)) return;
382
406
  void this.enqueueAgentTask(gateway, message, authorizedText, reactionId);
383
407
  }
384
408
 
409
+ /** 同聊天已有任务在跑时,把新消息并入该轮次;返回是否成功并入。 */
410
+ private trySteerIntoRunningTurn(
411
+ gateway: FeishuGateway,
412
+ message: FeishuIncomingMessage,
413
+ reactionId: string,
414
+ text: string,
415
+ ): boolean {
416
+ const runningTask = [...this.pendingTasks.values()].find(
417
+ (entry) => entry.started && !entry.cancelled && entry.chatId === message.chatId,
418
+ );
419
+ if (!runningTask) return false;
420
+ const steered = this.agent.steer?.(buildAgentPrompt(message, text)) ?? false;
421
+ if (!steered) return false;
422
+ // 并入后由运行中的任务统一回复;清掉本条消息的已读表情,改用文字确认。
423
+ void this.clearRead(gateway, message.messageId, reactionId);
424
+ void gateway
425
+ .sendText(
426
+ message.chatId,
427
+ "✅ 已收到,已并入当前正在处理的对话;回复会更新在上面的回复卡片里。",
428
+ message.messageId,
429
+ )
430
+ .catch(() => undefined);
431
+ return true;
432
+ }
433
+
385
434
  private isAllowlisted(senderOpenId: string): boolean {
386
435
  return this.credentials?.allowlist?.includes(senderOpenId) ?? false;
387
436
  }
@@ -423,19 +472,28 @@ export class FeishuController {
423
472
  await gateway
424
473
  .sendText(
425
474
  message.chatId,
426
- "本群还没有绑定到 Pi。Owner 在群里发送 /bind 即可绑定本群:之后群内消息会进入该群专属的独立 Pi 会话。",
475
+ "本群还没有绑定到 Pi。Owner 在群里发送 /bind 即可绑定本群;也可带目录把群锚定到项目:/bind /path/to/project。",
427
476
  message.messageId,
428
477
  )
429
478
  .catch(() => undefined);
430
479
  }
431
480
 
432
- /** 把当前群登记为受管群并持久化。 */
481
+ /** 把当前群登记为受管群并持久化;可同时绑定工作目录(对齐 easycodeclient 的 /bind <路径>)。 */
433
482
  private async bindManagedGroup(gateway: FeishuGateway, message: FeishuIncomingMessage): Promise<void> {
483
+ const command = parseRemoteCommand(message.text);
484
+ const dirArg = command?.args.trim();
485
+ if (dirArg) {
486
+ await this.bindGroupDirectory(gateway, message, dirArg);
487
+ return;
488
+ }
434
489
  if (this.managedGroupIds.has(message.chatId)) {
490
+ const dir = this.getChatDirectory(message.chatId);
435
491
  await gateway
436
492
  .sendText(
437
493
  message.chatId,
438
- "本群已绑定到 Pi,直接 @机器人 即可提问(开通免 @ 权限后无需 @)。",
494
+ dir
495
+ ? `本群已绑定到 Pi(工作目录:${dir})。直接 @机器人 即可提问(开通免 @ 权限后无需 @)。`
496
+ : "本群已绑定到 Pi,直接 @机器人 即可提问(开通免 @ 权限后无需 @)。\n如需把本群锚定到项目目录,发送:/bind /path/to/project",
439
497
  message.messageId,
440
498
  )
441
499
  .catch(() => undefined);
@@ -446,12 +504,55 @@ export class FeishuController {
446
504
  await gateway
447
505
  .sendText(
448
506
  message.chatId,
449
- "✅ 本群已绑定到 Pi。下一条群消息会自动创建该群专属的独立 Pi 会话;默认需要 @机器人 触发。",
507
+ "✅ 本群已绑定到 Pi。下一条群消息会自动创建该群专属的独立 Pi 会话;默认需要 @机器人 触发。\n💡 如需让本群在指定项目目录下工作,发送:/bind /path/to/project",
508
+ message.messageId,
509
+ )
510
+ .catch(() => undefined);
511
+ }
512
+
513
+ /** /bind <目录>:校验目录存在后,把群锚定到该目录(工作目录在会话切换时生效)。 */
514
+ private async bindGroupDirectory(
515
+ gateway: FeishuGateway,
516
+ message: FeishuIncomingMessage,
517
+ dirArg: string,
518
+ ): Promise<void> {
519
+ const resolved = resolve(dirArg);
520
+ let isDirectory = false;
521
+ try {
522
+ isDirectory = (await stat(resolved)).isDirectory();
523
+ } catch {
524
+ isDirectory = false;
525
+ }
526
+ if (!isDirectory) {
527
+ await gateway
528
+ .sendText(message.chatId, `❌ 目录不存在或不是文件夹:${resolved}\n用法:/bind /path/to/project`, message.messageId)
529
+ .catch(() => undefined);
530
+ return;
531
+ }
532
+ this.managedGroupIds.add(message.chatId);
533
+ await this.saveCredentials({
534
+ managedGroupIds: [...this.managedGroupIds],
535
+ groupDirs: { ...(this.credentials?.groupDirs ?? {}), [message.chatId]: resolved },
536
+ });
537
+ await gateway
538
+ .sendText(
539
+ message.chatId,
540
+ `✅ 本群已锚定到目录:${resolved}\n下一条群消息会在该目录下的独立 Pi 会话中处理(已有会话也会切换工作目录)。`,
450
541
  message.messageId,
451
542
  )
452
543
  .catch(() => undefined);
453
544
  }
454
545
 
546
+ /** 群绑定的项目目录;未绑定的群返回 undefined(沿用当前 cwd)。 */
547
+ getChatDirectory(chatId: string): string | undefined {
548
+ return this.credentials?.groupDirs?.[chatId];
549
+ }
550
+
551
+ /** 所有已绑定的群目录(用于 project trust 自动放行)。 */
552
+ getBoundDirectories(): string[] {
553
+ return Object.values(this.credentials?.groupDirs ?? {});
554
+ }
555
+
455
556
  /** Controller 自己处理的命令;返回 false 时回落到通用远程命令通道。 */
456
557
  private async runManagedChatCommand(
457
558
  gateway: FeishuGateway,
@@ -70,6 +70,7 @@ export function resolveRuntimeCredentials(
70
70
  ...(matching?.groupSessions ? { groupSessions: matching.groupSessions } : {}),
71
71
  ...(matching?.allowlist ? { allowlist: matching.allowlist } : {}),
72
72
  ...(matching?.allowlistNames ? { allowlistNames: matching.allowlistNames } : {}),
73
+ ...(matching?.groupDirs ? { groupDirs: matching.groupDirs } : {}),
73
74
  };
74
75
  }
75
76
  return stored;
@@ -146,6 +147,7 @@ export class FileCredentialStore implements CredentialStore {
146
147
  ? { allowlist: value.allowlist.filter((id): id is string => typeof id === "string") }
147
148
  : {}),
148
149
  ...(isStringRecord(value.allowlistNames) ? { allowlistNames: value.allowlistNames } : {}),
150
+ ...(isStringRecord(value.groupDirs) ? { groupDirs: value.groupDirs } : {}),
149
151
  };
150
152
  }
151
153
 
@@ -185,7 +187,8 @@ function isCredentialRecord(value: unknown): value is FeishuCredentials {
185
187
  (record.managedGroupIds === undefined || Array.isArray(record.managedGroupIds)) &&
186
188
  (record.groupSessions === undefined || isStringRecord(record.groupSessions)) &&
187
189
  (record.allowlist === undefined || Array.isArray(record.allowlist)) &&
188
- (record.allowlistNames === undefined || isStringRecord(record.allowlistNames))
190
+ (record.allowlistNames === undefined || isStringRecord(record.allowlistNames)) &&
191
+ (record.groupDirs === undefined || isStringRecord(record.groupDirs))
189
192
  );
190
193
  }
191
194
 
package/src/extension.ts CHANGED
@@ -1,4 +1,20 @@
1
- import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
1
+ import type {
2
+ ExtensionAPI,
3
+ ExtensionCommandContext,
4
+ ExtensionContext,
5
+ ProjectTrustEventResult,
6
+ ProjectTrustHandler,
7
+ } from "@earendil-works/pi-coding-agent";
8
+
9
+ /** switchSession/withSession 回调里的新会话上下文;Pi 未从包根导出该类型名,从签名提取。 */
10
+ type SwitchCallback = NonNullable<
11
+ Parameters<ExtensionCommandContext["switchSession"]>[1] extends infer O | undefined
12
+ ? O extends { withSession?: (ctx: infer C) => unknown }
13
+ ? C
14
+ : never
15
+ : never
16
+ >;
17
+ type WithSessionCallback = (ctx: SwitchCallback) => Promise<void>;
2
18
  import { Type } from "typebox";
3
19
  import type { AgentBridge, FeishuStatus, PiModelInfo, PiRuntime, PiRuntimeSnapshot } from "./contracts.js";
4
20
  import { FeishuController } from "./controller.js";
@@ -30,7 +46,7 @@ interface SharedFeishuState {
30
46
  current: SessionLink | undefined;
31
47
  activeBridge: PiAgentBridge | undefined;
32
48
  latestCommandContext: ExtensionCommandContext | undefined;
33
- sendCurrentMessage: ((text: string) => void) | undefined;
49
+ sendCurrentMessage: ((text: string, options?: { deliverAs?: "steer" | "followUp" }) => void) | undefined;
34
50
  /** Pi 会话文件:p2p 私聊消息归属的主会话(绝不会是群绑定会话)。 */
35
51
  mainSessionFile: string | undefined;
36
52
  /** Pi 进程当前所在的会话文件,随每个事件刷新。 */
@@ -81,6 +97,12 @@ function getSharedState(): SharedFeishuState {
81
97
  });
82
98
  },
83
99
  cancel: (reason) => state.activeBridge?.cancel(reason),
100
+ steer: (text) => {
101
+ // 仅当确有飞书轮次在跑时并入:发到当前活跃会话(轮次就在那里),绝不触发会话切换。
102
+ if (!state.activeBridge || !state.sendCurrentMessage) return false;
103
+ state.sendCurrentMessage(text, { deliverAs: "steer" });
104
+ return true;
105
+ },
84
106
  };
85
107
  const proxyRuntime: PiRuntime = {
86
108
  isIdle: () => state.current?.runtime.isIdle() ?? true,
@@ -180,19 +202,14 @@ async function sendToChatSession(state: SharedFeishuState, chatId: string, promp
180
202
  const context = state.latestCommandContext;
181
203
  if (!context) throw new Error("Pi 会话控制尚未就绪,请先在本地执行一次 /feishu status。");
182
204
 
205
+ const directory = state.controller.getChatDirectory(chatId);
183
206
  state.switchingSession = true;
184
207
  try {
185
208
  const sessionFile = state.controller.getChatSessionFile(chatId);
186
209
  if (sessionFile) {
187
- const result = await context.switchSession(sessionFile, {
188
- withSession: async (nextContext) => {
189
- state.latestCommandContext = nextContext;
190
- const currentFile = nextContext.sessionManager.getSessionFile();
191
- if (currentFile && currentFile !== sessionFile)
192
- await state.controller.setChatSessionFile(chatId, currentFile);
193
- await nextContext.sendUserMessage(prompt);
194
- },
195
- });
210
+ const result = await switchWithDirectory(context, sessionFile, directory, (nextContext) =>
211
+ sendAndTrackGroupSession(state, chatId, sessionFile, nextContext, prompt),
212
+ );
196
213
  if (result.cancelled) throw new Error("切换到飞书群绑定的 Pi 会话已取消。");
197
214
  return;
198
215
  }
@@ -202,6 +219,15 @@ async function sendToChatSession(state: SharedFeishuState, chatId: string, promp
202
219
  state.latestCommandContext = nextContext;
203
220
  const currentFile = nextContext.sessionManager.getSessionFile();
204
221
  if (currentFile) await state.controller.setChatSessionFile(chatId, currentFile);
222
+ if (directory) {
223
+ // 新会话默认继承当前 cwd:显式重新锚定到绑定目录,再投递消息。
224
+ const anchored = await switchWithDirectory(nextContext, currentFile ?? "", directory, (finalContext) => {
225
+ state.latestCommandContext = finalContext;
226
+ return finalContext.sendUserMessage(prompt);
227
+ });
228
+ if (anchored.cancelled) throw new Error("锚定群会话工作目录已取消。");
229
+ return;
230
+ }
205
231
  await nextContext.sendUserMessage(prompt);
206
232
  },
207
233
  });
@@ -211,6 +237,60 @@ async function sendToChatSession(state: SharedFeishuState, chatId: string, promp
211
237
  }
212
238
  }
213
239
 
240
+ /**
241
+ * 切换会话并在切换完成后(含目录锚定)执行回调。
242
+ * Pi 的命令上下文 d.ts 未声明 cwdOverride,但三种运行模式的 switchSession handler
243
+ * 都会把 options 原样转发给 runtime,而 runtime 完整支持 override(切换后
244
+ * createRuntime 直接使用 override 后的 cwd)。这里做一次窄化并在切换后校验
245
+ * cwd 生效——若未来 Pi 改为丢弃该参数,会得到可操作的错误而不是静默串目录。
246
+ */
247
+ async function switchWithDirectory(
248
+ context: ExtensionCommandContext,
249
+ sessionFile: string,
250
+ directory: string | undefined,
251
+ withSession: WithSessionCallback,
252
+ ): Promise<{ cancelled: boolean }> {
253
+ if (!directory) {
254
+ return context.switchSession(sessionFile, { withSession });
255
+ }
256
+ const options = { cwdOverride: directory, withSession };
257
+ const switcher = context.switchSession as unknown as (
258
+ path: string,
259
+ options: { cwdOverride: string; withSession: WithSessionCallback },
260
+ ) => Promise<{ cancelled: boolean }>;
261
+ const result = await switcher.call(context, sessionFile, options);
262
+ if (!result.cancelled) {
263
+ const effectiveCwd = safeCwd(context);
264
+ if (effectiveCwd && effectiveCwd !== directory) {
265
+ throw new Error(
266
+ `群会话未能切换到绑定目录(当前:${effectiveCwd})。请重启本地 Pi 后重试,或在本地执行 /feishu status 刷新会话控制。`,
267
+ );
268
+ }
269
+ }
270
+ return result;
271
+ }
272
+
273
+ function safeCwd(context: ExtensionContext): string | undefined {
274
+ try {
275
+ return context.cwd;
276
+ } catch {
277
+ return undefined;
278
+ }
279
+ }
280
+
281
+ async function sendAndTrackGroupSession(
282
+ state: SharedFeishuState,
283
+ chatId: string,
284
+ sessionFile: string,
285
+ nextContext: SwitchCallback,
286
+ prompt: string,
287
+ ): Promise<void> {
288
+ state.latestCommandContext = nextContext;
289
+ const currentFile = nextContext.sessionManager.getSessionFile();
290
+ if (currentFile && currentFile !== sessionFile) await state.controller.setChatSessionFile(chatId, currentFile);
291
+ await nextContext.sendUserMessage(prompt);
292
+ }
293
+
214
294
  export interface ParsedFeishuCommand {
215
295
  name: FeishuCommandName;
216
296
  args: string;
@@ -386,7 +466,7 @@ export function renderFeishuStatus(status: FeishuStatus, scopeHealth?: string[])
386
466
 
387
467
  export default function feishuExtension(pi: ExtensionAPI): void {
388
468
  const state = getSharedState();
389
- state.sendCurrentMessage = (text) => pi.sendUserMessage(text);
469
+ state.sendCurrentMessage = (text, options) => pi.sendUserMessage(text, options);
390
470
  let latestContext: ExtensionContext | undefined;
391
471
  let latestCommandContext: ExtensionCommandContext | undefined;
392
472
 
@@ -505,25 +585,42 @@ export default function feishuExtension(pi: ExtensionAPI): void {
505
585
  state.activeBridge?.cancel("Pi 会话已关闭。");
506
586
  });
507
587
 
588
+ // 对齐 easycodeclient:Owner 通过 /bind 或建群显式绑定的目录视为受信项目,
589
+ // 免去切换群会话时的本地信任弹窗。undecided = 不表态,Pi 落回默认流程
590
+ //(runner 对 undecided 的 handler 会跳过);handler 用带注解的变量传入,
591
+ // 内联箭头会让 on() 的重载推断失败。
592
+ const onProjectTrust: ProjectTrustHandler = (event) => {
593
+ if (state.controller.getBoundDirectories().includes(event.cwd)) {
594
+ return { trusted: "yes", remember: true };
595
+ }
596
+ return { trusted: "undecided" };
597
+ };
598
+ pi.on("project_trust", onProjectTrust);
599
+
508
600
  pi.registerTool({
509
601
  name: "feishu_create_group",
510
602
  label: "Create Feishu Group",
511
603
  description:
512
- "Create a Feishu group chat and invite the currently bound owner. Use when asked to 拉群、建群、创建飞书群 or create a group. This creates only the group (not a local project directory).",
604
+ "Create a Feishu group chat and invite the currently bound owner. Use when asked to 拉群、建群、创建飞书群 or create a group. Pass `path` when the user mentions a directory (e.g. 拉个群 /path/to/project) to anchor the group's sessions to that working directory; the directory must already exist.",
513
605
  parameters: Type.Object({
514
606
  name: Type.String({ description: "Name for the new Feishu group chat" }),
607
+ path: Type.Optional(
608
+ Type.String({ description: "Absolute path of an existing local directory to anchor the group to" }),
609
+ ),
515
610
  }),
516
611
  async execute(_toolCallId, params) {
612
+ const name = params.name.trim();
517
613
  try {
518
- const chatId = await state.controller.createGroupChat(params.name.trim());
614
+ const chatId = await state.controller.createGroupChat(name, params.path?.trim() || undefined);
615
+ const anchored = params.path?.trim() ? `,并锚定到目录 ${params.path.trim()}` : "";
519
616
  return {
520
617
  content: [
521
618
  {
522
619
  type: "text",
523
- text: `群聊「${params.name.trim()}」已创建,群聊 ID:${chatId}。已邀请当前绑定的飞书用户,并发送群欢迎消息。`,
620
+ text: `群聊「${name}」已创建,群聊 ID:${chatId}${anchored}。已邀请当前绑定的飞书用户,并发送群欢迎消息。`,
524
621
  },
525
622
  ],
526
- details: { chatId, name: params.name.trim() },
623
+ details: { chatId, name },
527
624
  };
528
625
  } catch (error) {
529
626
  return {