agent-loft 0.2.31 → 0.2.33
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 +14 -5
- package/package.json +1 -1
- package/src/agents/claude/driver.ts +90 -8
- package/src/agents/claude/session.ts +81 -12
- package/src/agents/codex/session.ts +70 -13
- package/src/channels/feishu/bot.ts +2 -2
- package/src/channels/web/api.ts +72 -2
- package/src/channels/web/conv-pages.ts +68 -0
- package/src/channels/web/http.ts +2 -2
- package/src/channels/web/server.ts +20 -3
- package/src/channels/web/skills-api.ts +225 -0
- package/src/core/chat-mode.ts +3 -3
- package/src/core/command-catalog.ts +8 -1
- package/src/core/commands.ts +3 -0
- package/src/core/hub.ts +168 -18
- package/src/core/personal.ts +3 -1
- package/src/core/skill-runtime.ts +359 -0
- package/src/core/skills-bridge.ts +288 -0
- package/src/core/skills.ts +676 -0
- package/src/core/tasks-bridge.ts +5 -1
- package/src/core/tasks-mcp.ts +11 -0
- package/src/core/types.ts +44 -3
- package/src/core/usage.ts +70 -0
- package/src/core/zip.ts +98 -0
- package/src/main.ts +4 -0
- package/src/mcp/tasks-server.ts +125 -4
- package/web/css/composer.css +0 -25
- package/web/css/messages.css +169 -0
- package/web/css/sidebar.css +5 -5
- package/web/css/skills.css +385 -0
- package/web/css/topbar.css +131 -0
- package/web/index.html +1 -0
- package/web/lib/dom.js +3 -1
- package/web/lib/markdown.js +84 -14
- package/web/lib/ws.js +49 -5
- package/web/settings/index.js +7 -3
- package/web/settings/skills.js +617 -0
- package/web/state.js +4 -3
- package/web/views/composer.js +16 -3
- package/web/views/interaction.js +12 -4
- package/web/views/layout.js +22 -15
- package/web/views/mention.js +193 -0
- package/web/views/messages.js +239 -81
- package/web/views/msg-nav.js +120 -0
- package/web/views/quota.js +1 -0
- package/web/views/sidebar.js +114 -16
- package/web/views/tasks.js +11 -2
- package/web/views/topbar.js +168 -40
package/src/core/hub.ts
CHANGED
|
@@ -11,6 +11,7 @@ import crypto from "node:crypto";
|
|
|
11
11
|
import { EventEmitter } from "node:events";
|
|
12
12
|
import { config, onConfigChange, resolveWorkspacePath } from "./config.js";
|
|
13
13
|
import { attachFooters, copyFooters, formatDuration, recordFooter } from "./footers.js";
|
|
14
|
+
import { flushUsage, recordUsage, usageOf, type UsageTotals } from "./usage.js";
|
|
14
15
|
import { interactions, SETTLEMENT_LABEL } from "./interactions.js";
|
|
15
16
|
import {
|
|
16
17
|
agentBinding,
|
|
@@ -46,7 +47,9 @@ import type {
|
|
|
46
47
|
import { buildCatalog, expandCustomCommand, type AgentCommand, type CommandEntry, type CommandPrefs } from "./command-catalog.js";
|
|
47
48
|
import { helpText, type ParsedCommand } from "./commands.js";
|
|
48
49
|
import { listScheduled } from "./scheduled.js";
|
|
49
|
-
import { stripContext } from "./chat-mode.js";
|
|
50
|
+
import { stripContext, wrapContext } from "./chat-mode.js";
|
|
51
|
+
import { isMachineCommand, mentionContext, sessionSkills, skillsText } from "./skill-runtime.js";
|
|
52
|
+
import { skillEvents } from "./skills.js";
|
|
50
53
|
import { isSilentReply } from "./task-prompt.js";
|
|
51
54
|
|
|
52
55
|
// ---------- 事件 ----------
|
|
@@ -63,7 +66,15 @@ export type Origin = {
|
|
|
63
66
|
};
|
|
64
67
|
|
|
65
68
|
export type ConvEvent =
|
|
66
|
-
|
|
69
|
+
/** id:这条消息的编号,后面的 queue 事件按它对上;queued:收到时前面还有在跑 / 在等的,要排队 */
|
|
70
|
+
| { type: "user"; text: string; attachments: string[]; files?: AttachmentInfo[]; origin: Origin; id?: string; queued?: boolean }
|
|
71
|
+
/** 排队的消息:轮到它了(started,或者被当成提问的回答用掉了),或者没发出去(dropped:停止、会话切换…) */
|
|
72
|
+
| { type: "queue"; id: string; state: "started" | "dropped" }
|
|
73
|
+
/**
|
|
74
|
+
* 这里弹出过一张授权 / 提问卡(内容和状态在 interactions 里按 id 查)。只是给卡片在对话里占个位置:
|
|
75
|
+
* 刷新页面时进行中的这一轮靠重放事件画出来,没有它卡片就丢了
|
|
76
|
+
*/
|
|
77
|
+
| { type: "ix"; id: string }
|
|
67
78
|
| { type: "turn_start" }
|
|
68
79
|
| { type: "step"; kind: StepKind; text: string }
|
|
69
80
|
| { type: "delta"; text: string }
|
|
@@ -139,6 +150,8 @@ export type ConversationInfo = {
|
|
|
139
150
|
runtimeEffort?: string;
|
|
140
151
|
/** 当前 agent 会话的上下文占用(这次启动以来跑过至少一轮才有) */
|
|
141
152
|
context?: ContextUsage;
|
|
153
|
+
/** 当前 agent 会话累计消耗的 token(升级后跑过至少一轮才有) */
|
|
154
|
+
usage?: UsageTotals;
|
|
142
155
|
busy: boolean;
|
|
143
156
|
activeAt: number;
|
|
144
157
|
createdAt: number;
|
|
@@ -188,6 +201,14 @@ class Hub extends EventEmitter<{ event: [EventEnvelope]; task: [TaskTurnEnd] }>
|
|
|
188
201
|
private seq = 0;
|
|
189
202
|
/** 每条会话还没收到结果的轮数,> 0 即忙 */
|
|
190
203
|
private inflight = new Map<string, number>();
|
|
204
|
+
/**
|
|
205
|
+
* 已经交给 agent 会话、还没出结果的消息,和 inflight、turnTags 一一对应:第一条在跑,后面的在排队。
|
|
206
|
+
* 用来告诉页面哪条消息轮到了 / 被丢了,以及「打断并发送」时把哪条挪到前面
|
|
207
|
+
*/
|
|
208
|
+
private turnFifo = new Map<string, { id: string; input: UserInput; queued: boolean }[]>();
|
|
209
|
+
/** 收到了、还在串行队列里没交给 agent 的消息数:这时再来的消息也算排队 */
|
|
210
|
+
private waitingSends = new Map<string, number>();
|
|
211
|
+
private msgSeq = 0;
|
|
191
212
|
private announced = new Set<string>();
|
|
192
213
|
/** 每条会话因缓冲区溢出被丢掉的最后一个 seq */
|
|
193
214
|
private truncated = new Map<string, number>();
|
|
@@ -227,6 +248,9 @@ class Hub extends EventEmitter<{ event: [EventEnvelope]; task: [TaskTurnEnd] }>
|
|
|
227
248
|
constructor() {
|
|
228
249
|
super();
|
|
229
250
|
this.setMaxListeners(100);
|
|
251
|
+
interactions.on("created", (i) => this.emitEvent(i.convKey, { type: "ix", id: i.id }));
|
|
252
|
+
// 技能装了、改了、停用了、变量改了:受影响的人空闲的会话换上新的
|
|
253
|
+
skillEvents.on("changed", (users) => this.personalChanged(new Set(users)));
|
|
230
254
|
// workspaceRoot 改了:store 会把没显式 /cd 过的会话目录重置,正在跑的旧目录会话要作废
|
|
231
255
|
onConfigChange((next, prev) => {
|
|
232
256
|
// 默认模型可能改了,缓存作废,下次显示时重新读
|
|
@@ -457,6 +481,10 @@ class Hub extends EventEmitter<{ event: [EventEnvelope]; task: [TaskTurnEnd] }>
|
|
|
457
481
|
const ctx = binding.sessionId ? this.contextBySession.get(binding.sessionId) : undefined;
|
|
458
482
|
return ctx ? { context: ctx } : {};
|
|
459
483
|
})(),
|
|
484
|
+
...(() => {
|
|
485
|
+
const usage = usageOf(binding.sessionId);
|
|
486
|
+
return usage ? { usage } : {};
|
|
487
|
+
})(),
|
|
460
488
|
busy: this.isBusy(key),
|
|
461
489
|
activeAt: conv.activeAt,
|
|
462
490
|
createdAt: conv.createdAt,
|
|
@@ -520,6 +548,8 @@ class Hub extends EventEmitter<{ event: [EventEnvelope]; task: [TaskTurnEnd] }>
|
|
|
520
548
|
const memory = sessionMemory(conv);
|
|
521
549
|
// 定时任务工具:和记忆一样只给有主人的对话(飞书群聊里谁都能让机器人建任务,算不清是谁的)
|
|
522
550
|
const taskOwner = memoryOwnerOf(conv);
|
|
551
|
+
// 技能也跟着主人走:他启用的技能、填的变量;成员看不到机器上原有的技能(见 skill-runtime.ts)
|
|
552
|
+
const skills = sessionSkills(taskOwner);
|
|
523
553
|
const mode = conv.mode ?? "work";
|
|
524
554
|
// 续着的会话上次被告知的模式和现在不一样(新会话没有历史,算原装的工作模式)
|
|
525
555
|
const announceMode = (resumeId ? (binding.announcedMode ?? "work") : "work") !== mode;
|
|
@@ -543,6 +573,7 @@ class Hub extends EventEmitter<{ event: [EventEnvelope]; task: [TaskTurnEnd] }>
|
|
|
543
573
|
...(memory ? { memory } : {}),
|
|
544
574
|
...(announceMode ? { announceMode: true } : {}),
|
|
545
575
|
...(taskOwner ? { tasks: { userId: taskOwner, convKey: key } } : {}),
|
|
576
|
+
...(skills ? { skills } : {}),
|
|
546
577
|
},
|
|
547
578
|
{
|
|
548
579
|
onTurnStart: () => {
|
|
@@ -574,6 +605,7 @@ class Hub extends EventEmitter<{ event: [EventEnvelope]; task: [TaskTurnEnd] }>
|
|
|
574
605
|
const conv = getConversation(key);
|
|
575
606
|
recordFooter(holder.session?.sessionId ?? (conv ? agentBinding(conv, agent).sessionId : undefined), result.text, footer);
|
|
576
607
|
this.setInflight(key, (this.inflight.get(key) ?? 1) - 1);
|
|
608
|
+
this.finishTurn(key);
|
|
577
609
|
this.refreshRuntimeOnce(key, liveId);
|
|
578
610
|
this.syncTitle(key, agent);
|
|
579
611
|
this.dropIfStale(key);
|
|
@@ -600,6 +632,7 @@ class Hub extends EventEmitter<{ event: [EventEnvelope]; task: [TaskTurnEnd] }>
|
|
|
600
632
|
this.emitEvent(key, { type: "error", message: diagnosis.chatMessage });
|
|
601
633
|
}
|
|
602
634
|
this.setInflight(key, (this.inflight.get(key) ?? 1) - 1);
|
|
635
|
+
this.finishTurn(key);
|
|
603
636
|
this.dropIfStale(key);
|
|
604
637
|
},
|
|
605
638
|
onContext: (ctx) => {
|
|
@@ -615,6 +648,8 @@ class Hub extends EventEmitter<{ event: [EventEnvelope]; task: [TaskTurnEnd] }>
|
|
|
615
648
|
// Codex 一轮里会报好几次,数字没变就不打扰前端
|
|
616
649
|
if (prev?.used !== ctx.used || prev?.window !== ctx.window) this.emitEvent(key, { type: "meta" });
|
|
617
650
|
},
|
|
651
|
+
// 不用另发 meta:一轮结束的 result / error 会把会话信息(含用量)推给页面
|
|
652
|
+
onUsage: (usage, cumulative) => alive() && recordUsage(holder.session?.sessionId, usage, cumulative),
|
|
618
653
|
onDropped: (n) => {
|
|
619
654
|
if (!alive() || n <= 0) return;
|
|
620
655
|
// 丢的是排在最后、还没开始的那 n 条(Codex 先报正在跑的那轮出错、再报丢弃,那时队头已经取走了)
|
|
@@ -623,6 +658,10 @@ class Hub extends EventEmitter<{ event: [EventEnvelope]; task: [TaskTurnEnd] }>
|
|
|
623
658
|
if (tag) this.emit("task", { ...tag, key, status: "interrupted", text: "排队时被停止了" });
|
|
624
659
|
}
|
|
625
660
|
this.setInflight(key, (this.inflight.get(key) ?? n) - n);
|
|
661
|
+
// 第一条是在跑的那一轮(它自己会报结果),丢的只会是后面排队的
|
|
662
|
+
const fifo = this.turnFifo.get(key) ?? [];
|
|
663
|
+
const k = Math.min(n, Math.max(0, fifo.length - 1));
|
|
664
|
+
for (const e of fifo.splice(fifo.length - k, k)) this.emitEvent(key, { type: "queue", id: e.id, state: "dropped" });
|
|
626
665
|
},
|
|
627
666
|
onSessionId: (sessionId) => {
|
|
628
667
|
// 切走之后旧会话晚到的回调不该覆盖新绑定
|
|
@@ -696,6 +735,7 @@ class Hub extends EventEmitter<{ event: [EventEnvelope]; task: [TaskTurnEnd] }>
|
|
|
696
735
|
this.live.delete(key);
|
|
697
736
|
for (const tag of this.turnTags.get(key) ?? []) if (tag) this.emit("task", { ...tag, key, status: "interrupted", text: note });
|
|
698
737
|
this.turnTags.delete(key);
|
|
738
|
+
this.clearFifo(key);
|
|
699
739
|
this.setInflight(key, 0);
|
|
700
740
|
if (!existing) return;
|
|
701
741
|
// dispose 卡住(CLI 不响应)不能把这条会话的操作队列永远堵死
|
|
@@ -824,6 +864,8 @@ class Hub extends EventEmitter<{ event: [EventEnvelope]; task: [TaskTurnEnd] }>
|
|
|
824
864
|
attachments: input.attachments.map((a) => `${KIND_LABEL[a.kind]} ${a.name}`),
|
|
825
865
|
files: input.attachments.map(attachmentInfo),
|
|
826
866
|
origin,
|
|
867
|
+
id: `m${++this.msgSeq}`,
|
|
868
|
+
queued: false,
|
|
827
869
|
};
|
|
828
870
|
const by = `${origin.name ?? "用户"}以消息作答`;
|
|
829
871
|
|
|
@@ -858,27 +900,70 @@ class Hub extends EventEmitter<{ event: [EventEnvelope]; task: [TaskTurnEnd] }>
|
|
|
858
900
|
updateConversation(key, { title: shown.text.trim().replace(/\s+/g, " ").slice(0, 40), titleAuto: true });
|
|
859
901
|
this.emitEvent(key, { type: "meta" });
|
|
860
902
|
}
|
|
903
|
+
// 前面有在跑的、或者还在串行队列里等着的:这条要排队
|
|
904
|
+
shown.queued = this.isBusy(key) || (this.waitingSends.get(key) ?? 0) > 0;
|
|
861
905
|
this.emitEvent(key, shown);
|
|
906
|
+
const dropped = () => this.emitEvent(key, { type: "queue", id: shown.id, state: "dropped" });
|
|
862
907
|
const gone = (text: string) => {
|
|
908
|
+
dropped();
|
|
863
909
|
if (task) this.emit("task", { ...task, key, status: "interrupted", text });
|
|
864
910
|
};
|
|
911
|
+
this.waitingSends.set(key, (this.waitingSends.get(key) ?? 0) + 1);
|
|
865
912
|
void this.serial(key, async () => {
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
913
|
+
try {
|
|
914
|
+
if (cancelled()) return gone("没有发出(停止了或者会话被切换了)");
|
|
915
|
+
if (questionAtEntry && tryAnswer(questionAtEntry)) {
|
|
916
|
+
// 被当成提问的回答用掉了:不再排队
|
|
917
|
+
if (shown.queued) this.emitEvent(key, { type: "queue", id: shown.id, state: "started" });
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
// 收到之后发生过显式切换(恢复 / 分叉 / 新会话…):用户已经明确指定了会话,不再按旧的闲置时间作废它。
|
|
921
|
+
// 定时任务也不按闲置过期:每天跑一次的任务要接着上次的上下文
|
|
922
|
+
const ttl = config().sessionTtlMs;
|
|
923
|
+
const switchedAfterEntry = (this.lastSwitch.get(key)?.at ?? 0) >= g.at;
|
|
924
|
+
const expired = !task && !switchedAfterEntry && ttl > 0 && Date.now() - lastActive > ttl;
|
|
925
|
+
if (expired) {
|
|
926
|
+
await this.expireNow(key);
|
|
927
|
+
// 清理要等旧会话 dispose,期间可能又点了停止
|
|
928
|
+
if (cancelled()) return dropped();
|
|
929
|
+
}
|
|
930
|
+
const blocked = await this.blockedSlash(key, input.text);
|
|
931
|
+
if (blocked) {
|
|
932
|
+
this.emitEvent(key, { type: "notice", text: blocked });
|
|
933
|
+
return gone(blocked);
|
|
934
|
+
}
|
|
935
|
+
this.sendNow(key, this.withMentions(key, input), task ?? null, { id: shown.id, queued: shown.queued });
|
|
936
|
+
} finally {
|
|
937
|
+
this.waitingSends.set(key, Math.max(0, (this.waitingSends.get(key) ?? 1) - 1));
|
|
877
938
|
}
|
|
878
|
-
this.sendNow(key, input, task ?? null);
|
|
879
939
|
});
|
|
880
940
|
}
|
|
881
941
|
|
|
942
|
+
/**
|
|
943
|
+
* 消息里 @ 了技能:附一段背景告诉 agent 用哪个、怎么用(用户看不到,见 skill-runtime.ts 的 mentionContext)。
|
|
944
|
+
* 按发出去这一刻对话的 agent 和模式拼:排队期间可能刚切过。
|
|
945
|
+
*/
|
|
946
|
+
private withMentions(key: string, input: UserInput): UserInput {
|
|
947
|
+
const conv = getConversation(key);
|
|
948
|
+
if (!conv) return input;
|
|
949
|
+
const lines = mentionContext(stripContext(input.text), { owner: memoryOwnerOf(conv), agent: conv.agent, mode: conv.mode ?? "work" });
|
|
950
|
+
return lines ? { ...input, text: `${input.text}\n\n${wrapContext(lines)}` } : input;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/** 成员直接发「/机器上的技能」:Claude 会绕过技能白名单把它展开,这里先拦下。返回拦下的原因 */
|
|
954
|
+
private async blockedSlash(key: string, text: string): Promise<string | null> {
|
|
955
|
+
const m = /^[//](\S+)/.exec(text.trim());
|
|
956
|
+
if (!m) return null;
|
|
957
|
+
const conv = getConversation(key);
|
|
958
|
+
if (!conv || conv.agent !== "claude" || conv.mode === "chat") return null;
|
|
959
|
+
const owner = memoryOwnerOf(conv);
|
|
960
|
+
if (!owner || getUser(owner)?.role === "admin") return null;
|
|
961
|
+
const driver = this.enabledAgents().find((d) => d.id === conv.agent);
|
|
962
|
+
if (!driver?.commands) return null;
|
|
963
|
+
const native = await withTimeout(driver.commands(conv.cwd), "读取命令列表").catch(() => [] as AgentCommand[]);
|
|
964
|
+
return isMachineCommand(m[1]!, native, conv.cwd) ? `/${m[1]} 是这台机器上装的技能,只有管理员能用。` : null;
|
|
965
|
+
}
|
|
966
|
+
|
|
882
967
|
/** 刚点过停止(被打断的那一轮还可能晚到一个出错,见 onError 里的 stopped) */
|
|
883
968
|
recentlyStopped(key: string): boolean {
|
|
884
969
|
return Date.now() - (this.stoppedAt.get(key) ?? 0) < 15_000;
|
|
@@ -909,7 +994,8 @@ class Hub extends EventEmitter<{ event: [EventEnvelope]; task: [TaskTurnEnd] }>
|
|
|
909
994
|
this.emitEvent(key, { type: "meta" });
|
|
910
995
|
}
|
|
911
996
|
|
|
912
|
-
|
|
997
|
+
/** msg:页面上那条消息的编号和收到时是否排队(见 send 里的 shown) */
|
|
998
|
+
private sendNow(key: string, input: UserInput, tag: TaskTurn | null, msg: { id: string; queued: boolean }): void {
|
|
913
999
|
const fail = (err: unknown) => {
|
|
914
1000
|
const message = err instanceof Error ? err.message : String(err);
|
|
915
1001
|
this.emitEvent(key, { type: "error", message });
|
|
@@ -926,12 +1012,22 @@ class Hub extends EventEmitter<{ event: [EventEnvelope]; task: [TaskTurnEnd] }>
|
|
|
926
1012
|
const tags = this.turnTags.get(key) ?? [];
|
|
927
1013
|
tags.push(tag);
|
|
928
1014
|
this.turnTags.set(key, tags);
|
|
1015
|
+
const fifo = this.turnFifo.get(key) ?? [];
|
|
1016
|
+
fifo.push({ id: msg.id, input, queued: msg.queued });
|
|
1017
|
+
this.turnFifo.set(key, fifo);
|
|
929
1018
|
try {
|
|
930
1019
|
session.send(input);
|
|
931
1020
|
} catch (err) {
|
|
932
1021
|
tags.pop();
|
|
1022
|
+
fifo.pop();
|
|
933
1023
|
this.setInflight(key, (this.inflight.get(key) ?? 1) - 1);
|
|
934
1024
|
fail(err);
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
// 收到时在排队(前面的在串行队列里等着),交出去时前面已经空了:这就开始了
|
|
1028
|
+
if (fifo.length === 1 && msg.queued) {
|
|
1029
|
+
fifo[0]!.queued = false;
|
|
1030
|
+
this.emitEvent(key, { type: "queue", id: msg.id, state: "started" });
|
|
935
1031
|
}
|
|
936
1032
|
}
|
|
937
1033
|
|
|
@@ -1140,7 +1236,7 @@ class Hub extends EventEmitter<{ event: [EventEnvelope]; task: [TaskTurnEnd] }>
|
|
|
1140
1236
|
this.staleLive.add(live.id);
|
|
1141
1237
|
return;
|
|
1142
1238
|
}
|
|
1143
|
-
await this.drop(key, "
|
|
1239
|
+
await this.drop(key, "记忆或技能已更新");
|
|
1144
1240
|
});
|
|
1145
1241
|
}
|
|
1146
1242
|
|
|
@@ -1149,9 +1245,15 @@ class Hub extends EventEmitter<{ event: [EventEnvelope]; task: [TaskTurnEnd] }>
|
|
|
1149
1245
|
* agent 自己用 remember / forget 改的不走这里 —— 那些本来就在上下文里,没必要重启进程、白白丢掉提示缓存。
|
|
1150
1246
|
*/
|
|
1151
1247
|
memoryChanged(userId: string): void {
|
|
1248
|
+
this.personalChanged(new Set([userId]));
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
/** 这些人的记忆或技能变了:他们名下活着的会话空闲下来就重新拉起 */
|
|
1252
|
+
private personalChanged(users: Set<string>): void {
|
|
1152
1253
|
for (const [key, live] of this.live) {
|
|
1153
1254
|
const conv = getConversation(key);
|
|
1154
|
-
|
|
1255
|
+
const owner = conv ? memoryOwnerOf(conv) : undefined;
|
|
1256
|
+
if (!owner || !users.has(owner)) continue;
|
|
1155
1257
|
this.staleLive.add(live.id);
|
|
1156
1258
|
this.dropIfStale(key);
|
|
1157
1259
|
}
|
|
@@ -1204,10 +1306,51 @@ class Hub extends EventEmitter<{ event: [EventEnvelope]; task: [TaskTurnEnd] }>
|
|
|
1204
1306
|
// 和 inflight 一起清零:被打断的那一轮不一定还会报结果,留着的话下一轮的结果会被当成定时任务的
|
|
1205
1307
|
for (const tag of this.turnTags.get(key) ?? []) if (tag) this.emit("task", { ...tag, key, status: "interrupted", text: "被停止了" });
|
|
1206
1308
|
this.turnTags.delete(key);
|
|
1309
|
+
this.clearFifo(key);
|
|
1207
1310
|
this.setInflight(key, 0);
|
|
1208
1311
|
return "已请求中断。";
|
|
1209
1312
|
}
|
|
1210
1313
|
|
|
1314
|
+
/**
|
|
1315
|
+
* 打断并发送:停掉正在跑的这一轮,排着队的 id 这条挪到最前面,马上处理它;其余排着的照常接在后面。
|
|
1316
|
+
* 和停止不同,不丢排队的消息、也不清计数 —— 被打断的这一轮会正常报结果(已中断),之后就轮到它
|
|
1317
|
+
*/
|
|
1318
|
+
async jump(key: string, id: string): Promise<string> {
|
|
1319
|
+
const fifo = this.turnFifo.get(key) ?? [];
|
|
1320
|
+
const idx = fifo.findIndex((e) => e.id === id);
|
|
1321
|
+
if (idx === 0) return "这条已经在处理了。";
|
|
1322
|
+
const live = this.live.get(key);
|
|
1323
|
+
if (idx < 0 || !live) return "这条消息已经不在排队了。";
|
|
1324
|
+
const [entry] = fifo.splice(idx, 1);
|
|
1325
|
+
fifo.splice(1, 0, entry!);
|
|
1326
|
+
// turnTags 和 fifo 一一对应,一起挪
|
|
1327
|
+
const tags = this.turnTags.get(key);
|
|
1328
|
+
if (tags && tags.length === fifo.length) tags.splice(1, 0, ...tags.splice(idx, 1));
|
|
1329
|
+
this.stoppedAt.set(key, Date.now());
|
|
1330
|
+
interactions.cancelAll(key, "本轮已中断");
|
|
1331
|
+
await live.session.interrupt({ keepQueue: true, first: entry!.input });
|
|
1332
|
+
return "已打断,马上处理这条。";
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
/** 一轮出结果了:队首出队,下一条要是排过队的,告诉页面轮到它了 */
|
|
1336
|
+
private finishTurn(key: string): void {
|
|
1337
|
+
const fifo = this.turnFifo.get(key);
|
|
1338
|
+
if (!fifo) return;
|
|
1339
|
+
fifo.shift();
|
|
1340
|
+
const next = fifo[0];
|
|
1341
|
+
if (next?.queued) {
|
|
1342
|
+
next.queued = false;
|
|
1343
|
+
this.emitEvent(key, { type: "queue", id: next.id, state: "started" });
|
|
1344
|
+
}
|
|
1345
|
+
if (fifo.length === 0) this.turnFifo.delete(key);
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
/** 停止 / 会话丢了:在跑的那条不管(它的结果或中断自己会报),排着队的都没发出 */
|
|
1349
|
+
private clearFifo(key: string): void {
|
|
1350
|
+
for (const e of (this.turnFifo.get(key) ?? []).slice(1)) this.emitEvent(key, { type: "queue", id: e.id, state: "dropped" });
|
|
1351
|
+
this.turnFifo.delete(key);
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1211
1354
|
private async cdNow(key: string, dir: string, op: SwitchOp): Promise<string> {
|
|
1212
1355
|
const resolved = resolveWorkspacePath(dir);
|
|
1213
1356
|
if (!resolved) return `不允许:目录必须存在,且在 ${config().workspaceRoot} 之内(不能经符号链接跳出去)。`;
|
|
@@ -1371,12 +1514,16 @@ class Hub extends EventEmitter<{ event: [EventEnvelope]; task: [TaskTurnEnd] }>
|
|
|
1371
1514
|
switch (cmd.kind) {
|
|
1372
1515
|
case "help":
|
|
1373
1516
|
return text(helpText(ctx.allowedAgents, channel));
|
|
1517
|
+
case "skills":
|
|
1518
|
+
return text(skillsText(memoryOwnerOf(getConversation(key)!)));
|
|
1374
1519
|
case "commands": {
|
|
1375
1520
|
const conv = getConversation(key)!;
|
|
1521
|
+
const owner = memoryOwnerOf(conv);
|
|
1376
1522
|
const all = await this.commandCatalog(conv.agent, {
|
|
1377
1523
|
cwd: conv.cwd,
|
|
1378
1524
|
allowedAgents: ctx.allowedAgents,
|
|
1379
1525
|
...(channel ? { channel } : {}),
|
|
1526
|
+
...(owner && getUser(owner)?.role !== "admin" ? { member: true } : {}),
|
|
1380
1527
|
});
|
|
1381
1528
|
return { type: "commands", agent: conv.agent, commands: all.filter((c) => !c.hidden) };
|
|
1382
1529
|
}
|
|
@@ -1449,7 +1596,7 @@ class Hub extends EventEmitter<{ event: [EventEnvelope]; task: [TaskTurnEnd] }>
|
|
|
1449
1596
|
*/
|
|
1450
1597
|
async commandCatalog(
|
|
1451
1598
|
agent: string,
|
|
1452
|
-
opts: { cwd?: string; allowedAgents: string[]; channel?: string },
|
|
1599
|
+
opts: { cwd?: string; allowedAgents: string[]; channel?: string; member?: boolean },
|
|
1453
1600
|
): Promise<CommandEntry[]> {
|
|
1454
1601
|
const driver = this.enabledAgents().find((d) => d.id === agent);
|
|
1455
1602
|
let native: AgentCommand[] = [];
|
|
@@ -1459,6 +1606,8 @@ class Hub extends EventEmitter<{ event: [EventEnvelope]; task: [TaskTurnEnd] }>
|
|
|
1459
1606
|
return [];
|
|
1460
1607
|
});
|
|
1461
1608
|
}
|
|
1609
|
+
// 成员:机器上装的技能不列(用了也会被 blockedSlash 拦下)
|
|
1610
|
+
if (opts.member) native = native.filter((c) => !isMachineCommand(c.name, native, opts.cwd ?? config().workspaceRoot));
|
|
1462
1611
|
const prefs = (config().commands as Record<string, CommandPrefs | undefined>)[agent];
|
|
1463
1612
|
return buildCatalog({ allowedAgents: opts.allowedAgents, native, prefs, ...(opts.channel ? { channel: opts.channel } : {}) });
|
|
1464
1613
|
}
|
|
@@ -1466,6 +1615,7 @@ class Hub extends EventEmitter<{ event: [EventEnvelope]; task: [TaskTurnEnd] }>
|
|
|
1466
1615
|
async shutdown(): Promise<void> {
|
|
1467
1616
|
for (const key of [...this.live.keys()]) await this.drop(key, "服务正在重启");
|
|
1468
1617
|
for (const driver of this.drivers.values()) await driver.stop().catch(() => {});
|
|
1618
|
+
flushUsage();
|
|
1469
1619
|
}
|
|
1470
1620
|
}
|
|
1471
1621
|
|
package/src/core/personal.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { deleteMemory, memoryPrompt, readMemory } from "./memory.js";
|
|
|
3
3
|
import { purgeHistory } from "./chat-history.js";
|
|
4
4
|
import { unlinkUser } from "./feishu-links.js";
|
|
5
5
|
import { deleteUserTasks } from "./tasks.js";
|
|
6
|
+
import { purgeUserSkills } from "./skills.js";
|
|
6
7
|
import type { Conversation } from "./store.js";
|
|
7
8
|
import { getUser, legacyOwnerId } from "./users.js";
|
|
8
9
|
|
|
@@ -37,10 +38,11 @@ export function sessionMemory(
|
|
|
37
38
|
};
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
/**
|
|
41
|
+
/** 删用户时一起删掉的个人数据:记忆、聊天记录索引、飞书绑定、定时任务和技能(他建的技能,装了的人也就没了) */
|
|
41
42
|
export function purgeUserData(userId: string): void {
|
|
42
43
|
deleteMemory(userId);
|
|
43
44
|
purgeHistory(userId);
|
|
44
45
|
unlinkUser(userId);
|
|
45
46
|
deleteUserTasks(userId);
|
|
47
|
+
purgeUserSkills(userId);
|
|
46
48
|
}
|