agent-loft 0.2.30 → 0.2.32

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.
Files changed (47) hide show
  1. package/README.md +11 -1
  2. package/package.json +1 -1
  3. package/src/agents/claude/driver.ts +13 -8
  4. package/src/agents/claude/session.ts +38 -6
  5. package/src/agents/codex/session.ts +32 -5
  6. package/src/channels/feishu/bot.ts +8 -4
  7. package/src/channels/feishu/turn-stream.ts +3 -2
  8. package/src/channels/web/api.ts +8 -0
  9. package/src/channels/web/http.ts +2 -2
  10. package/src/channels/web/server.ts +21 -1
  11. package/src/channels/web/skills-api.ts +225 -0
  12. package/src/channels/web/tasks-api.ts +94 -0
  13. package/src/core/chat-mode.ts +16 -16
  14. package/src/core/command-catalog.ts +8 -1
  15. package/src/core/commands.ts +3 -0
  16. package/src/core/hub.ts +147 -19
  17. package/src/core/memory-mcp.ts +1 -1
  18. package/src/core/personal.ts +5 -1
  19. package/src/core/skill-runtime.ts +359 -0
  20. package/src/core/skills-bridge.ts +288 -0
  21. package/src/core/skills.ts +675 -0
  22. package/src/core/store.ts +2 -0
  23. package/src/core/task-prompt.ts +38 -0
  24. package/src/core/task-runner.ts +232 -0
  25. package/src/core/task-schedule.ts +250 -0
  26. package/src/core/tasks-bridge.ts +256 -0
  27. package/src/core/tasks-mcp.ts +66 -0
  28. package/src/core/tasks.ts +281 -0
  29. package/src/core/types.ts +21 -0
  30. package/src/core/zip.ts +98 -0
  31. package/src/main.ts +12 -0
  32. package/src/mcp/tasks-server.ts +327 -0
  33. package/web/css/skills.css +368 -0
  34. package/web/css/tasks.css +455 -0
  35. package/web/index.html +2 -0
  36. package/web/lib/dom.js +1 -0
  37. package/web/lib/ws.js +8 -0
  38. package/web/state.js +2 -0
  39. package/web/views/composer.js +15 -3
  40. package/web/views/conv-actions.js +15 -7
  41. package/web/views/layout.js +22 -1
  42. package/web/views/mention.js +193 -0
  43. package/web/views/messages.js +23 -2
  44. package/web/views/sidebar.js +4 -0
  45. package/web/views/skills.js +703 -0
  46. package/web/views/tasks.js +469 -0
  47. package/web/views/topbar.js +9 -3
package/src/core/hub.ts CHANGED
@@ -46,6 +46,10 @@ import type {
46
46
  import { buildCatalog, expandCustomCommand, type AgentCommand, type CommandEntry, type CommandPrefs } from "./command-catalog.js";
47
47
  import { helpText, type ParsedCommand } from "./commands.js";
48
48
  import { listScheduled } from "./scheduled.js";
49
+ import { stripContext, wrapContext } from "./chat-mode.js";
50
+ import { isMachineCommand, mentionContext, sessionSkills, skillsText } from "./skill-runtime.js";
51
+ import { skillEvents } from "./skills.js";
52
+ import { isSilentReply } from "./task-prompt.js";
49
53
 
50
54
  // ---------- 事件 ----------
51
55
 
@@ -66,12 +70,22 @@ export type ConvEvent =
66
70
  | { type: "step"; kind: StepKind; text: string }
67
71
  | { type: "delta"; text: string }
68
72
  | { type: "text"; text: string }
69
- | { type: "result"; text: string; footer: string; at: number; anchor?: string }
73
+ /**
74
+ * task:这一轮是定时任务发起的;silent:任务让 agent 自己判断要不要提醒,它判断这次不用 ——
75
+ * 推送、飞书都不发,网页上折叠显示
76
+ */
77
+ | { type: "result"; text: string; footer: string; at: number; anchor?: string; task?: { id: string; title: string }; silent?: true }
70
78
  | { type: "error"; message: string }
71
79
  | { type: "notice"; text: string }
72
80
  | { type: "state"; busy: boolean }
73
81
  | { type: "meta" };
74
82
 
83
+ /** 定时任务发起的一轮(见 core/task-runner.ts) */
84
+ export type TaskTurn = { taskId: string; runId: string; title: string; smart: boolean };
85
+
86
+ /** 定时任务的一轮结束了 */
87
+ export type TaskTurnEnd = TaskTurn & { key: string; status: "notified" | "silent" | "error" | "interrupted"; text: string };
88
+
75
89
  /** 一条输入是在哪个时刻收到的:停止代次 + 生命周期代次 */
76
90
  /**
77
91
  * 一条输入收到时的状态快照。
@@ -141,6 +155,8 @@ export type ConversationInfo = {
141
155
  personal?: boolean;
142
156
  /** 排着的定时消息(见 core/scheduled.ts),按发送时间先后 */
143
157
  scheduled?: { id: string; text: string; at: number }[];
158
+ /** 定时任务的专用对话 */
159
+ taskId?: string;
144
160
  };
