feique 1.5.0 → 1.5.7

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.
@@ -13,7 +13,7 @@ import { resolveKnowledgeRoots, searchKnowledgeBase } from '../knowledge/search.
13
13
  import { resolveMessageResources } from '../feishu/message-resource.js';
14
14
  import { MemoryStore } from '../state/memory-store.js';
15
15
  import { CodexSessionIndex } from '../codex/session-index.js';
16
- import { resolveProjectBackendWithOverride, resolveProjectBackendName } from '../backend/factory.js';
16
+ import { resolveProjectBackendWithOverride, resolveProjectBackendName, resolveFallbackChain } from '../backend/factory.js';
17
17
  import { getBackendDefinition, listBackendNames } from '../backend/registry.js';
18
18
  import { buildQueueKey, isExecutionRunStatus, isVisibleRunStatus, mapRunStatusToPhase, buildMessageDedupeKey, buildCardDedupeKey, truncateExcerpt, friendlyErrorMessage, resolveAdminListTarget, buildConversationKeyForConversation, renderMemorySection, formatAgeFromNow, replaceObject, replaceProjects, } from './service-utils.js';
19
19
  import { handleDocCommand as handleDocCommandImpl, handleTaskCommand as handleTaskCommandImpl, handleBaseCommand as handleBaseCommandImpl, handleWikiCommand as handleWikiCommandImpl, } from './feishu-commands.js';
@@ -194,6 +194,7 @@ export class FeiqueService {
194
194
  return runMaintenanceCycleImpl(this);
195
195
  }
