team-anya 0.2.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 +38 -0
- package/apps/server/dist/broker/cc-broker.js +267 -0
- package/apps/server/dist/cli.js +296 -0
- package/apps/server/dist/config.js +78 -0
- package/apps/server/dist/daemon.js +51 -0
- package/apps/server/dist/franky/context-builder.js +161 -0
- package/apps/server/dist/franky/franky-mcp-server.js +110 -0
- package/apps/server/dist/franky/franky-orchestrator.js +629 -0
- package/apps/server/dist/franky/index.js +5 -0
- package/apps/server/dist/franky/topic-router.js +16 -0
- package/apps/server/dist/gateway/chat-sync.js +135 -0
- package/apps/server/dist/gateway/command-router.js +116 -0
- package/apps/server/dist/gateway/commands/cancel.js +32 -0
- package/apps/server/dist/gateway/commands/help.js +16 -0
- package/apps/server/dist/gateway/commands/index.js +26 -0
- package/apps/server/dist/gateway/commands/restart.js +43 -0
- package/apps/server/dist/gateway/commands/status.js +34 -0
- package/apps/server/dist/gateway/commands/tasks.js +33 -0
- package/apps/server/dist/gateway/feishu-sender.js +508 -0
- package/apps/server/dist/gateway/feishu-ws.js +353 -0
- package/apps/server/dist/gateway/health-monitor.js +154 -0
- package/apps/server/dist/gateway/http.js +1064 -0
- package/apps/server/dist/gateway/media-downloader.js +182 -0
- package/apps/server/dist/gateway/message-events.js +10 -0
- package/apps/server/dist/gateway/message-intake.js +72 -0
- package/apps/server/dist/gateway/message-queue.js +118 -0
- package/apps/server/dist/gateway/session-reader.js +142 -0
- package/apps/server/dist/gateway/ws-push.js +115 -0
- package/apps/server/dist/loid/brain.js +121 -0
- package/apps/server/dist/loid/clarifier.js +162 -0
- package/apps/server/dist/loid/context-builder.js +462 -0
- package/apps/server/dist/loid/mcp-server.js +119 -0
- package/apps/server/dist/loid/memory-settler.js +189 -0
- package/apps/server/dist/loid/opportunity-manager.js +148 -0
- package/apps/server/dist/loid/profile-updater.js +179 -0
- package/apps/server/dist/loid/project-registry.js +192 -0
- package/apps/server/dist/loid/reporter.js +148 -0
- package/apps/server/dist/loid/schemas.js +117 -0
- package/apps/server/dist/loid/self-calibrator.js +314 -0
- package/apps/server/dist/loid/session-manager.js +472 -0
- package/apps/server/dist/loid/session.js +276 -0
- package/apps/server/dist/main.js +528 -0
- package/apps/server/dist/tracing/index.js +2 -0
- package/apps/server/dist/tracing/trace-context.js +92 -0
- package/apps/server/dist/types/message.js +2 -0
- package/apps/server/dist/yor/yor-mcp-server.js +107 -0
- package/apps/server/dist/yor/yor-orchestrator.js +248 -0
- package/apps/web/dist/assets/index-BiiEB0qZ.css +1 -0
- package/apps/web/dist/assets/index-Dnb9LGZd.js +798 -0
- package/apps/web/dist/index.html +13 -0
- package/package.json +42 -0
- package/packages/cc-client/dist/claude-code-backend.js +792 -0
- package/packages/cc-client/dist/index.js +2 -0
- package/packages/cc-client/package.json +11 -0
- package/packages/core/dist/constants.js +60 -0
- package/packages/core/dist/errors.js +35 -0
- package/packages/core/dist/index.js +9 -0
- package/packages/core/dist/office-init.js +190 -0
- package/packages/core/dist/repo-cache.js +70 -0
- package/packages/core/dist/scope/checker.js +114 -0
- package/packages/core/dist/scope/defaults.js +55 -0
- package/packages/core/dist/scope/index.js +3 -0
- package/packages/core/dist/state-machine.js +86 -0
- package/packages/core/dist/types/audit.js +12 -0
- package/packages/core/dist/types/backend.js +2 -0
- package/packages/core/dist/types/commitment.js +17 -0
- package/packages/core/dist/types/communication.js +18 -0
- package/packages/core/dist/types/index.js +9 -0
- package/packages/core/dist/types/opportunity.js +27 -0
- package/packages/core/dist/types/org.js +26 -0
- package/packages/core/dist/types/task.js +46 -0
- package/packages/core/dist/types/workspace.js +39 -0
- package/packages/core/dist/workspace-manager.js +314 -0
- package/packages/core/package.json +10 -0
- package/packages/db/dist/client.js +69 -0
- package/packages/db/dist/index.js +756 -0
- package/packages/db/dist/schema/audit-events.js +13 -0
- package/packages/db/dist/schema/cc-sessions.js +14 -0
- package/packages/db/dist/schema/chats.js +35 -0
- package/packages/db/dist/schema/commitments.js +18 -0
- package/packages/db/dist/schema/communication-events.js +14 -0
- package/packages/db/dist/schema/index.js +14 -0
- package/packages/db/dist/schema/message-log.js +20 -0
- package/packages/db/dist/schema/opportunities.js +23 -0
- package/packages/db/dist/schema/org.js +36 -0
- package/packages/db/dist/schema/projects.js +23 -0
- package/packages/db/dist/schema/tasks.js +51 -0
- package/packages/db/dist/schema/topics.js +22 -0
- package/packages/db/dist/schema/trace-spans.js +19 -0
- package/packages/db/dist/schema/workspaces.js +15 -0
- package/packages/db/package.json +12 -0
- package/packages/db/src/migrations/0000_baseline.sql +251 -0
- package/packages/db/src/migrations/0001_workspaces.sql +19 -0
- package/packages/db/src/migrations/0002_workspace_parent.sql +1 -0
- package/packages/db/src/migrations/0003_chat_context.sql +3 -0
- package/packages/db/src/migrations/meta/_journal.json +34 -0
- package/packages/mcp-tools/dist/index.js +41 -0
- package/packages/mcp-tools/dist/layer1/audit-append.js +38 -0
- package/packages/mcp-tools/dist/layer1/audit-query.js +51 -0
- package/packages/mcp-tools/dist/layer1/memory-brief.js +168 -0
- package/packages/mcp-tools/dist/layer1/memory-context.js +124 -0
- package/packages/mcp-tools/dist/layer1/memory-digest.js +126 -0
- package/packages/mcp-tools/dist/layer1/memory-forget.js +108 -0
- package/packages/mcp-tools/dist/layer1/memory-learn.js +63 -0
- package/packages/mcp-tools/dist/layer1/memory-recall.js +287 -0
- package/packages/mcp-tools/dist/layer1/memory-reflect.js +80 -0
- package/packages/mcp-tools/dist/layer1/memory-remember.js +119 -0
- package/packages/mcp-tools/dist/layer1/memory-search.js +263 -0
- package/packages/mcp-tools/dist/layer1/memory-write.js +21 -0
- package/packages/mcp-tools/dist/layer1/org-lookup.js +47 -0
- package/packages/mcp-tools/dist/layer1/project-get.js +28 -0
- package/packages/mcp-tools/dist/layer1/project-list.js +20 -0
- package/packages/mcp-tools/dist/layer1/report-daily.js +68 -0
- package/packages/mcp-tools/dist/layer1/task-get.js +29 -0
- package/packages/mcp-tools/dist/layer1/task-update.js +34 -0
- package/packages/mcp-tools/dist/layer2/franky/topic-checkpoint.js +43 -0
- package/packages/mcp-tools/dist/layer2/franky/topic-escalate.js +19 -0
- package/packages/mcp-tools/dist/layer2/loid/decision-log.js +15 -0
- package/packages/mcp-tools/dist/layer2/loid/decision-no-action.js +15 -0
- package/packages/mcp-tools/dist/layer2/loid/delivery-create-pr.js +30 -0
- package/packages/mcp-tools/dist/layer2/loid/delivery-share.js +12 -0
- package/packages/mcp-tools/dist/layer2/loid/delivery-submit.js +77 -0
- package/packages/mcp-tools/dist/layer2/loid/delivery-upload.js +18 -0
- package/packages/mcp-tools/dist/layer2/loid/project-remove.js +16 -0
- package/packages/mcp-tools/dist/layer2/loid/project-upsert.js +33 -0
- package/packages/mcp-tools/dist/layer2/loid/task-dispatch.js +206 -0
- package/packages/mcp-tools/dist/layer2/loid/task-escalate-to-topic.js +170 -0
- package/packages/mcp-tools/dist/layer2/loid/task-lookup.js +45 -0
- package/packages/mcp-tools/dist/layer2/loid/topic-close.js +22 -0
- package/packages/mcp-tools/dist/layer2/loid/topic-create.js +60 -0
- package/packages/mcp-tools/dist/layer2/loid/yor-approve.js +8 -0
- package/packages/mcp-tools/dist/layer2/loid/yor-kill.js +7 -0
- package/packages/mcp-tools/dist/layer2/loid/yor-rework.js +7 -0
- package/packages/mcp-tools/dist/layer2/loid/yor-spawn.js +28 -0
- package/packages/mcp-tools/dist/layer2/loid/yor-status.js +8 -0
- package/packages/mcp-tools/dist/layer2/yor/task-block.js +11 -0
- package/packages/mcp-tools/dist/layer2/yor/task-deliver.js +35 -0
- package/packages/mcp-tools/dist/layer2/yor/task-progress.js +21 -0
- package/packages/mcp-tools/dist/layer3/adapters/feishu-adapter.js +203 -0
- package/packages/mcp-tools/dist/layer3/adapters/types.js +28 -0
- package/packages/mcp-tools/dist/layer3/channel-receive.js +11 -0
- package/packages/mcp-tools/dist/layer3/channel-send.js +75 -0
- package/packages/mcp-tools/dist/layer3/file-upload.js +44 -0
- package/packages/mcp-tools/dist/registry.js +911 -0
- package/packages/mcp-tools/package.json +13 -0
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { writeFile } from 'node:fs/promises';
|
|
3
|
+
import { ensureLoidWorkspace } from '@team-anya/core';
|
|
4
|
+
import { getTasksByStatus, getChatContext, getCCSessionsByChat, updateCCSessionEnded } from '@team-anya/db';
|
|
5
|
+
import { TaskStatus } from '@team-anya/core';
|
|
6
|
+
import { LoidSession, } from './session.js';
|
|
7
|
+
import { formatMessageContextPrompt, formatFollowUpPrompt, formatDeliveryContextPrompt, formatRecoveryPrompt, } from './context-builder.js';
|
|
8
|
+
// ── LoidSessionManager ──
|
|
9
|
+
export class LoidSessionManager {
|
|
10
|
+
sessions = new Map();
|
|
11
|
+
config;
|
|
12
|
+
deps;
|
|
13
|
+
logger;
|
|
14
|
+
/** 崩溃恢复中的话术池(像同事重启电脑一样自然) */
|
|
15
|
+
static CRASH_RECOVERY_REPLIES = [
|
|
16
|
+
'抱歉,刚才脑子卡了一下,我重新捋一下',
|
|
17
|
+
'不好意思,刚才断片了,我重新接上',
|
|
18
|
+
'刚才出了点状况,我重新来过',
|
|
19
|
+
'抱歉打断了,我重新整理一下思路',
|
|
20
|
+
'不好意思,刚才掉线了,马上接上',
|
|
21
|
+
];
|
|
22
|
+
lastCrashRecoveryIndex = -1;
|
|
23
|
+
/** 限流请假话术池(像同事请假一样自然) */
|
|
24
|
+
static RATE_LIMIT_REPLIES = [
|
|
25
|
+
'🙋 抱歉,我今天处理太多事情了,需要歇一会儿。预计 {time} 恢复工作,这段时间新消息会排队,回来后优先处理。',
|
|
26
|
+
'🙋 不好意思,我体力不支了需要休息一下。预计 {time} 回来继续干活,不用担心,消息我都记着。',
|
|
27
|
+
'🙋 不好意思,我需要请个短假,大概 {time} 回来。这段时间的消息我会攒着,一回来就处理。',
|
|
28
|
+
];
|
|
29
|
+
lastRateLimitIndex = -1;
|
|
30
|
+
/** 崩溃恢复失败 / 启动失败的话术池 */
|
|
31
|
+
static STARTUP_FAILURE_REPLIES = [
|
|
32
|
+
'抱歉,我这边遇到了点问题暂时处理不了,稍后我再试试,或者你可以再发一次',
|
|
33
|
+
'不好意思,出了点技术问题我暂时响应不了,你可以稍后再@我试试',
|
|
34
|
+
'抱歉,我这边卡住了,短时间内可能没法响应,你可以过一会儿再找我',
|
|
35
|
+
];
|
|
36
|
+
lastStartupFailureIndex = -1;
|
|
37
|
+
constructor(config, deps) {
|
|
38
|
+
this.config = config;
|
|
39
|
+
this.deps = deps;
|
|
40
|
+
this.logger = config.logger ?? { info: console.log, error: console.error };
|
|
41
|
+
// 监听 Loid 实例限流事件
|
|
42
|
+
this.config.broker.on('instance.rate_limited', (instanceId, role, result) => {
|
|
43
|
+
if (role !== 'loid')
|
|
44
|
+
return;
|
|
45
|
+
const chatId = this.findChatIdByInstanceId(instanceId);
|
|
46
|
+
if (chatId && result.rateLimitInfo) {
|
|
47
|
+
this.sendRateLimitNotification(chatId, 'Loid', result.rateLimitInfo);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
/** 从话术池随机取一句,避免连续重复 */
|
|
52
|
+
pickRandom(pool, lastIndex) {
|
|
53
|
+
let idx;
|
|
54
|
+
do {
|
|
55
|
+
idx = Math.floor(Math.random() * pool.length);
|
|
56
|
+
} while (idx === lastIndex && pool.length > 1);
|
|
57
|
+
return { text: pool[idx], index: idx };
|
|
58
|
+
}
|
|
59
|
+
pickCrashRecoveryReply() {
|
|
60
|
+
const { text, index } = this.pickRandom(LoidSessionManager.CRASH_RECOVERY_REPLIES, this.lastCrashRecoveryIndex);
|
|
61
|
+
this.lastCrashRecoveryIndex = index;
|
|
62
|
+
return text;
|
|
63
|
+
}
|
|
64
|
+
pickStartupFailureReply() {
|
|
65
|
+
const { text, index } = this.pickRandom(LoidSessionManager.STARTUP_FAILURE_REPLIES, this.lastStartupFailureIndex);
|
|
66
|
+
this.lastStartupFailureIndex = index;
|
|
67
|
+
return text;
|
|
68
|
+
}
|
|
69
|
+
// ── 公开方法 ──
|
|
70
|
+
/**
|
|
71
|
+
* 处理新消息:有活跃 session 就 enqueue,没有就创建
|
|
72
|
+
*/
|
|
73
|
+
async handleMessage(chatId, ctx) {
|
|
74
|
+
const key = chatId;
|
|
75
|
+
const existing = this.sessions.get(key);
|
|
76
|
+
if (existing) {
|
|
77
|
+
const state = existing.getState();
|
|
78
|
+
if (state !== 'disposed') {
|
|
79
|
+
const prompt = formatFollowUpPrompt(ctx);
|
|
80
|
+
existing.enqueue({ type: 'message', prompt, enqueuedAt: new Date() });
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
// session 已销毁,删除并新建
|
|
84
|
+
this.sessions.delete(key);
|
|
85
|
+
}
|
|
86
|
+
// 尝试从 DB 恢复之前的 session(程序重启场景)
|
|
87
|
+
const resumeSessionId = this.findResumableSession(key);
|
|
88
|
+
try {
|
|
89
|
+
if (resumeSessionId) {
|
|
90
|
+
try {
|
|
91
|
+
const session = await this.createSession(key, resumeSessionId);
|
|
92
|
+
this.logger.info(`[SessionManager] 从 DB 恢复 session (resume ${resumeSessionId}): ${key}`);
|
|
93
|
+
const prompt = formatFollowUpPrompt(ctx);
|
|
94
|
+
session.enqueue({ type: 'message', prompt, enqueuedAt: new Date() });
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
catch (resumeErr) {
|
|
98
|
+
this.logger.error(`[SessionManager] resume 失败,fallback 到全新 session: ${resumeErr}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// 全新 session
|
|
102
|
+
const session = await this.createSession(key);
|
|
103
|
+
const contextPrompt = formatMessageContextPrompt(ctx);
|
|
104
|
+
session.enqueue({ type: 'message', prompt: contextPrompt, enqueuedAt: new Date() });
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
108
|
+
this.logger.error(`[SessionManager] 创建 session 失败 (${key}):\n${errMsg}`);
|
|
109
|
+
this.sendChatNotification(key, this.pickStartupFailureReply());
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* 处理 Yor 交付验收:查找来源 session,活着就复用,否则新建
|
|
114
|
+
*/
|
|
115
|
+
async handleDelivery(ctx) {
|
|
116
|
+
const sourceChatId = ctx.sourceChatId;
|
|
117
|
+
// 尝试复用来源 session
|
|
118
|
+
if (sourceChatId && this.sessions.has(sourceChatId)) {
|
|
119
|
+
const session = this.sessions.get(sourceChatId);
|
|
120
|
+
const state = session.getState();
|
|
121
|
+
if (state !== 'disposed') {
|
|
122
|
+
const prompt = formatDeliveryContextPrompt(ctx);
|
|
123
|
+
session.enqueue({ type: 'delivery', prompt, enqueuedAt: new Date() });
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
this.sessions.delete(sourceChatId);
|
|
127
|
+
}
|
|
128
|
+
// 来源 session 已回收,新建独立 session
|
|
129
|
+
const key = `delivery:${ctx.taskId}`;
|
|
130
|
+
try {
|
|
131
|
+
const session = await this.createSession(key);
|
|
132
|
+
const protocol = this.config.protocols['review'] ?? '';
|
|
133
|
+
const prompt = [
|
|
134
|
+
protocol ? protocol + '\n\n---\n\n' : '',
|
|
135
|
+
formatDeliveryContextPrompt(ctx),
|
|
136
|
+
].join('');
|
|
137
|
+
session.enqueue({ type: 'delivery', prompt, enqueuedAt: new Date() });
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
141
|
+
this.logger.error(`[SessionManager] 创建 delivery session 失败 (${key}):\n${errMsg}`);
|
|
142
|
+
// delivery 场景通知来源群
|
|
143
|
+
if (ctx.sourceChatId) {
|
|
144
|
+
this.sendChatNotification(ctx.sourceChatId, this.pickStartupFailureReply());
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* 获取当前活跃 session 数量
|
|
150
|
+
*/
|
|
151
|
+
getSessionCount() {
|
|
152
|
+
return this.sessions.size;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* 获取指定 session 的状态(用于诊断)
|
|
156
|
+
*/
|
|
157
|
+
getSessionState(key) {
|
|
158
|
+
return this.sessions.get(key)?.getState() ?? null;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* 重启指定 chatId 的 Loid session(/restart 命令)
|
|
162
|
+
* dispose 当前 session + 标记 DB 中所有未结束的 session 为已结束,
|
|
163
|
+
* 确保下次来消息时走全新 session 而非 resume
|
|
164
|
+
*/
|
|
165
|
+
async restartChat(chatId) {
|
|
166
|
+
const key = chatId;
|
|
167
|
+
const session = this.sessions.get(key);
|
|
168
|
+
if (session) {
|
|
169
|
+
await session.dispose();
|
|
170
|
+
this.sessions.delete(key);
|
|
171
|
+
}
|
|
172
|
+
// 标记 DB 中所有未结束的 loid session 为已结束,防止下次 resume
|
|
173
|
+
try {
|
|
174
|
+
const dbSessions = getCCSessionsByChat(this.deps.db, chatId);
|
|
175
|
+
for (const s of dbSessions) {
|
|
176
|
+
if (s.role === 'loid' && !s.ended_at) {
|
|
177
|
+
updateCCSessionEnded(this.deps.db, s.session_id);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
catch (err) {
|
|
182
|
+
this.logger.error('[SessionManager] restart 清理 DB session 失败:', err);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* 关闭所有 session,释放资源
|
|
187
|
+
*/
|
|
188
|
+
async dispose() {
|
|
189
|
+
const disposePromises = [];
|
|
190
|
+
for (const [key, session] of this.sessions) {
|
|
191
|
+
this.logger.info(`[SessionManager] 正在关闭 session: ${key}`);
|
|
192
|
+
disposePromises.push(session.dispose());
|
|
193
|
+
}
|
|
194
|
+
await Promise.all(disposePromises);
|
|
195
|
+
this.sessions.clear();
|
|
196
|
+
this.logger.info('[SessionManager] 所有 session 已关闭');
|
|
197
|
+
}
|
|
198
|
+
// ── 私有方法 ──
|
|
199
|
+
/**
|
|
200
|
+
* 创建新 LoidSession,注册生命周期回调
|
|
201
|
+
* 如果 broker 槽位满,尝试 LRU 淘汰后重试
|
|
202
|
+
* @param resumeSessionId 传入则尝试 resume 到之前的 CC session
|
|
203
|
+
*/
|
|
204
|
+
async createSession(key, resumeSessionId) {
|
|
205
|
+
this.logger.info(`[SessionManager] 创建新 session: ${key}`);
|
|
206
|
+
// per-chatId 工作区:sanitize key 作为目录名
|
|
207
|
+
const safeDirName = key.replace(/[/:]/g, '_');
|
|
208
|
+
const workDir = join(this.config.loidBaseDir, safeDirName);
|
|
209
|
+
// 确保工作区就绪(模板同步 + 共享目录 symlink)
|
|
210
|
+
await ensureLoidWorkspace(workDir, this.config.templateDir, this.config.officePath, this.logger);
|
|
211
|
+
// 写入 chat-context.md(基于 DB 中的 chat 绑定配置)
|
|
212
|
+
await this.writeChatContext(key, workDir);
|
|
213
|
+
const instanceId = `loid:${key}`;
|
|
214
|
+
const logFile = this.config.logDir
|
|
215
|
+
? join(this.config.logDir, `loid-${safeDirName}-${Date.now()}.log`)
|
|
216
|
+
: undefined;
|
|
217
|
+
const sessionConfig = {
|
|
218
|
+
broker: this.config.broker,
|
|
219
|
+
instanceId,
|
|
220
|
+
backendConfig: { ...this.config.backendConfigTemplate, workingDir: workDir },
|
|
221
|
+
idleTimeoutMs: this.config.idleTimeoutMs,
|
|
222
|
+
maxTurns: this.config.maxTurnsPerSession,
|
|
223
|
+
logFile,
|
|
224
|
+
db: this.deps.db,
|
|
225
|
+
chatId: key,
|
|
226
|
+
logger: this.logger,
|
|
227
|
+
};
|
|
228
|
+
const session = new LoidSession(sessionConfig, {
|
|
229
|
+
onCrash: (orphanMessages, savedSessionId) => {
|
|
230
|
+
this.onSessionCrash(key, orphanMessages, savedSessionId);
|
|
231
|
+
},
|
|
232
|
+
onDisposed: () => {
|
|
233
|
+
this.logger.info(`[SessionManager] session 自然结束: ${key}`);
|
|
234
|
+
this.sessions.delete(key);
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
try {
|
|
238
|
+
await session.connect(resumeSessionId);
|
|
239
|
+
}
|
|
240
|
+
catch (err) {
|
|
241
|
+
// 槽位满时尝试 LRU 淘汰
|
|
242
|
+
if (err instanceof Error && err.message.includes('实例已满')) {
|
|
243
|
+
const evicted = await this.evictLRU();
|
|
244
|
+
if (evicted) {
|
|
245
|
+
this.logger.info(`[SessionManager] LRU 淘汰 ${evicted},重试创建 ${key}`);
|
|
246
|
+
await session.connect(resumeSessionId);
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
throw err;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
throw err;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
this.sessions.set(key, session);
|
|
257
|
+
return session;
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* LRU 淘汰:找到最久没活动且处于 IDLE 的 session,dispose 它
|
|
261
|
+
* 返回被淘汰的 session key,无可淘汰时返回 null
|
|
262
|
+
*/
|
|
263
|
+
async evictLRU() {
|
|
264
|
+
let oldestKey = null;
|
|
265
|
+
let oldestTime = Infinity;
|
|
266
|
+
for (const [key, session] of this.sessions) {
|
|
267
|
+
if (session.getState() === 'idle') {
|
|
268
|
+
const activityTime = session.getLastActivityAt().getTime();
|
|
269
|
+
if (activityTime < oldestTime) {
|
|
270
|
+
oldestTime = activityTime;
|
|
271
|
+
oldestKey = key;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (oldestKey) {
|
|
276
|
+
const session = this.sessions.get(oldestKey);
|
|
277
|
+
await session.dispose();
|
|
278
|
+
this.sessions.delete(oldestKey);
|
|
279
|
+
this.logger.info(`[SessionManager] LRU 淘汰 session: ${oldestKey}`);
|
|
280
|
+
return oldestKey;
|
|
281
|
+
}
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Session 崩溃恢复:优先 resume 旧 session(保留完整对话上下文),
|
|
286
|
+
* 失败则 fallback 到全新 session + recovery prompt
|
|
287
|
+
*/
|
|
288
|
+
async onSessionCrash(key, orphanedMessages, cliSessionId) {
|
|
289
|
+
this.logger.error(`[SessionManager] session 崩溃: ${key} (${orphanedMessages.length} 条孤儿消息, sessionId=${cliSessionId ?? 'none'})`);
|
|
290
|
+
// 删除旧 session
|
|
291
|
+
this.sessions.delete(key);
|
|
292
|
+
// 通知用户:正在恢复
|
|
293
|
+
const chatId = this.extractChatId(key);
|
|
294
|
+
if (chatId) {
|
|
295
|
+
this.sendChatNotification(chatId, this.pickCrashRecoveryReply());
|
|
296
|
+
}
|
|
297
|
+
try {
|
|
298
|
+
// 优先尝试 resume(保留完整对话上下文)
|
|
299
|
+
if (cliSessionId) {
|
|
300
|
+
try {
|
|
301
|
+
const resumedSession = await this.createSession(key, cliSessionId);
|
|
302
|
+
this.logger.info(`[SessionManager] 崩溃恢复成功 (resume ${cliSessionId}): ${key}`);
|
|
303
|
+
// 投递孤儿消息
|
|
304
|
+
for (const msg of orphanedMessages) {
|
|
305
|
+
resumedSession.enqueue(msg);
|
|
306
|
+
}
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
catch (resumeErr) {
|
|
310
|
+
this.logger.error(`[SessionManager] resume 失败,fallback 到全新 session: ${resumeErr}`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
// Fallback: 全新 session + recovery prompt
|
|
314
|
+
const activeTasks = this.getActiveTasks();
|
|
315
|
+
const recoveryPrompt = formatRecoveryPrompt(activeTasks);
|
|
316
|
+
const newSession = await this.createSession(key);
|
|
317
|
+
newSession.enqueue({ type: 'recovery', prompt: recoveryPrompt, enqueuedAt: new Date() });
|
|
318
|
+
for (const msg of orphanedMessages) {
|
|
319
|
+
newSession.enqueue(msg);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
catch (err) {
|
|
323
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
324
|
+
this.logger.error(`[SessionManager] 崩溃恢复失败 (${key}):\n${errMsg}`);
|
|
325
|
+
if (chatId) {
|
|
326
|
+
this.sendChatNotification(chatId, this.pickStartupFailureReply());
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* 从 session key 提取 chatId(过滤掉 delivery: 等前缀的内部 key)
|
|
332
|
+
* 只有来自用户对话的 session 才需要通知
|
|
333
|
+
*/
|
|
334
|
+
extractChatId(key) {
|
|
335
|
+
// delivery:ANYA-xxx 等内部 session 不直接通知用户
|
|
336
|
+
if (key.startsWith('delivery:'))
|
|
337
|
+
return null;
|
|
338
|
+
return key;
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* 通过 instanceId 反查 chatId(session key)
|
|
342
|
+
*/
|
|
343
|
+
findChatIdByInstanceId(instanceId) {
|
|
344
|
+
for (const [key, session] of this.sessions) {
|
|
345
|
+
if (instanceId === `loid:${key}`)
|
|
346
|
+
return this.extractChatId(key);
|
|
347
|
+
}
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* 发送限流"请假"通知
|
|
352
|
+
*/
|
|
353
|
+
sendRateLimitNotification(chatId, roleName, info) {
|
|
354
|
+
const resetTime = new Date(info.resetsAt * 1000);
|
|
355
|
+
const timeStr = resetTime.toLocaleTimeString('zh-CN', {
|
|
356
|
+
hour: '2-digit',
|
|
357
|
+
minute: '2-digit',
|
|
358
|
+
timeZone: 'Asia/Shanghai',
|
|
359
|
+
});
|
|
360
|
+
const { text, index } = this.pickRandom(LoidSessionManager.RATE_LIMIT_REPLIES, this.lastRateLimitIndex);
|
|
361
|
+
this.lastRateLimitIndex = index;
|
|
362
|
+
const message = text.replace('{time}', timeStr);
|
|
363
|
+
this.logger.info(`[SessionManager] 限流通知 (${chatId}): ${roleName} 请假至 ${timeStr}`);
|
|
364
|
+
this.sendChatNotification(chatId, message);
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* 向飞书群发送通知(fire-and-forget),同时清除"处理中"表情
|
|
368
|
+
*/
|
|
369
|
+
sendChatNotification(chatId, text) {
|
|
370
|
+
if (!this.deps.feishuSender)
|
|
371
|
+
return;
|
|
372
|
+
// 清除 typing reaction(通知本身就是反馈)
|
|
373
|
+
this.deps.feishuSender.clearTypingReaction(chatId).catch(() => { });
|
|
374
|
+
this.deps.feishuSender.sendText({
|
|
375
|
+
receiveIdType: 'chat_id',
|
|
376
|
+
receiveId: chatId,
|
|
377
|
+
text,
|
|
378
|
+
}).catch(err => {
|
|
379
|
+
this.logger.error('[SessionManager] 崩溃通知发送失败:', err);
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* 写入 chat-context.md 到工作区(基于 DB 中的绑定配置)
|
|
384
|
+
* 无绑定时删除文件(如存在)
|
|
385
|
+
*/
|
|
386
|
+
async writeChatContext(chatId, workDir) {
|
|
387
|
+
const contextFile = join(workDir, 'chat-context.md');
|
|
388
|
+
try {
|
|
389
|
+
const chatCtx = getChatContext(this.deps.db, chatId);
|
|
390
|
+
if (!chatCtx?.default_project_id && !chatCtx?.custom_context) {
|
|
391
|
+
// 无绑定,不写文件
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
const lines = ['# 对话上下文设定', ''];
|
|
395
|
+
// 项目绑定
|
|
396
|
+
if (chatCtx.default_project_id && this.deps.projectRegistry) {
|
|
397
|
+
const project = this.deps.projectRegistry.findProject(chatCtx.default_project_id);
|
|
398
|
+
if (project) {
|
|
399
|
+
lines.push('## 绑定项目');
|
|
400
|
+
lines.push(`- 项目: ${project.name} (${project.projectId})`);
|
|
401
|
+
if (project.description)
|
|
402
|
+
lines.push(`- 描述: ${project.description}`);
|
|
403
|
+
if (project.repos.length > 0) {
|
|
404
|
+
lines.push(`- 仓库: ${project.repos.map(r => r.name).join(', ')}`);
|
|
405
|
+
}
|
|
406
|
+
if (project.claudeMd) {
|
|
407
|
+
lines.push('- 技术栈:');
|
|
408
|
+
lines.push(project.claudeMd);
|
|
409
|
+
}
|
|
410
|
+
lines.push('');
|
|
411
|
+
lines.push('> 派发任务时,默认使用此项目的 project_id,无需用户额外指定。');
|
|
412
|
+
lines.push('');
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
// 自定义上下文
|
|
416
|
+
if (chatCtx.custom_context) {
|
|
417
|
+
lines.push('## 自定义设定');
|
|
418
|
+
lines.push(chatCtx.custom_context);
|
|
419
|
+
lines.push('');
|
|
420
|
+
}
|
|
421
|
+
await writeFile(contextFile, lines.join('\n'), 'utf-8');
|
|
422
|
+
this.logger.info(`[SessionManager] 已写入 chat-context.md: ${chatId}`);
|
|
423
|
+
}
|
|
424
|
+
catch (err) {
|
|
425
|
+
this.logger.error(`[SessionManager] 写入 chat-context.md 失败 (${chatId}):`, err);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* 从 DB 查找可 resume 的 CC session(程序重启后恢复)
|
|
430
|
+
* 返回最近一个未结束的 loid session_id,无则返回 undefined
|
|
431
|
+
*/
|
|
432
|
+
findResumableSession(chatId) {
|
|
433
|
+
try {
|
|
434
|
+
const sessions = getCCSessionsByChat(this.deps.db, chatId);
|
|
435
|
+
const resumable = sessions.find(s => s.role === 'loid' && !s.ended_at);
|
|
436
|
+
if (resumable) {
|
|
437
|
+
this.logger.info(`[SessionManager] 发现可恢复 session: ${resumable.session_id} (chat=${chatId})`);
|
|
438
|
+
return resumable.session_id;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
catch (err) {
|
|
442
|
+
this.logger.error('[SessionManager] 查询可恢复 session 失败:', err);
|
|
443
|
+
}
|
|
444
|
+
return undefined;
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* 获取活跃任务列表(用于崩溃恢复上下文)
|
|
448
|
+
*/
|
|
449
|
+
getActiveTasks() {
|
|
450
|
+
const statuses = [
|
|
451
|
+
TaskStatus.IN_PROGRESS,
|
|
452
|
+
TaskStatus.READY,
|
|
453
|
+
TaskStatus.NEED_CLARIFICATION,
|
|
454
|
+
TaskStatus.DELIVERING,
|
|
455
|
+
TaskStatus.BLOCKED,
|
|
456
|
+
];
|
|
457
|
+
const tasks = [];
|
|
458
|
+
for (const status of statuses) {
|
|
459
|
+
const statusTasks = getTasksByStatus(this.deps.db, status);
|
|
460
|
+
for (const t of statusTasks) {
|
|
461
|
+
tasks.push({
|
|
462
|
+
task_id: t.task_id,
|
|
463
|
+
title: t.title,
|
|
464
|
+
status: t.status,
|
|
465
|
+
assignee: t.assignee,
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return tasks;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
//# sourceMappingURL=session-manager.js.map
|