feique 1.1.4 → 1.2.1

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.
@@ -37,7 +37,10 @@ import { classifyOperation, enforceTrustBoundary, recordRunOutcome, formatTrustS
37
37
  import { buildProjectTimeline, buildOnboardingContext, formatTimeline, isNewActor } from '../collaboration/timeline.js';
38
38
  import { HandoffStore } from '../state/handoff-store.js';
39
39
  import { TrustStore } from '../state/trust-store.js';
40
+ import { IntentClassifier } from './intent-classifier.js';
40
41
  import { buildTeamDigest, formatTeamDigest, createDigestPeriod } from '../collaboration/digest.js';
42
+ import { checkRunAlerts, checkLongRunningAlerts, formatAlert, DEFAULT_ALERT_RULES } from '../collaboration/proactive-alerts.js';
43
+ import { detectKnowledgeGaps, formatKnowledgeGaps } from '../collaboration/knowledge-gaps.js';
41
44
  import { estimateCost } from '../observability/cost.js';
42
45
  export class FeiqueService {
43
46
  config;
@@ -62,6 +65,9 @@ export class FeiqueService {
62
65
  chatRateWindows = new Map();
63
66
  maintenanceTimer;
64
67
  digestTimer;
68
+ intentClassifier;
69
+ /** Tracks the current incoming message for @mention in replies. */
70
+ currentMessageContext;
65
71
  constructor(config, feishuClient, sessionStore, auditLog, logger, metrics, idempotencyStore = new IdempotencyStore(config.storage.dir), runStateStore = new RunStateStore(config.storage.dir), memoryStore = new MemoryStore(config.storage.dir), codexSessionIndex = new CodexSessionIndex(), runtimeControl, adminAuditLog = new AuditLog(config.storage.dir, 'admin-audit.jsonl'), configHistoryStore = new ConfigHistoryStore(config.storage.dir), handoffStore = new HandoffStore(config.storage.dir), trustStore = new TrustStore(config.storage.dir)) {
66
72
  this.config = config;
67
73
  this.feishuClient = feishuClient;
@@ -78,6 +84,20 @@ export class FeiqueService {
78
84
  this.configHistoryStore = configHistoryStore;
79
85
  this.handoffStore = handoffStore;
80
86
  this.trustStore = trustStore;
87
+ if (config.service.intent_classifier_enabled) {
88
+ const defaultBackend = config.backend?.default ?? 'codex';
89
+ const isClaude = defaultBackend === 'claude';
90
+ this.intentClassifier = new IntentClassifier({
91
+ enabled: true,
92
+ backend: defaultBackend,
93
+ backend_bin: isClaude ? (config.claude?.bin ?? 'claude') : config.codex.bin,
94
+ shell: isClaude ? config.claude?.shell : config.codex.shell,
95
+ pre_exec: isClaude ? config.claude?.pre_exec : config.codex.pre_exec,
96
+ ollama_base_url: config.embedding.provider === 'ollama' ? config.embedding.ollama_base_url : undefined,
97
+ timeout_ms: config.service.intent_classifier_timeout_ms,
98
+ min_confidence: config.service.intent_classifier_min_confidence,
99
+ });
100
+ }
81
101
  }
82
102
  async recoverRuntimeState() {
83
103
  const recovered = await this.runStateStore.recoverOrphanedRuns();
@@ -234,8 +254,25 @@ export class FeiqueService {
234
254
  await this.runMemoryMaintenance();
235
255
  }
236
256
  await this.runAuditMaintenance();
257
+ // Proactive: check for long-running tasks
258
+ try {
259
+ const activeRuns = await this.runStateStore.listRuns();
260
+ const longAlerts = checkLongRunningAlerts(activeRuns);
261
+ for (const alert of longAlerts) {
262
+ const text = formatAlert(alert);
263
+ for (const chatId of this.config.security.admin_chat_ids) {
264
+ try {
265
+ await this.feishuClient.sendText(chatId, text);
266
+ }
267
+ catch { /* best-effort */ }
268
+ }
269
+ await this.notifyProjectChats(alert.project_alias, text);
270
+ }
271
+ }
272
+ catch { /* best-effort */ }
237
273
  }
238
274
  async handleIncomingMessage(context) {
275
+ this.currentMessageContext = context;
239
276
  if (!context.text.trim() && context.attachments.length === 0) {
240
277
  return;
241
278
  }
@@ -263,7 +300,18 @@ export class FeiqueService {
263
300
  }
264
301
  const normalizedText = normalizeIncomingText(context.text);
265
302
  const selectionKey = await this.getSelectionConversationKey(context);
266
- const command = parseBridgeCommand(context.text);
303
+ let command = parseBridgeCommand(context.text);
304
+ // AI intent fallback: when regex doesn't match, try AI classification
305
+ if (command.kind === 'prompt' && this.intentClassifier) {
306
+ try {
307
+ const aiCommand = await this.intentClassifier.classify(normalizedText);
308
+ if (aiCommand) {
309
+ command = aiCommand;
310
+ this.logger.info({ originalText: normalizedText, aiCommand: command.kind }, 'AI intent classifier matched');
311
+ }
312
+ }
313
+ catch { /* AI classification is best-effort */ }
314
+ }
267
315
  this.metrics?.recordIncomingMessage(context.chat_type, command.kind);
268
316
  await this.auditLog.append({
269
317
  type: 'message.received',
@@ -367,6 +415,9 @@ export class FeiqueService {
367
415
  this.metrics?.recordCollaborationEvent('digest');
368
416
  await this.handleDigestCommand(context);
369
417
  return;
418
+ case 'gaps':
419
+ await this.handleGapsCommand(context);
420
+ return;
370
421
  case 'prompt':
371
422
  await this.handlePromptMessage(context, selectionKey, command.prompt, context.text);
372
423
  return;
@@ -787,6 +838,14 @@ export class FeiqueService {
787
838
  this.metrics?.recordTrustLevel(input.projectAlias, updated.current_level);
788
839
  }
789
840
  catch { /* trust tracking is best-effort */ }
841
+ // Proactive alerts: check if this run triggers any team alerts
842
+ try {
843
+ const completedRunState = await this.runStateStore.getRun(runId);
844
+ if (completedRunState) {
845
+ await this.checkAndSendAlerts(completedRunState);
846
+ }
847
+ }
848
+ catch { /* alerts are best-effort */ }
790
849
  // Direction 2: Auto-extract knowledge
791
850
  if (this.config.service.memory_enabled && excerpt.length >= 100) {
792
851
  try {
@@ -868,6 +927,14 @@ export class FeiqueService {
868
927
  catch { /* trust tracking is best-effort */ }
869
928
  // Notify project chats about the failure
870
929
  await this.notifyProjectChats(input.projectAlias, `❌ 运行失败 [${input.projectAlias}]\n${message.slice(0, 200)}`);
930
+ // Proactive alerts on failure
931
+ try {
932
+ const failedRunState = await this.runStateStore.getRun(runId);
933
+ if (failedRunState) {
934
+ await this.checkAndSendAlerts(failedRunState);
935
+ }
936
+ }
937
+ catch { /* alerts are best-effort */ }
871
938
  }
872
939
  if (cancelled) {
873
940
  this.logger.warn({
@@ -1473,6 +1540,44 @@ export class FeiqueService {
1473
1540
  const text = formatTeamDigest(digest);
1474
1541
  await this.sendTextReply(context.chat_id, text, context.message_id, context.text);
1475
1542
  }
1543
+ // ── Proactive Alerts ──
1544
+ async checkAndSendAlerts(completedRun) {
1545
+ const recentRuns = await this.runStateStore.listRuns();
1546
+ const projectConfig = this.config.projects[completedRun.project_alias];
1547
+ const dailyQuota = projectConfig?.daily_token_quota;
1548
+ const alerts = checkRunAlerts(completedRun, recentRuns, DEFAULT_ALERT_RULES, dailyQuota);
1549
+ if (alerts.length === 0)
1550
+ return;
1551
+ for (const alert of alerts) {
1552
+ const text = formatAlert(alert);
1553
+ // Send to admin chat IDs
1554
+ for (const chatId of this.config.security.admin_chat_ids) {
1555
+ try {
1556
+ await this.feishuClient.sendText(chatId, text);
1557
+ }
1558
+ catch { /* best-effort */ }
1559
+ }
1560
+ // Send to project notification channels
1561
+ await this.notifyProjectChats(alert.project_alias, text);
1562
+ await this.auditLog.append({
1563
+ type: 'collaboration.alert.sent',
1564
+ alert_kind: alert.kind,
1565
+ severity: alert.severity,
1566
+ project_alias: alert.project_alias,
1567
+ actor_id: alert.actor_id,
1568
+ });
1569
+ }
1570
+ }
1571
+ // ── Knowledge Gap Detection ──
1572
+ async handleGapsCommand(context) {
1573
+ const runs = await this.runStateStore.listRuns();
1574
+ const memories = this.config.service.memory_enabled
1575
+ ? await this.memoryStore.listRecentMemories({ scope: 'project', project_alias: '' }, 200)
1576
+ : [];
1577
+ const gaps = detectKnowledgeGaps(runs, memories);
1578
+ const text = formatKnowledgeGaps(gaps);
1579
+ await this.sendTextReply(context.chat_id, text, context.message_id, context.text);
1580
+ }
1476
1581
  // ── Direction 6: Timeline ──
1477
1582
  async handleTimelineCommand(context, selectionKey, projectArg) {
1478
1583
  const projectContext = await this.resolveProjectContext(context, selectionKey);
@@ -3274,9 +3379,17 @@ export class FeiqueService {
3274
3379
  await this.sessionStore.dropProjectSession(conversationKey, projectAlias, session.thread_id);
3275
3380
  }
3276
3381
  }
3277
- async sendTextReply(chatId, body, replyToMessageId, originalText, presentation) {
3382
+ async sendTextReply(chatId, body, replyToMessageId, originalText, presentation, mentionActor) {
3383
+ // In group chats, prepend @mention to the reply so the requester gets notified
3384
+ const actor = mentionActor ?? this.currentMessageContext;
3385
+ let mentionPrefix = '';
3386
+ if (actor?.chat_type === 'group' && actor.actor_id) {
3387
+ const displayName = actor.actor_name || actor.actor_id;
3388
+ mentionPrefix = `<at user_id="${actor.actor_id}">${displayName}</at>\n`;
3389
+ }
3390
+ const bodyWithMention = mentionPrefix ? mentionPrefix + body : body;
3278
3391
  const title = this.buildReplyTitle(this.sanitizeUserVisibleReply(body));
3279
- const formattedBody = this.sanitizeUserVisibleReply(this.formatQuotedReply(body, originalText));
3392
+ const formattedBody = this.sanitizeUserVisibleReply(this.formatQuotedReply(bodyWithMention, originalText));
3280
3393
  if (this.config.service.reply_mode === 'card') {
3281
3394
  const card = buildMessageCard({
3282
3395
  title,
@@ -3318,7 +3431,7 @@ export class FeiqueService {
3318
3431
  return response;
3319
3432
  }
3320
3433
  if (this.config.service.reply_quote_user_message && replyToMessageId) {
3321
- const response = await this.feishuClient.sendText(chatId, this.sanitizeUserVisibleReply(body), { replyToMessageId });
3434
+ const response = await this.feishuClient.sendText(chatId, this.sanitizeUserVisibleReply(bodyWithMention), { replyToMessageId });
3322
3435
  await this.auditLog.append({
3323
3436
  type: 'message.replied',
3324
3437
  chat_id: chatId,