forked from EduCraft/curriculum-project-hub
feat(hub): session transcript 连续性 + slash commands (/new /resume /reset)
修复每次 @bot 都是失忆状态的问题。 - transcript.ts: JSONL 文件存 session 对话消息(workspace/.cph/sessions/ <id>.jsonl),append-only;和 Claude Code 一致;ADR-0003 说的'不存全 历史'是群聊历史,session 对话是 Hub 自己产生的,不冲突 - runner.ts: RunRequest 加 transcriptPath;run 开始时读历史拼进初始 messages(system prompt 只在首条);run 结束时 append 本轮新消息 (loadedCount 区分历史 vs 新增,不重复写);best-effort(写失败不丢 run 结果) - trigger.ts: slash commands 在 lock 检查之前(/new /resume /reset 不创建 run 不需要锁);transcriptPath 传给 runner /new = 归档当前 session,下次 @bot 自动建新的 /resume = 恢复最近归档的 session /reset = 同 /new 未知 /cmd 透传给 agent 当普通 prompt - unit: transcript.test.ts 读写回环/append 累积/损坏行恢复/路径(6) - integration: trigger.test.ts 加 /new /resume /reset /unknown(4) 修了 slash command 被 lock 挡住的问题(提到 lock 之前)。 vitest run 45/45 通过;tsc rc=0。
This commit is contained in:
@@ -22,6 +22,7 @@ import type { ModelRegistry } from "../agent/models.js";
|
||||
import { runAgent, type ProjectContext } from "../agent/runner.js";
|
||||
import { acquireLock, currentLockRunId, releaseLock } from "../lock.js";
|
||||
import { canTriggerAgent } from "../permission.js";
|
||||
import { transcriptPath } from "../agent/transcript.js";
|
||||
|
||||
interface TriggerDeps {
|
||||
readonly prisma: PrismaClient;
|
||||
@@ -69,6 +70,52 @@ export function makeTriggerHandler(deps: TriggerDeps) {
|
||||
}
|
||||
}
|
||||
|
||||
const prompt = extractPrompt(msg);
|
||||
if (prompt === null) return;
|
||||
|
||||
// Slash commands: session management, not agent runs. These bypass the
|
||||
// lock — they don't create a run, so they can't conflict with one.
|
||||
if (prompt.startsWith("/")) {
|
||||
const cmd = prompt.split(/\s+/)[0];
|
||||
switch (cmd) {
|
||||
case "/new": {
|
||||
await deps.prisma.agentSession.updateMany({
|
||||
where: { projectId, archivedAt: null },
|
||||
data: { archivedAt: new Date() },
|
||||
});
|
||||
await sendText(rt, chatId, "已开新会话,下次 @bot 将从头开始。");
|
||||
return;
|
||||
}
|
||||
case "/resume": {
|
||||
const latest = await deps.prisma.agentSession.findFirst({
|
||||
where: { projectId, archivedAt: { not: null } },
|
||||
orderBy: { archivedAt: "desc" },
|
||||
select: { id: true },
|
||||
});
|
||||
if (latest === null) {
|
||||
await sendText(rt, chatId, "没有可恢复的会话。");
|
||||
return;
|
||||
}
|
||||
await deps.prisma.agentSession.update({
|
||||
where: { id: latest.id },
|
||||
data: { archivedAt: null },
|
||||
});
|
||||
await sendText(rt, chatId, "已恢复上一个会话。");
|
||||
return;
|
||||
}
|
||||
case "/reset": {
|
||||
await deps.prisma.agentSession.updateMany({
|
||||
where: { projectId, archivedAt: null },
|
||||
data: { archivedAt: new Date() },
|
||||
});
|
||||
await sendText(rt, chatId, "已重置,下次 @bot 将从头开始。");
|
||||
return;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ADR-0002: if locked by another run, reply busy.
|
||||
const existing = await currentLockRunId(deps.prisma, projectId);
|
||||
if (existing !== null) {
|
||||
@@ -76,9 +123,6 @@ export function makeTriggerHandler(deps: TriggerDeps) {
|
||||
return;
|
||||
}
|
||||
|
||||
const prompt = extractPrompt(msg);
|
||||
if (prompt === null) return;
|
||||
|
||||
const project = await deps.prisma.project.findUnique({
|
||||
where: { id: projectId },
|
||||
select: { workspaceDir: true },
|
||||
@@ -155,7 +199,7 @@ export function makeTriggerHandler(deps: TriggerDeps) {
|
||||
};
|
||||
|
||||
// Run the agent — fire-and-forget from the ws handler's perspective.
|
||||
runAgent(deps.provider, deps.tools, { prompt, model, project: projectCtx })
|
||||
runAgent(deps.provider, deps.tools, { prompt, model, project: projectCtx, transcriptPath: transcriptPath(project.workspaceDir, session.id) })
|
||||
.then(async (result) => {
|
||||
await deps.prisma.agentRun.update({
|
||||
where: { id: run.id },
|
||||
|
||||
Reference in New Issue
Block a user