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.
- package/README.md +11 -1
- package/package.json +1 -1
- package/src/agents/claude/driver.ts +13 -8
- package/src/agents/claude/session.ts +38 -6
- package/src/agents/codex/session.ts +32 -5
- package/src/channels/feishu/bot.ts +8 -4
- package/src/channels/feishu/turn-stream.ts +3 -2
- package/src/channels/web/api.ts +8 -0
- package/src/channels/web/http.ts +2 -2
- package/src/channels/web/server.ts +21 -1
- package/src/channels/web/skills-api.ts +225 -0
- package/src/channels/web/tasks-api.ts +94 -0
- package/src/core/chat-mode.ts +16 -16
- package/src/core/command-catalog.ts +8 -1
- package/src/core/commands.ts +3 -0
- package/src/core/hub.ts +147 -19
- package/src/core/memory-mcp.ts +1 -1
- package/src/core/personal.ts +5 -1
- package/src/core/skill-runtime.ts +359 -0
- package/src/core/skills-bridge.ts +288 -0
- package/src/core/skills.ts +675 -0
- package/src/core/store.ts +2 -0
- package/src/core/task-prompt.ts +38 -0
- package/src/core/task-runner.ts +232 -0
- package/src/core/task-schedule.ts +250 -0
- package/src/core/tasks-bridge.ts +256 -0
- package/src/core/tasks-mcp.ts +66 -0
- package/src/core/tasks.ts +281 -0
- package/src/core/types.ts +21 -0
- package/src/core/zip.ts +98 -0
- package/src/main.ts +12 -0
- package/src/mcp/tasks-server.ts +327 -0
- package/web/css/skills.css +368 -0
- package/web/css/tasks.css +455 -0
- package/web/index.html +2 -0
- package/web/lib/dom.js +1 -0
- package/web/lib/ws.js +8 -0
- package/web/state.js +2 -0
- package/web/views/composer.js +15 -3
- package/web/views/conv-actions.js +15 -7
- package/web/views/layout.js +22 -1
- package/web/views/mention.js +193 -0
- package/web/views/messages.js +23 -2
- package/web/views/sidebar.js +4 -0
- package/web/views/skills.js +703 -0
- package/web/views/tasks.js +469 -0
- 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
|
-
|
|
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
|
-
|
|
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())
|
|
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
|
-
|
|
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
|
-
|
|
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" &&
|
|
863
|
+
if (!conv.title && conv.channel === "web" && shown.text.trim()) {
|
|
811
864
|
// 先用首条消息占位,第一轮结束后换成 agent 自己生成的短标题(syncTitle)
|
|
812
|
-
updateConversation(key, { title:
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/core/memory-mcp.ts
CHANGED
|
@@ -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");
|
package/src/core/personal.ts
CHANGED
|
@@ -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
|
}
|