196
196
  async handleIncomingMessage(context) {
197
+ context = this.normalizeIncomingChatContext(context);
197
198
  this.currentMessageContext = context;
198
199
  if (!context.text.trim() && context.attachments.length === 0) {
199
200
  return;
@@ -284,7 +285,7 @@ export class FeiqueService {
284
285
  await this.handleWikiCommand(context, selectionKey, command.action, command.value, command.extra, command.target, command.role);
285
286
  return;
286
287
  case 'backend':
287
- await this.handleBackendCommand(context, selectionKey, command.name);
288
+ await this.handleBackendCommand(context, selectionKey, command.name, command.action);
288
289
  return;
289
290
  case 'session':
290
291
  const sessionArgument = command.action === 'adopt' ? command.target : command.threadId;
@@ -572,7 +573,7 @@ export class FeiqueService {
572
573
  await this.handleWikiCommand(context, selectionKey, command.action, command.value, command.extra, command.target, command.role);
573
574
  return;
574
575
  case 'backend':
575
- await this.handleBackendCommand(context, selectionKey, command.name);
576
+ await this.handleBackendCommand(context, selectionKey, command.name, command.action);
576
577
  return;
577
578
  case 'session': {
578
579
  const sessionArgument = command.action === 'adopt' ? command.target : command.threadId;
@@ -607,7 +608,7 @@ export class FeiqueService {
607
608
  openaiImageModel: this.config.service.openai_image_model,
608
609
  logger: this.logger,
609
610
  });
610
- if (context.chat_type === 'group' && this.shouldRequireMention(projectContext.project) && context.mentions.length === 0) {
611
+ if (context.chat_type === 'group' && this.shouldRequireMention(projectContext.project) && !this.messageMentionsBot(context)) {
611
612
  return;
612
613
  }
613
614
  const rateLimitMessage = this.checkAndConsumeChatRateLimit(projectContext.projectAlias, projectContext.project, context.chat_id);
@@ -1157,8 +1158,41 @@ export class FeiqueService {
1157
1158
  }
1158
1159
  await this.sendTextReply(context.chat_id, adoption.text, context.message_id, context.text);
1159
1160
  }
1160
- async handleBackendCommand(context, selectionKey, name) {
1161
+ async handleBackendCommand(context, selectionKey, name, action) {
1161
1162
  const projectContext = await this.resolveProjectContext(context, selectionKey);
1163
+ if (action === 'list') {
1164
+ const sessionOverride = await this.sessionStore.getProjectBackend(projectContext.sessionKey, projectContext.projectAlias);
1165
+ const primaryName = resolveProjectBackendName(this.config, projectContext.projectAlias, sessionOverride);
1166
+ const chain = resolveFallbackChain(this.config, projectContext.projectAlias, primaryName);
1167
+ const project = this.config.projects[projectContext.projectAlias];
1168
+ const failoverEnabled = project?.failover ?? this.config.backend?.failover ?? true;
1169
+ const failoverSource = project?.failover !== undefined
1170
+ ? '项目配置'
1171
+ : this.config.backend?.failover !== undefined ? '全局默认' : '注册表默认';
1172
+ const registered = listBackendNames();
1173
+ const lines = [
1174
+ `项目: ${projectContext.projectAlias}`,
1175
+ '',
1176
+ `当前主后端: ${primaryName}${sessionOverride ? '(会话级覆盖)' : project?.backend ? '(项目配置)' : '(全局默认)'}`,
1177
+ `Failover: ${failoverEnabled ? '启用' : '关闭'}(${failoverSource})`,
1178
+ chain.length > 0
1179
+ ? `Fallback 链: ${primaryName} → ${chain.join(' → ')}`
1180
+ : `Fallback 链: 无(链为空)`,
1181
+ '',
1182
+ '已注册的后端:',
1183
+ ...registered.map((n) => {
1184
+ const marker = n === primaryName ? ' ← 当前' : chain.includes(n) ? ' (fallback)' : '';
1185
+ return ` • ${n}${marker}`;
1186
+ }),
1187
+ '',
1188
+ '用法:',
1189
+ ` /backend ${registered.join('|')} — 切换到指定后端`,
1190
+ ' /backend — 只查看当前后端',
1191
+ ' /backend list — 查看完整清单(本命令)',
1192
+ ];
1193
+ await this.sendTextReply(context.chat_id, lines.join('\n'), context.message_id, context.text);
1194
+ return;
1195
+ }
1162
1196
  if (!name) {
1163
1197
  const sessionOverride = await this.sessionStore.getProjectBackend(projectContext.sessionKey, projectContext.projectAlias);
1164
1198
  const effectiveName = resolveProjectBackendName(this.config, projectContext.projectAlias, sessionOverride);
@@ -1484,6 +1518,25 @@ export class FeiqueService {
1484
1518
  shouldRequireMention(project) {
1485
1519
  return project.mention_required || this.config.security.require_group_mentions;
1486
1520
  }
1521
+ messageMentionsBot(context) {
1522
+ const botOpenIds = new Set(this.config.feishu.bot_open_ids ?? []);
1523
+ const botName = this.config.feishu.bot_name?.trim();
1524
+ if (botOpenIds.size === 0 && !botName) {
1525
+ return context.mentions.length > 0;
1526
+ }
1527
+ return context.mentions.some((mention) => {
1528
+ if (mention.id && botOpenIds.has(mention.id)) {
1529
+ return true;
1530
+ }
1531
+ return Boolean(botName && mention.name?.trim() === botName);
1532
+ });
1533
+ }
1534
+ normalizeIncomingChatContext(context) {
1535
+ if (context.chat_type !== 'group' && this.config.feishu.allowed_group_ids.includes(context.chat_id)) {
1536
+ return { ...context, chat_type: 'group' };
1537
+ }
1538
+ return context;
1539
+ }
1487
1540
  async cancelActiveRun(queueKey, reason) {
1488
1541
  const live = this.activeRuns.get(queueKey);
1489
1542
  if (live) {
@@ -2019,6 +2072,11 @@ export class FeiqueService {
2019
2072
  }
2020
2073
  async sendInitialRunLifecycleReply(input) {
2021
2074
  const lifecycleMode = resolveRunLifecycleReplyMode(this.config);
2075
+ // Keep normal runs to a single final reply. Queued runs still get an
2076
+ // explicit queue notice because the user is waiting behind other work.
2077
+ if (!input.queued || lifecycleMode === 'post') {
2078
+ return;
2079
+ }
2022
2080
  const draft = this.buildInitialRunLifecycleReply(input.projectAlias, input.queued, lifecycleMode);
2023
2081
  try {
2024
2082
  const response = await this.sendRunLifecycleReply({
@@ -2121,48 +2179,61 @@ export class FeiqueService {
2121
2179
  return false;
2122
2180
  }
2123
2181
  const sanitizedBody = sanitizeUserVisibleReply(input.body);
2124
- if (target.mode === 'card') {
2125
- const includeActions = input.runStatus === 'success' && supportsInteractiveCardActions(this.config) && input.sessionKey !== undefined;
2126
- await this.feishuClient.updateCard(target.messageId, buildRunLifecycleCard({
2127
- title: input.title,
2128
- body: input.body,
2129
- projectAlias: input.projectAlias,
2130
- runStatus: input.runStatus,
2131
- runPhase: input.runPhase,
2132
- cardSummary: input.cardSummary,
2133
- includeActions,
2134
- rerunPayload: includeActions && input.sessionKey
2135
- ? {
2136
- action: 'rerun',
2137
- conversation_key: input.sessionKey,
2138
- project_alias: input.projectAlias,
2139
- chat_id: input.chatId,
2140
- }
2141
- : undefined,
2142
- newSessionPayload: includeActions && input.sessionKey
2143
- ? {
2144
- action: 'new',
2145
- conversation_key: input.sessionKey,
2146
- project_alias: input.projectAlias,
2147
- chat_id: input.chatId,
2148
- }
2149
- : undefined,
2150
- statusPayload: includeActions && input.sessionKey
2151
- ? {
2152
- action: 'status',
2153
- conversation_key: input.sessionKey,
2154
- project_alias: input.projectAlias,
2155
- chat_id: input.chatId,
2156
- }
2157
- : undefined,
2158
- }));
2159
- }
2160
- else if (target.mode === 'post') {
2161
- const title = buildReplyTitle(sanitizedBody);
2162
- await this.feishuClient.updatePost(target.messageId, buildFeishuPost(title, sanitizedBody));
2182
+ try {
2183
+ if (target.mode === 'card') {
2184
+ const includeActions = input.runStatus === 'success' && supportsInteractiveCardActions(this.config) && input.sessionKey !== undefined;
2185
+ await this.feishuClient.updateCard(target.messageId, buildRunLifecycleCard({
2186
+ title: input.title,
2187
+ body: input.body,
2188
+ projectAlias: input.projectAlias,
2189
+ runStatus: input.runStatus,
2190
+ runPhase: input.runPhase,
2191
+ cardSummary: input.cardSummary,
2192
+ includeActions,
2193
+ rerunPayload: includeActions && input.sessionKey
2194
+ ? {
2195
+ action: 'rerun',
2196
+ conversation_key: input.sessionKey,
2197
+ project_alias: input.projectAlias,
2198
+ chat_id: input.chatId,
2199
+ }
2200
+ : undefined,
2201
+ newSessionPayload: includeActions && input.sessionKey
2202
+ ? {
2203
+ action: 'new',
2204
+ conversation_key: input.sessionKey,
2205
+ project_alias: input.projectAlias,
2206
+ chat_id: input.chatId,
2207
+ }
2208
+ : undefined,
2209
+ statusPayload: includeActions && input.sessionKey
2210
+ ? {
2211
+ action: 'status',
2212
+ conversation_key: input.sessionKey,
2213
+ project_alias: input.projectAlias,
2214
+ chat_id: input.chatId,
2215
+ }
2216
+ : undefined,
2217
+ }));
2218
+ }
2219
+ else if (target.mode === 'post') {
2220
+ const title = buildReplyTitle(sanitizedBody);
2221
+ await this.feishuClient.updatePost(target.messageId, buildFeishuPost(title, sanitizedBody));
2222
+ }
2223
+ else {
2224
+ await this.feishuClient.updateText(target.messageId, sanitizedBody);
2225
+ }
2163
2226
  }
2164
- else {
2165
- await this.feishuClient.updateText(target.messageId, sanitizedBody);
2227
+ catch (error) {
2228
+ this.runReplyTargets.delete(input.runId);
2229
+ this.logger.warn({
2230
+ error,
2231
+ chatId: input.chatId,
2232
+ projectAlias: input.projectAlias,
2233
+ runId: input.runId,
2234
+ replyMode: target.mode,
2235
+ }, 'Failed to update run lifecycle reply; will fall back to a new reply');
2236
+ return false;
2166
2237
  }
2167
2238
  await this.auditLog.append({
2168
2239
  type: 'message.updated',