145
161
 
146
162
  /** 普通成员点了会落盘的「总是允许」 */
@@ -162,9 +178,14 @@ function withTimeout<T>(p: Promise<T>, what: string, ms = QUERY_TIMEOUT_MS): Pro
162
178
 
163
179
  type Live = { id: number; agent: string; cwd: string; session: AgentSession };
164
180
 
165
- class Hub extends EventEmitter<{ event: [EventEnvelope] }> {
181
+ class Hub extends EventEmitter<{ event: [EventEnvelope]; task: [TaskTurnEnd] }> {
166
182
  private drivers = new Map<string, AgentDriver>();
167
183
  private live = new Map<string, Live>();
184
+ /**
185
+ * 每条会话已经送进 agent、还没出结果的轮次,按先后排:定时任务发起的带着 TaskTurn,普通消息是 null。
186
+ * 和 inflight 一样一轮一项,出结果 / 出错 / 被丢弃时从头上取走。
187
+ */
188
+ private turnTags = new Map<string, (TaskTurn | null)[]>();
168
189
  private buffers = new Map<string, EventEnvelope[]>();
169
190
  private seq = 0;
170
191
  /** 每条会话还没收到结果的轮数,> 0 即忙 */
@@ -208,6 +229,8 @@ class Hub extends EventEmitter<{ event: [EventEnvelope] }> {
208
229
  constructor() {
209
230
  super();
210
231
  this.setMaxListeners(100);
232
+ // 技能装了、改了、停用了、变量改了:受影响的人空闲的会话换上新的
233
+ skillEvents.on("changed", (users) => this.personalChanged(new Set(users)));
211
234
  // workspaceRoot 改了:store 会把没显式 /cd 过的会话目录重置,正在跑的旧目录会话要作废
212
235
  onConfigChange((next, prev) => {
213
236
  // 默认模型可能改了,缓存作废,下次显示时重新读
@@ -446,6 +469,7 @@ class Hub extends EventEmitter<{ event: [EventEnvelope] }> {
446
469
  ...(this.drivers.get(conv.agent)?.forkAt ? { canFork: true } : {}),
447
470
  mode: conv.mode ?? "work",
448
471
  ...(memoryOwnerOf(conv) ? { personal: true } : {}),
472
+ ...(conv.taskId ? { taskId: conv.taskId } : {}),
449
473
  ...(() => {
450
474
  const list = listScheduled(key);
451
475
  return list.length ? { scheduled: list.map(({ id, text, at }) => ({ id, text, at })) } : {};
@@ -498,6 +522,10 @@ class Hub extends EventEmitter<{ event: [EventEnvelope] }> {
498
522
  // 记忆在拉起会话这一刻读一次:会话里 agent 自己记下的,上下文里本来就有;
499
523
  // 用户在设置里改了,memoryChanged() 会让空闲的会话下次说话时带着新的重新拉起
500
524
  const memory = sessionMemory(conv);
525
+ // 定时任务工具:和记忆一样只给有主人的对话(飞书群聊里谁都能让机器人建任务,算不清是谁的)
526
+ const taskOwner = memoryOwnerOf(conv);
527
+ // 技能也跟着主人走:他启用的技能、填的变量;成员看不到机器上原有的技能(见 skill-runtime.ts)
528
+ const skills = sessionSkills(taskOwner);
501
529
  const mode = conv.mode ?? "work";
502
530
  // 续着的会话上次被告知的模式和现在不一样(新会话没有历史,算原装的工作模式)
503
531
  const announceMode = (resumeId ? (binding.announcedMode ?? "work") : "work") !== mode;
@@ -520,6 +548,8 @@ class Hub extends EventEmitter<{ event: [EventEnvelope] }> {
520
548
  ...(conv.mode === "chat" ? { mode: "chat" as const } : {}),
521
549
  ...(memory ? { memory } : {}),
522
550
  ...(announceMode ? { announceMode: true } : {}),
551
+ ...(taskOwner ? { tasks: { userId: taskOwner, convKey: key } } : {}),
552
+ ...(skills ? { skills } : {}),
523
553
  },
524
554
  {
525
555
  onTurnStart: () => {
@@ -536,13 +566,18 @@ class Hub extends EventEmitter<{ event: [EventEnvelope] }> {
536
566
  if (result.costUsd !== undefined) parts.push(`约 $${result.costUsd.toFixed(4)}`);
537
567
  if (result.tokens !== undefined) parts.push(`${result.tokens} tokens`);
538
568
  const footer = parts.join(" ");
569
+ const tag = this.takeTurnTag(key);
570
+ const silent = !!tag?.smart && isSilentReply(result.text);
539
571
  this.emitEvent(key, {
540
572
  type: "result",
541
573
  text: result.text,
542
574
  footer,
543
575
  at: Date.now(),
544
576
  ...(result.anchor ? { anchor: result.anchor } : {}),
577
+ ...(tag ? { task: { id: tag.taskId, title: tag.title } } : {}),
578
+ ...(silent ? { silent: true as const } : {}),
545
579
  });
580
+ if (tag) this.emit("task", { ...tag, key, status: silent ? "silent" : "notified", text: result.text });
546
581
  const conv = getConversation(key);
547
582
  recordFooter(holder.session?.sessionId ?? (conv ? agentBinding(conv, agent).sessionId : undefined), result.text, footer);
548
583
  this.setInflight(key, (this.inflight.get(key) ?? 1) - 1);
@@ -560,7 +595,12 @@ class Hub extends EventEmitter<{ event: [EventEnvelope] }> {
560
595
  console.error(`[${agent}] ${key} 出错:`, err instanceof Error ? err.message : err);
561
596
  // 这一轮已经结束了,挂着的审批没人会再等它
562
597
  interactions.cancelAll(key, "本轮出错结束");
563
- if (Date.now() - (this.stoppedAt.get(key) ?? 0) < 15_000) {
598
+ const stopped = Date.now() - (this.stoppedAt.get(key) ?? 0) < 15_000;
599
+ const tag = this.takeTurnTag(key);
600
+ if (tag) {
601
+ this.emit("task", { ...tag, key, status: stopped ? "interrupted" : "error", text: stopped ? "被停止了" : diagnosis.chatMessage });
602
+ }
603
+ if (stopped) {
564
604
  // 用户自己点的停止:中断导致的「没能正常收尾」不是故障
565
605
  this.emitEvent(key, { type: "notice", text: "已中断。" });
566
606
  } else {
@@ -584,6 +624,11 @@ class Hub extends EventEmitter<{ event: [EventEnvelope] }> {
584
624
  },
585
625
  onDropped: (n) => {
586
626
  if (!alive() || n <= 0) return;
627
+ // 丢的是排在最后、还没开始的那 n 条(Codex 先报正在跑的那轮出错、再报丢弃,那时队头已经取走了)
628
+ const tags = this.turnTags.get(key) ?? [];
629
+ for (const tag of tags.splice(Math.max(0, tags.length - n), n)) {
630
+ if (tag) this.emit("task", { ...tag, key, status: "interrupted", text: "排队时被停止了" });
631
+ }
587
632
  this.setInflight(key, (this.inflight.get(key) ?? n) - n);
588
633
  },
589
634
  onSessionId: (sessionId) => {
@@ -656,6 +701,8 @@ class Hub extends EventEmitter<{ event: [EventEnvelope] }> {
656
701
  const existing = this.live.get(key);
657
702
  if (existing) this.rememberRuntime(key, existing);
658
703
  this.live.delete(key);
704
+ for (const tag of this.turnTags.get(key) ?? []) if (tag) this.emit("task", { ...tag, key, status: "interrupted", text: note });
705
+ this.turnTags.delete(key);
659
706
  this.setInflight(key, 0);
660
707
  if (!existing) return;
661
708
  // dispose 卡住(CLI 不响应)不能把这条会话的操作队列永远堵死
@@ -747,9 +794,10 @@ class Hub extends EventEmitter<{ event: [EventEnvelope] }> {
747
794
  * gen:这条消息是在哪个停止代次下收到的。渠道自己还要先做点异步的事(飞书下载附件)
748
795
  * 才调到这里时必须传:下载期间点了停止,这条消息就不该再发出去。
749
796
  */
750
- send(key: string, input: UserInput, origin: Origin, gen?: InputGen): void {
797
+ send(key: string, input: UserInput, origin: Origin, gen?: InputGen, opts: { task?: TaskTurn } = {}): void {
751
798
  const conv = getConversation(key);
752
799
  if (!conv) throw new Error(`会话不存在:${key}`);
800
+ const task = opts.task;
753
801
  // 用户自定义命令(设置 → 命令):展开成预设的提示词再发,界面上和 agent 收到的都是展开后的内容
754
802
  const custom = (config().commands as Record<string, CommandPrefs | undefined>)[conv.agent]?.custom;
755
803
  const expanded = expandCustomCommand(input.text, custom);
@@ -772,10 +820,14 @@ class Hub extends EventEmitter<{ event: [EventEnvelope] }> {
772
820
  return false;
773
821
  };
774
822
  // 最先检查:停止之前收到的旧消息,不能去回答停止之后新冒出来的提问
775
- if (cancelled()) return;
823
+ if (cancelled()) {
824
+ if (task) this.emit("task", { ...task, key, status: "interrupted", text: "没有发出(停止了或者会话被切换了)" });
825
+ return;
826
+ }
776
827
  const shown = {
777
828
  type: "user" as const,
778
- text: input.text,
829
+ // 附给 agent 的背景(定时任务的说明)不显示
830
+ text: stripContext(input.text),
779
831
  attachments: input.attachments.map((a) => `${KIND_LABEL[a.kind]} ${a.name}`),
780
832
  files: input.attachments.map(attachmentInfo),
781
833
  origin,
@@ -787,7 +839,8 @@ class Hub extends EventEmitter<{ event: [EventEnvelope] }> {
787
839
  * 没有切换生效过;expectId 给了的话,还必须正是收到时挂着的那一个(不能去回答后来才冒出的新提问)。
788
840
  */
789
841
  const tryAnswer = (expectId?: string): boolean => {
790
- if (input.attachments.length > 0) return false;
842
+ // 定时任务的提示词不是给挂着的提问的回答
843
+ if (task || input.attachments.length > 0) return false;
791
844
  const asking = interactions.findQuestion(key);
792
845
  if (!asking || asking.owner === undefined || asking.owner !== this.liveSessionId(key)) return false;
793
846
  if (expectId !== undefined && asking.id !== expectId) return false;
@@ -807,28 +860,79 @@ class Hub extends EventEmitter<{ event: [EventEnvelope] }> {
807
860
  // 闲置过期要按这次说话之前的活跃时间判断,所以先记下、后 touch
808
861
  const lastActive = conv.activeAt;
809
862
  touchConversation(key);
810
- if (!conv.title && conv.channel === "web" && input.text.trim()) {
863
+ if (!conv.title && conv.channel === "web" && shown.text.trim()) {
811
864
  // 先用首条消息占位,第一轮结束后换成 agent 自己生成的短标题(syncTitle)
812
- updateConversation(key, { title: input.text.trim().replace(/\s+/g, " ").slice(0, 40), titleAuto: true });
865
+ updateConversation(key, { title: shown.text.trim().replace(/\s+/g, " ").slice(0, 40), titleAuto: true });
813
866
  this.emitEvent(key, { type: "meta" });
814
867
  }
815
868
  this.emitEvent(key, shown);
869
+ const gone = (text: string) => {
870
+ if (task) this.emit("task", { ...task, key, status: "interrupted", text });
871
+ };
816
872
  void this.serial(key, async () => {
817
- if (cancelled()) return;
873
+ if (cancelled()) return gone("没有发出(停止了或者会话被切换了)");
818
874
  if (questionAtEntry && tryAnswer(questionAtEntry)) return;
819
- // 收到之后发生过显式切换(恢复 / 分叉 / 新会话…):用户已经明确指定了会话,不再按旧的闲置时间作废它
875
+ // 收到之后发生过显式切换(恢复 / 分叉 / 新会话…):用户已经明确指定了会话,不再按旧的闲置时间作废它。
876
+ // 定时任务也不按闲置过期:每天跑一次的任务要接着上次的上下文
820
877
  const ttl = config().sessionTtlMs;
821
878
  const switchedAfterEntry = (this.lastSwitch.get(key)?.at ?? 0) >= g.at;
822
- const expired = !switchedAfterEntry && ttl > 0 && Date.now() - lastActive > ttl;
879
+ const expired = !task && !switchedAfterEntry && ttl > 0 && Date.now() - lastActive > ttl;
823
880
  if (expired) {
824
881
  await this.expireNow(key);
825
882
  // 清理要等旧会话 dispose,期间可能又点了停止
826
883
  if (cancelled()) return;
827
884
  }
828
- this.sendNow(key, input);
885
+ const blocked = await this.blockedSlash(key, input.text);
886
+ if (blocked) {
887
+ this.emitEvent(key, { type: "notice", text: blocked });
888
+ return gone(blocked);
889
+ }
890
+ this.sendNow(key, this.withMentions(key, input), task ?? null);
829
891
  });
830
892
  }
831
893
 
894
+ /**
895
+ * 消息里 @ 了技能:附一段背景告诉 agent 用哪个、怎么用(用户看不到,见 skill-runtime.ts 的 mentionContext)。
896
+ * 按发出去这一刻对话的 agent 和模式拼:排队期间可能刚切过。
897
+ */
898
+ private withMentions(key: string, input: UserInput): UserInput {
899
+ const conv = getConversation(key);
900
+ if (!conv) return input;
901
+ const lines = mentionContext(stripContext(input.text), { owner: memoryOwnerOf(conv), agent: conv.agent, mode: conv.mode ?? "work" });
902
+ return lines ? { ...input, text: `${input.text}\n\n${wrapContext(lines)}` } : input;
903
+ }
904
+
905
+ /** 成员直接发「/机器上的技能」:Claude 会绕过技能白名单把它展开,这里先拦下。返回拦下的原因 */
906
+ private async blockedSlash(key: string, text: string): Promise<string | null> {
907
+ const m = /^[//](\S+)/.exec(text.trim());
908
+ if (!m) return null;
909
+ const conv = getConversation(key);
910
+ if (!conv || conv.agent !== "claude" || conv.mode === "chat") return null;
911
+ const owner = memoryOwnerOf(conv);
912
+ if (!owner || getUser(owner)?.role === "admin") return null;
913
+ const driver = this.enabledAgents().find((d) => d.id === conv.agent);
914
+ if (!driver?.commands) return null;
915
+ const native = await withTimeout(driver.commands(conv.cwd), "读取命令列表").catch(() => [] as AgentCommand[]);
916
+ return isMachineCommand(m[1]!, native, conv.cwd) ? `/${m[1]} 是这台机器上装的技能,只有管理员能用。` : null;
917
+ }
918
+
919
+ /** 刚点过停止(被打断的那一轮还可能晚到一个出错,见 onError 里的 stopped) */
920
+ recentlyStopped(key: string): boolean {
921
+ return Date.now() - (this.stoppedAt.get(key) ?? 0) < 15_000;
922
+ }
923
+
924
+ /** 这条会话里正在跑的那一轮是不是定时任务发起的 */
925
+ currentTaskTurn(key: string): TaskTurn | undefined {
926
+ return this.turnTags.get(key)?.[0] ?? undefined;
927
+ }
928
+
929
+ private takeTurnTag(key: string): TaskTurn | null {
930
+ const tags = this.turnTags.get(key);
931
+ const tag = tags?.shift() ?? null;
932
+ if (tags && tags.length === 0) this.turnTags.delete(key);
933
+ return tag;
934
+ }
935
+
832
936
  /** 闲置太久:不再续用上次的会话(活着的也丢掉),开新的 */
833
937
  private async expireNow(key: string): Promise<void> {
834
938
  const conv = getConversation(key);
@@ -842,20 +946,29 @@ class Hub extends EventEmitter<{ event: [EventEnvelope] }> {
842
946
  this.emitEvent(key, { type: "meta" });
843
947
  }
844
948
 
845
- private sendNow(key: string, input: UserInput): void {
949
+ private sendNow(key: string, input: UserInput, tag: TaskTurn | null): void {
950
+ const fail = (err: unknown) => {
951
+ const message = err instanceof Error ? err.message : String(err);
952
+ this.emitEvent(key, { type: "error", message });
953
+ if (tag) this.emit("task", { ...tag, key, status: "error", text: message });
954
+ };
846
955
  let session: AgentSession;
847
956
  try {
848
957
  session = this.session(key);
849
958
  } catch (err) {
850
- this.emitEvent(key, { type: "error", message: err instanceof Error ? err.message : String(err) });
959
+ fail(err);
851
960
  return;
852
961
  }
853
962
  this.setInflight(key, (this.inflight.get(key) ?? 0) + 1);
963
+ const tags = this.turnTags.get(key) ?? [];
964
+ tags.push(tag);
965
+ this.turnTags.set(key, tags);
854
966
  try {
855
967
  session.send(input);
856
968
  } catch (err) {
969
+ tags.pop();
857
970
  this.setInflight(key, (this.inflight.get(key) ?? 1) - 1);
858
- this.emitEvent(key, { type: "error", message: err instanceof Error ? err.message : String(err) });
971
+ fail(err);
859
972
  }
860
973
  }
861
974
 
@@ -1064,7 +1177,7 @@ class Hub extends EventEmitter<{ event: [EventEnvelope] }> {
1064
1177
  this.staleLive.add(live.id);
1065
1178
  return;
1066
1179
  }
1067
- await this.drop(key, "记忆已更新");
1180
+ await this.drop(key, "记忆或技能已更新");
1068
1181
  });
1069
1182
  }
1070
1183
 
@@ -1073,9 +1186,15 @@ class Hub extends EventEmitter<{ event: [EventEnvelope] }> {
1073
1186
  * agent 自己用 remember / forget 改的不走这里 —— 那些本来就在上下文里,没必要重启进程、白白丢掉提示缓存。
1074
1187
  */
1075
1188
  memoryChanged(userId: string): void {
1189
+ this.personalChanged(new Set([userId]));
1190
+ }
1191
+
1192
+ /** 这些人的记忆或技能变了:他们名下活着的会话空闲下来就重新拉起 */
1193
+ private personalChanged(users: Set<string>): void {
1076
1194
  for (const [key, live] of this.live) {
1077
1195
  const conv = getConversation(key);
1078
- if (!conv || memoryOwnerOf(conv) !== userId) continue;
1196
+ const owner = conv ? memoryOwnerOf(conv) : undefined;
1197
+ if (!owner || !users.has(owner)) continue;
1079
1198
  this.staleLive.add(live.id);
1080
1199
  this.dropIfStale(key);
1081
1200
  }
@@ -1125,6 +1244,9 @@ class Hub extends EventEmitter<{ event: [EventEnvelope] }> {
1125
1244
  this.stoppedAt.set(key, Date.now());
1126
1245
  interactions.cancelAll(key, "本轮已中断");
1127
1246
  await this.live.get(key)?.session.interrupt();
1247
+ // 和 inflight 一起清零:被打断的那一轮不一定还会报结果,留着的话下一轮的结果会被当成定时任务的
1248
+ for (const tag of this.turnTags.get(key) ?? []) if (tag) this.emit("task", { ...tag, key, status: "interrupted", text: "被停止了" });
1249
+ this.turnTags.delete(key);
1128
1250
  this.setInflight(key, 0);
1129
1251
  return "已请求中断。";
1130
1252
  }
@@ -1292,12 +1414,16 @@ class Hub extends EventEmitter<{ event: [EventEnvelope] }> {
1292
1414
  switch (cmd.kind) {
1293
1415
  case "help":
1294
1416
  return text(helpText(ctx.allowedAgents, channel));
1417
+ case "skills":
1418
+ return text(skillsText(memoryOwnerOf(getConversation(key)!)));
1295
1419
  case "commands": {
1296
1420
  const conv = getConversation(key)!;
1421
+ const owner = memoryOwnerOf(conv);
1297
1422
  const all = await this.commandCatalog(conv.agent, {
1298
1423
  cwd: conv.cwd,
1299
1424
  allowedAgents: ctx.allowedAgents,
1300
1425
  ...(channel ? { channel } : {}),
1426
+ ...(owner && getUser(owner)?.role !== "admin" ? { member: true } : {}),
1301
1427
  });
1302
1428
  return { type: "commands", agent: conv.agent, commands: all.filter((c) => !c.hidden) };
1303
1429
  }
@@ -1370,7 +1496,7 @@ class Hub extends EventEmitter<{ event: [EventEnvelope] }> {
1370
1496
  */
1371
1497
  async commandCatalog(
1372
1498
  agent: string,
1373
- opts: { cwd?: string; allowedAgents: string[]; channel?: string },
1499
+ opts: { cwd?: string; allowedAgents: string[]; channel?: string; member?: boolean },
1374
1500
  ): Promise<CommandEntry[]> {
1375
1501
  const driver = this.enabledAgents().find((d) => d.id === agent);
1376
1502
  let native: AgentCommand[] = [];
@@ -1380,6 +1506,8 @@ class Hub extends EventEmitter<{ event: [EventEnvelope] }> {
1380
1506
  return [];
1381
1507
  });
1382
1508
  }
1509
+ // 成员:机器上装的技能不列(用了也会被 blockedSlash 拦下)
1510
+ if (opts.member) native = native.filter((c) => !isMachineCommand(c.name, native, opts.cwd ?? config().workspaceRoot));
1383
1511
  const prefs = (config().commands as Record<string, CommandPrefs | undefined>)[agent];
1384
1512
  return buildCatalog({ allowedAgents: opts.allowedAgents, native, prefs, ...(opts.channel ? { channel: opts.channel } : {}) });
1385
1513
  }
@@ -9,7 +9,7 @@ export const MEMORY_MCP_NAME = "agent_loft_memory";
9
9
  const serverPath = fileURLToPath(new URL("../mcp/memory-server.ts", import.meta.url));
10
10
 
11
11
  /** tsx 的 ESM loader:源码是 TypeScript,子进程和主进程一样靠它直接跑 */
12
- const tsxLoader = (() => {
12
+ export const tsxLoader = (() => {
13
13
  const require = createRequire(import.meta.url);
14
14
  // tsx/esm 的导出只有 import 条件,require.resolve 解不到;从包目录拼出来
15
15
  const pkg = require.resolve("tsx/package.json");
@@ -2,6 +2,8 @@
2
2
  import { deleteMemory, memoryPrompt, readMemory } from "./memory.js";
3
3
  import { purgeHistory } from "./chat-history.js";
4
4
  import { unlinkUser } from "./feishu-links.js";
5
+ import { deleteUserTasks } from "./tasks.js";
6
+ import { purgeUserSkills } from "./skills.js";
5
7
  import type { Conversation } from "./store.js";
6
8
  import { getUser, legacyOwnerId } from "./users.js";
7
9
 
@@ -36,9 +38,11 @@ export function sessionMemory(
36
38
  };
37
39
  }
38
40
 
39
- /** 删用户时一起删掉的个人数据:记忆、聊天记录索引和飞书绑定 */
41
+ /** 删用户时一起删掉的个人数据:记忆、聊天记录索引、飞书绑定、定时任务和技能(他建的技能,装了的人也就没了) */
40
42
  export function purgeUserData(userId: string): void {
41
43
  deleteMemory(userId);
42
44
  purgeHistory(userId);
43
45
  unlinkUser(userId);
46
+ deleteUserTasks(userId);
47
+ purgeUserSkills(userId);
44
48
  }