feique 1.3.2 → 1.4.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.en.md +3 -2
- package/README.md +3 -2
- package/dist/backend/claude.js +28 -54
- package/dist/backend/claude.js.map +1 -1
- package/dist/backend/factory.d.ts +28 -0
- package/dist/backend/factory.js +61 -0
- package/dist/backend/factory.js.map +1 -1
- package/dist/backend/probe.d.ts +29 -0
- package/dist/backend/probe.js +99 -0
- package/dist/backend/probe.js.map +1 -0
- package/dist/bridge/admin-config.d.ts +47 -0
- package/dist/bridge/admin-config.js +141 -0
- package/dist/bridge/admin-config.js.map +1 -0
- package/dist/bridge/collab-commands.d.ts +42 -0
- package/dist/bridge/collab-commands.js +254 -0
- package/dist/bridge/collab-commands.js.map +1 -0
- package/dist/bridge/commands.d.ts +1 -1
- package/dist/bridge/commands.js +3 -0
- package/dist/bridge/commands.js.map +1 -1
- package/dist/bridge/feishu-commands.d.ts +27 -0
- package/dist/bridge/feishu-commands.js +462 -0
- package/dist/bridge/feishu-commands.js.map +1 -0
- package/dist/bridge/lifecycle.d.ts +46 -0
- package/dist/bridge/lifecycle.js +228 -0
- package/dist/bridge/lifecycle.js.map +1 -0
- package/dist/bridge/memory-commands.d.ts +26 -0
- package/dist/bridge/memory-commands.js +330 -0
- package/dist/bridge/memory-commands.js.map +1 -0
- package/dist/bridge/reply-builders.d.ts +30 -0
- package/dist/bridge/reply-builders.js +72 -0
- package/dist/bridge/reply-builders.js.map +1 -0
- package/dist/bridge/run-pipeline.d.ts +86 -0
- package/dist/bridge/run-pipeline.js +442 -0
- package/dist/bridge/run-pipeline.js.map +1 -0
- package/dist/bridge/run-scheduler.d.ts +47 -0
- package/dist/bridge/run-scheduler.js +121 -0
- package/dist/bridge/run-scheduler.js.map +1 -0
- package/dist/bridge/service-utils.d.ts +47 -0
- package/dist/bridge/service-utils.js +309 -0
- package/dist/bridge/service-utils.js.map +1 -0
- package/dist/bridge/service.d.ts +114 -66
- package/dist/bridge/service.js +225 -2196
- package/dist/bridge/service.js.map +1 -1
- package/dist/config/load.js +1 -1
- package/dist/config/load.js.map +1 -1
- package/dist/config/paths.js +1 -20
- package/dist/config/paths.js.map +1 -1
- package/dist/config/schema.d.ts +3 -0
- package/dist/config/schema.js +3 -1
- package/dist/config/schema.js.map +1 -1
- package/dist/feishu/long-connection.js +1 -0
- package/dist/feishu/long-connection.js.map +1 -1
- package/dist/feishu/webhook.js +1 -0
- package/dist/feishu/webhook.js.map +1 -1
- package/package.json +1 -1
package/dist/bridge/service.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import { watch as watchFile } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import { randomUUID } from 'node:crypto';
|
|
5
4
|
import { buildHelpText, buildFullHelpText, isReadOnlyCommand, normalizeIncomingText, parseBridgeCommand, } from './commands.js';
|
|
6
5
|
import { buildConversationKey } from '../state/session-store.js';
|
|
7
6
|
import { buildMessageCard, buildStatusCard } from '../feishu/cards.js';
|
|
@@ -11,38 +10,34 @@ import { IdempotencyStore } from '../state/idempotency-store.js';
|
|
|
11
10
|
import { RunStateStore } from '../state/run-state-store.js';
|
|
12
11
|
import { isProcessAlive, terminateProcess } from '../runtime/process.js';
|
|
13
12
|
import { resolveKnowledgeRoots, searchKnowledgeBase } from '../knowledge/search.js';
|
|
14
|
-
import { FeishuWikiClient } from '../feishu/wiki.js';
|
|
15
|
-
import { FeishuDocClient } from '../feishu/doc.js';
|
|
16
|
-
import { FeishuBaseClient } from '../feishu/base.js';
|
|
17
|
-
import { FeishuTaskClient } from '../feishu/task.js';
|
|
18
13
|
import { resolveMessageResources } from '../feishu/message-resource.js';
|
|
19
14
|
import { MemoryStore } from '../state/memory-store.js';
|
|
20
|
-
import { retrieveMemoryContext } from '../memory/retrieve.js';
|
|
21
|
-
import { summarizeThreadTurn } from '../memory/summarize.js';
|
|
22
15
|
import { CodexSessionIndex } from '../codex/session-index.js';
|
|
23
16
|
import { resolveProjectBackendWithOverride, resolveProjectBackendName } from '../backend/factory.js';
|
|
17
|
+
import { buildQueueKey, isExecutionRunStatus, isVisibleRunStatus, mapRunStatusToPhase, buildMessageDedupeKey, buildCardDedupeKey, truncateExcerpt, friendlyErrorMessage, resolveAdminListTarget, buildConversationKeyForConversation, renderMemorySection, formatAgeFromNow, replaceObject, replaceProjects, } from './service-utils.js';
|
|
18
|
+
import { handleDocCommand as handleDocCommandImpl, handleTaskCommand as handleTaskCommandImpl, handleBaseCommand as handleBaseCommandImpl, handleWikiCommand as handleWikiCommandImpl, } from './feishu-commands.js';
|
|
19
|
+
import { handleMemoryCommand as handleMemoryCommandImpl } from './memory-commands.js';
|
|
20
|
+
import { handleAdminConfigCommand as handleAdminConfigCommandImpl, parseProjectPatch as parseProjectPatchImpl, } from './admin-config.js';
|
|
21
|
+
import { scheduleProjectExecution as scheduleProjectExecutionImpl, buildAcknowledgedRunReply, buildRunStatusSummary, } from './run-scheduler.js';
|
|
22
|
+
import { formatQuotedReply, buildReplyTitle, sanitizeUserVisibleReply, supportsInteractiveCardActions, resolveRunLifecycleReplyMode, buildRunLifecycleCard, } from './reply-builders.js';
|
|
23
|
+
import { executePrompt as executePromptImpl } from './run-pipeline.js';
|
|
24
|
+
import { recoverRuntimeState as recoverRuntimeStateImpl, reloadConfig as reloadConfigImpl, runDigestCycle as runDigestCycleImpl, runMemoryMaintenance as runMemoryMaintenanceImpl, runAuditMaintenance as runAuditMaintenanceImpl, runMaintenanceCycle as runMaintenanceCycleImpl, } from './lifecycle.js';
|
|
25
|
+
import { handleLearnCommand as handleLearnCommandImpl, handleRecallCommand as handleRecallCommandImpl, handleHandoffCommand as handleHandoffCommandImpl, handlePickupCommand as handlePickupCommandImpl, handleReviewCommand as handleReviewCommandImpl, handleApproveCommand as handleApproveCommandImpl, handleRejectCommand as handleRejectCommandImpl, handleInsightsCommand as handleInsightsCommandImpl, handleTrustCommand as handleTrustCommandImpl, handleDigestCommand as handleDigestCommandImpl, handleGapsCommand as handleGapsCommandImpl, handleTimelineCommand as handleTimelineCommandImpl, } from './collab-commands.js';
|
|
24
26
|
import { bindProjectAlias, createProjectAlias, removeProjectAlias, updateProjectConfig, updateStringList } from '../config/mutate.js';
|
|
25
|
-
import { buildFeishuPost
|
|
27
|
+
import { buildFeishuPost } from '../feishu/text.js';
|
|
26
28
|
import { ConfigHistoryStore } from '../state/config-history-store.js';
|
|
27
29
|
import { loadBridgeConfigFile } from '../config/load.js';
|
|
28
|
-
import { writeUtf8Atomic } from '../utils/fs.js';
|
|
29
30
|
import { expandHomePath } from '../utils/path.js';
|
|
30
31
|
import { canAccessGlobalCapability, canAccessProject, canAccessProjectCapability, describeMinimumRole, filterAccessibleProjects, resolveProjectAccessRole } from '../security/access.js';
|
|
31
32
|
import { adoptProjectSession as adoptSharedProjectSession, listBridgeSessions as listSharedBridgeSessions, switchProjectBinding as switchSharedProjectBinding } from '../control-plane/project-session.js';
|
|
32
|
-
import {
|
|
33
|
+
import { getProjectAuditDir, getProjectCacheDir, getProjectDownloadsDir, getProjectTempDir } from '../projects/paths.js';
|
|
33
34
|
import { buildTeamActivityView, detectOverlaps, formatTeamView, formatOverlapAlerts } from '../collaboration/awareness.js';
|
|
34
|
-
import {
|
|
35
|
-
import {
|
|
36
|
-
import { analyzeTeamHealth, formatInsightsReport } from '../collaboration/insights.js';
|
|
37
|
-
import { classifyOperation, enforceTrustBoundary, recordRunOutcome, formatTrustState, DEFAULT_TRUST_POLICY } from '../collaboration/trust.js';
|
|
38
|
-
import { buildProjectTimeline, buildOnboardingContext, formatTimeline, isNewActor } from '../collaboration/timeline.js';
|
|
35
|
+
import { createReview } from '../collaboration/handoff.js';
|
|
36
|
+
import { classifyOperation, enforceTrustBoundary } from '../collaboration/trust.js';
|
|
39
37
|
import { HandoffStore } from '../state/handoff-store.js';
|
|
40
38
|
import { TrustStore } from '../state/trust-store.js';
|
|
41
39
|
import { IntentClassifier } from './intent-classifier.js';
|
|
42
|
-
import {
|
|
43
|
-
import { checkRunAlerts, checkLongRunningAlerts, formatAlert, DEFAULT_ALERT_RULES } from '../collaboration/proactive-alerts.js';
|
|
44
|
-
import { detectKnowledgeGaps, formatKnowledgeGaps } from '../collaboration/knowledge-gaps.js';
|
|
45
|
-
import { estimateCost } from '../observability/cost.js';
|
|
40
|
+
import { checkRunAlerts, formatAlert, DEFAULT_ALERT_RULES } from '../collaboration/proactive-alerts.js';
|
|
46
41
|
export class FeiqueService {
|
|
47
42
|
config;
|
|
48
43
|
feishuClient;
|
|
@@ -64,6 +59,10 @@ export class FeiqueService {
|
|
|
64
59
|
activeRuns = new Map();
|
|
65
60
|
runReplyTargets = new Map();
|
|
66
61
|
chatRateWindows = new Map();
|
|
62
|
+
/** Dedupe admin notifications for backend failover: one alert per (from→to) direction per process lifetime. */
|
|
63
|
+
failoverNotified = new Set();
|
|
64
|
+
/** Dedupe rejected-chat notifications: one reply + one admin alert per chat_id per process lifetime. */
|
|
65
|
+
rejectedChatNotified = new Set();
|
|
67
66
|
maintenanceTimer;
|
|
68
67
|
digestTimer;
|
|
69
68
|
configWatcher;
|
|
@@ -102,79 +101,14 @@ export class FeiqueService {
|
|
|
102
101
|
}
|
|
103
102
|
}
|
|
104
103
|
async recoverRuntimeState() {
|
|
105
|
-
|
|
106
|
-
for (const run of recovered) {
|
|
107
|
-
await this.auditLog.append({
|
|
108
|
-
type: 'codex.run.recovered',
|
|
109
|
-
run_id: run.run_id,
|
|
110
|
-
project_alias: run.project_alias,
|
|
111
|
-
conversation_key: run.conversation_key,
|
|
112
|
-
status: run.status,
|
|
113
|
-
pid: run.pid,
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
return recovered;
|
|
104
|
+
return recoverRuntimeStateImpl(this);
|
|
117
105
|
}
|
|
118
|
-
/**
|
|
119
|
-
* Reload config from disk with validation, diff, and admin notification.
|
|
120
|
-
*
|
|
121
|
-
* Flow:
|
|
122
|
-
* 1. Parse new config — if invalid, reject and notify admin with error
|
|
123
|
-
* 2. Diff against current config — identify what changed
|
|
124
|
-
* 3. Apply new config to memory
|
|
125
|
-
* 4. Notify admin chat(s) with change summary
|
|
126
|
-
*/
|
|
127
106
|
async reloadConfig(configPath) {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
newConfig = config;
|
|
132
|
-
}
|
|
133
|
-
catch (error) {
|
|
134
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
135
|
-
this.logger.error({ configPath, error: msg }, 'Config reload rejected — invalid config');
|
|
136
|
-
// Notify admin about the broken config
|
|
137
|
-
const alertText = `🔴 配置变更被拒绝\n\n文件: ${configPath}\n原因: ${msg}\n\n当前服务继续使用旧配置运行。请修正后重新保存。`;
|
|
138
|
-
for (const chatId of this.config.security.admin_chat_ids) {
|
|
139
|
-
try {
|
|
140
|
-
await this.feishuClient.sendText(chatId, alertText);
|
|
141
|
-
}
|
|
142
|
-
catch { /* best-effort */ }
|
|
143
|
-
}
|
|
144
|
-
await this.auditLog.append({
|
|
145
|
-
type: 'config.reload.rejected',
|
|
146
|
-
config_path: configPath,
|
|
147
|
-
error: msg,
|
|
148
|
-
});
|
|
149
|
-
return { ok: false, error: msg };
|
|
107
|
+
const result = await reloadConfigImpl(this, configPath);
|
|
108
|
+
if (result.newConfig) {
|
|
109
|
+
this.config = result.newConfig;
|
|
150
110
|
}
|
|
151
|
-
|
|
152
|
-
const changes = diffConfigs(this.config, newConfig);
|
|
153
|
-
if (changes.length === 0) {
|
|
154
|
-
this.logger.debug({ configPath }, 'Config file changed but no effective differences');
|
|
155
|
-
return { ok: true, changes: [] };
|
|
156
|
-
}
|
|
157
|
-
// Apply
|
|
158
|
-
const oldConfig = this.config;
|
|
159
|
-
this.config = newConfig;
|
|
160
|
-
this.logger.info({ configPath, changeCount: changes.length }, 'Config reloaded');
|
|
161
|
-
// Notify admin
|
|
162
|
-
const changeList = changes.slice(0, 15).map((c) => ` • ${c}`).join('\n');
|
|
163
|
-
const truncated = changes.length > 15 ? `\n …及其他 ${changes.length - 15} 项变更` : '';
|
|
164
|
-
const notifyText = `✅ 配置已热加载\n\n${changes.length} 项变更:\n${changeList}${truncated}`;
|
|
165
|
-
for (const chatId of (oldConfig.security.admin_chat_ids.length > 0 ? oldConfig.security.admin_chat_ids : newConfig.security.admin_chat_ids)) {
|
|
166
|
-
try {
|
|
167
|
-
await this.feishuClient.sendText(chatId, notifyText);
|
|
168
|
-
}
|
|
169
|
-
catch { /* best-effort */ }
|
|
170
|
-
}
|
|
171
|
-
await this.auditLog.append({
|
|
172
|
-
type: 'config.reload.applied',
|
|
173
|
-
config_path: configPath,
|
|
174
|
-
change_count: changes.length,
|
|
175
|
-
changes: changes.slice(0, 20),
|
|
176
|
-
});
|
|
177
|
-
return { ok: true, changes };
|
|
111
|
+
return { ok: result.ok, ...(result.error ? { error: result.error } : {}), ...(result.changes ? { changes: result.changes } : {}) };
|
|
178
112
|
}
|
|
179
113
|
/**
|
|
180
114
|
* Watch config file for changes and auto-reload with validation.
|
|
@@ -247,120 +181,16 @@ export class FeiqueService {
|
|
|
247
181
|
this.digestTimer.unref?.();
|
|
248
182
|
}
|
|
249
183
|
async runDigestCycle() {
|
|
250
|
-
|
|
251
|
-
if (chatIds.length === 0)
|
|
252
|
-
return;
|
|
253
|
-
try {
|
|
254
|
-
const period = createDigestPeriod(this.config.service.team_digest_interval_hours);
|
|
255
|
-
const runs = await this.runStateStore.listRuns();
|
|
256
|
-
const memories = this.config.service.memory_enabled
|
|
257
|
-
? await this.memoryStore.listRecentMemories({ scope: 'project', project_alias: '' }, 100)
|
|
258
|
-
: [];
|
|
259
|
-
const auditEvents = await this.auditLog.tail(500);
|
|
260
|
-
const digest = buildTeamDigest(runs, memories, auditEvents, period);
|
|
261
|
-
if (digest.summary.total_runs === 0) {
|
|
262
|
-
return; // Nothing to report
|
|
263
|
-
}
|
|
264
|
-
const text = formatTeamDigest(digest);
|
|
265
|
-
for (const chatId of chatIds) {
|
|
266
|
-
try {
|
|
267
|
-
await this.feishuClient.sendText(chatId, text);
|
|
268
|
-
}
|
|
269
|
-
catch (error) {
|
|
270
|
-
this.logger.warn({ chatId, error }, 'Failed to send team digest');
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
await this.auditLog.append({
|
|
274
|
-
type: 'collaboration.digest.sent',
|
|
275
|
-
period_label: period.label,
|
|
276
|
-
total_runs: digest.summary.total_runs,
|
|
277
|
-
chat_ids: chatIds,
|
|
278
|
-
});
|
|
279
|
-
// Send per-project mini-digests to project notification chats
|
|
280
|
-
for (const projectDigest of digest.topProjects) {
|
|
281
|
-
const projectChatIds = this.config.projects[projectDigest.alias]?.notification_chat_ids ?? [];
|
|
282
|
-
if (projectChatIds.length === 0)
|
|
283
|
-
continue;
|
|
284
|
-
const successPct = Math.round(projectDigest.success_rate * 100);
|
|
285
|
-
const miniDigestText = [
|
|
286
|
-
`📊 项目摘要 [${projectDigest.alias}] — ${period.label}`,
|
|
287
|
-
`运行: ${projectDigest.runs} | 成功率: ${successPct}%`,
|
|
288
|
-
`参与者: ${projectDigest.actors.join(', ') || '无'}`,
|
|
289
|
-
].join('\n');
|
|
290
|
-
for (const chatId of projectChatIds) {
|
|
291
|
-
try {
|
|
292
|
-
await this.feishuClient.sendText(chatId, miniDigestText);
|
|
293
|
-
}
|
|
294
|
-
catch { /* best-effort */ }
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
catch (error) {
|
|
299
|
-
this.logger.error({ error }, 'Failed to generate team digest');
|
|
300
|
-
}
|
|
184
|
+
return runDigestCycleImpl(this);
|
|
301
185
|
}
|
|
302
186
|
async runMemoryMaintenance() {
|
|
303
|
-
|
|
304
|
-
return 0;
|
|
305
|
-
}
|
|
306
|
-
const cleaned = await this.memoryStore.cleanupExpiredMemories();
|
|
307
|
-
if (cleaned > 0) {
|
|
308
|
-
await this.auditLog.append({
|
|
309
|
-
type: 'memory.archive.expired.maintenance',
|
|
310
|
-
count: cleaned,
|
|
311
|
-
});
|
|
312
|
-
this.logger.info({ cleaned }, 'Expired memories cleaned by background maintenance');
|
|
313
|
-
}
|
|
314
|
-
return cleaned;
|
|
187
|
+
return runMemoryMaintenanceImpl(this);
|
|
315
188
|
}
|
|
316
189
|
async runAuditMaintenance() {
|
|
317
|
-
|
|
318
|
-
let scanned = 0;
|
|
319
|
-
let archived = 0;
|
|
320
|
-
let removed = 0;
|
|
321
|
-
for (const target of auditTargets) {
|
|
322
|
-
const auditLog = new AuditLog(target.stateDir, target.fileName);
|
|
323
|
-
const result = await auditLog.cleanup({
|
|
324
|
-
retentionDays: this.config.service.audit_retention_days,
|
|
325
|
-
archiveAfterDays: this.config.service.audit_archive_after_days,
|
|
326
|
-
archiveDir: target.archiveDir,
|
|
327
|
-
});
|
|
328
|
-
scanned += 1;
|
|
329
|
-
archived += result.archived;
|
|
330
|
-
removed += result.removed;
|
|
331
|
-
}
|
|
332
|
-
if (archived > 0 || removed > 0) {
|
|
333
|
-
await this.auditLog.append({
|
|
334
|
-
type: 'audit.cleanup.completed',
|
|
335
|
-
scanned,
|
|
336
|
-
archived,
|
|
337
|
-
removed,
|
|
338
|
-
});
|
|
339
|
-
this.logger.info({ scanned, archived, removed }, 'Audit retention cleanup completed');
|
|
340
|
-
}
|
|
341
|
-
return { scanned, archived, removed };
|
|
190
|
+
return runAuditMaintenanceImpl(this);
|
|
342
191
|
}
|
|
343
192
|
async runMaintenanceCycle() {
|
|
344
|
-
|
|
345
|
-
await this.runMemoryMaintenance();
|
|
346
|
-
}
|
|
347
|
-
await this.runAuditMaintenance();
|
|
348
|
-
// Proactive: check for long-running tasks
|
|
349
|
-
try {
|
|
350
|
-
const activeRuns = await this.runStateStore.listRuns();
|
|
351
|
-
const longAlerts = checkLongRunningAlerts(activeRuns);
|
|
352
|
-
for (const alert of longAlerts) {
|
|
353
|
-
const text = formatAlert(alert);
|
|
354
|
-
for (const chatId of this.config.security.admin_chat_ids) {
|
|
355
|
-
try {
|
|
356
|
-
await this.feishuClient.sendText(chatId, text);
|
|
357
|
-
}
|
|
358
|
-
catch { /* best-effort */ }
|
|
359
|
-
}
|
|
360
|
-
await this.notifyProjectChats(alert.project_alias, text);
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
catch { /* best-effort */ }
|
|
193
|
+
return runMaintenanceCycleImpl(this);
|
|
364
194
|
}
|
|
365
195
|
async handleIncomingMessage(context) {
|
|
366
196
|
this.currentMessageContext = context;
|
|
@@ -510,7 +340,7 @@ export class FeiqueService {
|
|
|
510
340
|
await this.handleGapsCommand(context);
|
|
511
341
|
return;
|
|
512
342
|
case 'prompt':
|
|
513
|
-
await this.handlePromptMessage(context, selectionKey, command.prompt
|
|
343
|
+
await this.handlePromptMessage(context, selectionKey, command.prompt);
|
|
514
344
|
return;
|
|
515
345
|
}
|
|
516
346
|
}
|
|
@@ -655,430 +485,7 @@ export class FeiqueService {
|
|
|
655
485
|
return this.runStateStore.listRuns();
|
|
656
486
|
}
|
|
657
487
|
async executePrompt(input) {
|
|
658
|
-
|
|
659
|
-
(await this.sessionStore.ensureConversation(input.sessionKey, {
|
|
660
|
-
chat_id: input.chatId,
|
|
661
|
-
actor_id: input.actorId,
|
|
662
|
-
tenant_key: input.tenantKey,
|
|
663
|
-
scope: input.project.session_scope,
|
|
664
|
-
}));
|
|
665
|
-
let currentSession = conversation.projects[input.projectAlias];
|
|
666
|
-
// Auto-adopt latest local session when no active session exists
|
|
667
|
-
if (!currentSession?.thread_id && this.config.service.project_switch_auto_adopt_latest) {
|
|
668
|
-
try {
|
|
669
|
-
const sessionBackendOverrideForAdopt = await this.sessionStore.getProjectBackend(input.sessionKey, input.projectAlias);
|
|
670
|
-
const backendForAdopt = this.resolveBackendByName(input.projectAlias, sessionBackendOverrideForAdopt);
|
|
671
|
-
const latestLocal = await backendForAdopt.findLatestSession(input.project.root);
|
|
672
|
-
if (latestLocal) {
|
|
673
|
-
await this.sessionStore.upsertProjectSession(input.sessionKey, input.projectAlias, {
|
|
674
|
-
thread_id: latestLocal.sessionId,
|
|
675
|
-
});
|
|
676
|
-
const refreshed = await this.sessionStore.getConversation(input.sessionKey);
|
|
677
|
-
currentSession = refreshed?.projects[input.projectAlias];
|
|
678
|
-
this.logger.info({ projectAlias: input.projectAlias, sessionId: latestLocal.sessionId, backend: latestLocal.backend }, 'Auto-adopted latest local session for prompt execution');
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
catch { /* auto-adopt is best-effort */ }
|
|
682
|
-
}
|
|
683
|
-
if (this.config.service.memory_enabled) {
|
|
684
|
-
await this.memoryStore.cleanupExpiredMemories();
|
|
685
|
-
}
|
|
686
|
-
const memoryContext = this.config.service.memory_enabled
|
|
687
|
-
? await retrieveMemoryContext(this.memoryStore, {
|
|
688
|
-
conversationKey: input.sessionKey,
|
|
689
|
-
projectAlias: input.projectAlias,
|
|
690
|
-
threadId: currentSession?.thread_id,
|
|
691
|
-
query: input.prompt,
|
|
692
|
-
searchLimit: this.config.service.memory_search_limit,
|
|
693
|
-
groupChatId: input.incomingMessage.chat_type === 'group' ? input.incomingMessage.chat_id : undefined,
|
|
694
|
-
includeGroupMemories: this.config.service.memory_group_enabled && input.incomingMessage.chat_type === 'group',
|
|
695
|
-
})
|
|
696
|
-
: { pinnedMemories: [], relevantMemories: [], pinnedGroupMemories: [], relevantGroupMemories: [] };
|
|
697
|
-
// Direction 6: Inject onboarding context for new actors
|
|
698
|
-
let onboardingPrefix = '';
|
|
699
|
-
if (input.actorId && this.config.service.memory_enabled) {
|
|
700
|
-
try {
|
|
701
|
-
const allRuns = await this.runStateStore.listRuns();
|
|
702
|
-
if (isNewActor(input.actorId, allRuns, input.projectAlias)) {
|
|
703
|
-
const memories = await this.memoryStore.listRecentMemories({ scope: 'project', project_alias: input.projectAlias }, 10);
|
|
704
|
-
const timeline = buildProjectTimeline(allRuns, memories, [], input.projectAlias, 10);
|
|
705
|
-
onboardingPrefix = buildOnboardingContext(timeline, memories, input.projectAlias);
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
catch { /* onboarding injection is best-effort */ }
|
|
709
|
-
}
|
|
710
|
-
const effectivePrompt = onboardingPrefix
|
|
711
|
-
? `${onboardingPrefix}\n\n${input.prompt}`
|
|
712
|
-
: input.prompt;
|
|
713
|
-
const bridgePrompt = await this.buildBridgePrompt(input.projectAlias, input.project, input.incomingMessage, effectivePrompt, memoryContext);
|
|
714
|
-
const startedAt = Date.now();
|
|
715
|
-
const projectRoot = this.resolveProjectRoot(input.project);
|
|
716
|
-
const runId = input.runId ?? randomUUID();
|
|
717
|
-
let lastProgressUpdate = 0;
|
|
718
|
-
const activeRun = {
|
|
719
|
-
runId,
|
|
720
|
-
controller: new AbortController(),
|
|
721
|
-
};
|
|
722
|
-
this.activeRuns.set(input.queueKey, activeRun);
|
|
723
|
-
const sessionBackendOverride = await this.sessionStore.getProjectBackend(input.sessionKey, input.projectAlias);
|
|
724
|
-
const backend = this.resolveBackendByName(input.projectAlias, sessionBackendOverride);
|
|
725
|
-
const backendLabel = backend.name === 'claude' ? 'Claude' : 'Codex';
|
|
726
|
-
await this.updateRunStartedReply(input.chatId, input.projectAlias, runId, backendLabel);
|
|
727
|
-
await this.runStateStore.upsertRun(runId, {
|
|
728
|
-
queue_key: input.queueKey,
|
|
729
|
-
conversation_key: input.sessionKey,
|
|
730
|
-
project_alias: input.projectAlias,
|
|
731
|
-
chat_id: input.chatId,
|
|
732
|
-
actor_id: input.actorId,
|
|
733
|
-
actor_name: input.incomingMessage.actor_name,
|
|
734
|
-
session_id: currentSession?.thread_id,
|
|
735
|
-
project_root: projectRoot,
|
|
736
|
-
prompt_excerpt: truncateExcerpt(input.prompt),
|
|
737
|
-
status: 'running',
|
|
738
|
-
status_detail: undefined,
|
|
739
|
-
});
|
|
740
|
-
await this.auditLog.append({
|
|
741
|
-
type: 'codex.run.started',
|
|
742
|
-
run_id: runId,
|
|
743
|
-
chat_id: input.chatId,
|
|
744
|
-
actor_id: input.actorId,
|
|
745
|
-
project_alias: input.projectAlias,
|
|
746
|
-
conversation_key: input.sessionKey,
|
|
747
|
-
session_id: currentSession?.thread_id,
|
|
748
|
-
prompt: input.prompt,
|
|
749
|
-
});
|
|
750
|
-
await this.appendProjectAuditEvent(input.projectAlias, input.project, {
|
|
751
|
-
type: 'codex.run.started',
|
|
752
|
-
run_id: runId,
|
|
753
|
-
chat_id: input.chatId,
|
|
754
|
-
actor_id: input.actorId,
|
|
755
|
-
session_id: currentSession?.thread_id,
|
|
756
|
-
project_root: projectRoot,
|
|
757
|
-
});
|
|
758
|
-
this.logger.info({
|
|
759
|
-
runId,
|
|
760
|
-
queueKey: input.queueKey,
|
|
761
|
-
sessionKey: input.sessionKey,
|
|
762
|
-
projectAlias: input.projectAlias,
|
|
763
|
-
projectRoot,
|
|
764
|
-
sessionId: currentSession?.thread_id,
|
|
765
|
-
}, 'Codex run started');
|
|
766
|
-
this.metrics?.recordCodexTurnStarted(input.projectAlias, runId);
|
|
767
|
-
try {
|
|
768
|
-
const outputTokenLimit = backend.name === 'claude'
|
|
769
|
-
? (this.config.claude?.output_token_limit ?? this.config.codex.output_token_limit)
|
|
770
|
-
: this.config.codex.output_token_limit;
|
|
771
|
-
const result = await backend.run({
|
|
772
|
-
workdir: input.project.root,
|
|
773
|
-
prompt: bridgePrompt,
|
|
774
|
-
sessionId: currentSession?.thread_id,
|
|
775
|
-
timeoutMs: backend.name === 'claude'
|
|
776
|
-
? (this.config.claude?.run_timeout_ms ?? this.config.codex.run_timeout_ms)
|
|
777
|
-
: this.config.codex.run_timeout_ms,
|
|
778
|
-
signal: activeRun.controller.signal,
|
|
779
|
-
logger: this.logger,
|
|
780
|
-
projectConfig: backend.name === 'codex'
|
|
781
|
-
? {
|
|
782
|
-
profile: input.project.profile ?? this.config.codex.default_profile,
|
|
783
|
-
model: input.project.codex_model,
|
|
784
|
-
sandbox: input.project.codex_sandbox ?? input.project.sandbox ?? this.config.codex.default_sandbox,
|
|
785
|
-
tempDir: this.resolveProjectTempDir(input.projectAlias, input.project),
|
|
786
|
-
cacheDir: this.resolveProjectCacheDir(input.projectAlias, input.project),
|
|
787
|
-
}
|
|
788
|
-
: {
|
|
789
|
-
permissionMode: input.project.claude_permission_mode ?? this.config.claude?.default_permission_mode,
|
|
790
|
-
model: input.project.claude_model ?? this.config.claude?.default_model,
|
|
791
|
-
maxBudgetUsd: input.project.claude_max_budget_usd ?? this.config.claude?.max_budget_usd,
|
|
792
|
-
allowedTools: input.project.claude_allowed_tools ?? this.config.claude?.allowed_tools,
|
|
793
|
-
systemPromptAppend: input.project.claude_system_prompt_append ?? this.config.claude?.system_prompt_append,
|
|
794
|
-
},
|
|
795
|
-
onSpawn: async (pid) => {
|
|
796
|
-
activeRun.pid = pid;
|
|
797
|
-
await this.runStateStore.upsertRun(runId, {
|
|
798
|
-
queue_key: input.queueKey,
|
|
799
|
-
conversation_key: input.sessionKey,
|
|
800
|
-
project_alias: input.projectAlias,
|
|
801
|
-
chat_id: input.chatId,
|
|
802
|
-
actor_id: input.actorId,
|
|
803
|
-
session_id: currentSession?.thread_id,
|
|
804
|
-
project_root: projectRoot,
|
|
805
|
-
prompt_excerpt: truncateExcerpt(input.prompt),
|
|
806
|
-
status: 'running',
|
|
807
|
-
status_detail: undefined,
|
|
808
|
-
pid,
|
|
809
|
-
});
|
|
810
|
-
},
|
|
811
|
-
onEvent: async (event) => {
|
|
812
|
-
if (!this.config.service.emit_progress_updates) {
|
|
813
|
-
return;
|
|
814
|
-
}
|
|
815
|
-
const message = backend.summarizeEvent(event);
|
|
816
|
-
if (!message) {
|
|
817
|
-
return;
|
|
818
|
-
}
|
|
819
|
-
const now = Date.now();
|
|
820
|
-
if (now - lastProgressUpdate < this.config.service.progress_update_interval_ms) {
|
|
821
|
-
return;
|
|
822
|
-
}
|
|
823
|
-
lastProgressUpdate = now;
|
|
824
|
-
await this.updateRunProgressReply(input, runId, message, backendLabel);
|
|
825
|
-
},
|
|
826
|
-
});
|
|
827
|
-
const excerpt = result.finalMessage.slice(0, outputTokenLimit);
|
|
828
|
-
if (!excerpt.trim()) {
|
|
829
|
-
this.logger.warn({
|
|
830
|
-
runId,
|
|
831
|
-
queueKey: input.queueKey,
|
|
832
|
-
sessionKey: input.sessionKey,
|
|
833
|
-
projectAlias: input.projectAlias,
|
|
834
|
-
sessionId: result.sessionId,
|
|
835
|
-
durationMs: Date.now() - startedAt,
|
|
836
|
-
}, 'Codex run completed without a displayable final message');
|
|
837
|
-
}
|
|
838
|
-
// Extract and send any [SEND_FILE:path] markers before text reply
|
|
839
|
-
const { cleanText: excerptWithoutFiles, filePaths } = extractFileMarkers(excerpt);
|
|
840
|
-
if (filePaths.length > 0) {
|
|
841
|
-
for (const filePath of filePaths) {
|
|
842
|
-
try {
|
|
843
|
-
await this.feishuClient.sendFile(input.chatId, filePath);
|
|
844
|
-
this.logger.info({ chatId: input.chatId, filePath }, 'Sent file to Feishu');
|
|
845
|
-
}
|
|
846
|
-
catch (err) {
|
|
847
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
848
|
-
this.logger.warn({ chatId: input.chatId, filePath, error: msg }, 'Failed to send file to Feishu');
|
|
849
|
-
// Notify user about the failure inline
|
|
850
|
-
excerptWithoutFiles === excerpt || await this.feishuClient.sendText(input.chatId, `⚠️ 文件发送失败: ${filePath}\n${msg}`);
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
}
|
|
854
|
-
const finalExcerpt = excerptWithoutFiles.trim() || excerpt;
|
|
855
|
-
const cardSummary = truncateForFeishuCard(finalExcerpt || `${backendLabel} 已完成,但没有返回可显示文本。`);
|
|
856
|
-
await this.auditLog.append({
|
|
857
|
-
type: 'codex.run.completed',
|
|
858
|
-
run_id: runId,
|
|
859
|
-
chat_id: input.chatId,
|
|
860
|
-
actor_id: input.actorId,
|
|
861
|
-
project_alias: input.projectAlias,
|
|
862
|
-
conversation_key: input.sessionKey,
|
|
863
|
-
session_id: result.sessionId,
|
|
864
|
-
exit_code: result.exitCode,
|
|
865
|
-
duration_ms: Date.now() - startedAt,
|
|
866
|
-
backend: backend.name,
|
|
867
|
-
});
|
|
868
|
-
await this.appendProjectAuditEvent(input.projectAlias, input.project, {
|
|
869
|
-
type: 'codex.run.completed',
|
|
870
|
-
run_id: runId,
|
|
871
|
-
chat_id: input.chatId,
|
|
872
|
-
actor_id: input.actorId,
|
|
873
|
-
session_id: result.sessionId,
|
|
874
|
-
duration_ms: Date.now() - startedAt,
|
|
875
|
-
backend: backend.name,
|
|
876
|
-
});
|
|
877
|
-
this.logger.info({
|
|
878
|
-
runId,
|
|
879
|
-
queueKey: input.queueKey,
|
|
880
|
-
sessionKey: input.sessionKey,
|
|
881
|
-
projectAlias: input.projectAlias,
|
|
882
|
-
sessionId: result.sessionId,
|
|
883
|
-
exitCode: result.exitCode,
|
|
884
|
-
finalMessageChars: excerpt.length,
|
|
885
|
-
durationMs: Date.now() - startedAt,
|
|
886
|
-
}, 'Codex run completed');
|
|
887
|
-
await this.sessionStore.upsertProjectSession(input.sessionKey, input.projectAlias, {
|
|
888
|
-
thread_id: result.sessionId,
|
|
889
|
-
last_prompt: input.prompt,
|
|
890
|
-
last_response_excerpt: excerpt,
|
|
891
|
-
});
|
|
892
|
-
if (this.config.service.memory_enabled && result.sessionId) {
|
|
893
|
-
const summaryDraft = summarizeThreadTurn({
|
|
894
|
-
previousSummary: memoryContext.threadSummary?.summary,
|
|
895
|
-
prompt: input.prompt,
|
|
896
|
-
responseExcerpt: excerpt,
|
|
897
|
-
maxChars: this.config.service.thread_summary_max_chars,
|
|
898
|
-
});
|
|
899
|
-
const threadSummary = await this.memoryStore.upsertThreadSummary({
|
|
900
|
-
conversation_key: input.sessionKey,
|
|
901
|
-
project_alias: input.projectAlias,
|
|
902
|
-
thread_id: result.sessionId,
|
|
903
|
-
summary: summaryDraft.summary,
|
|
904
|
-
recent_prompt: input.prompt,
|
|
905
|
-
recent_response_excerpt: excerpt,
|
|
906
|
-
files_touched: summaryDraft.filesTouched,
|
|
907
|
-
open_tasks: summaryDraft.openTasks,
|
|
908
|
-
decisions: summaryDraft.decisions,
|
|
909
|
-
});
|
|
910
|
-
await this.auditLog.append({
|
|
911
|
-
type: 'memory.thread_summary.updated',
|
|
912
|
-
run_id: runId,
|
|
913
|
-
project_alias: input.projectAlias,
|
|
914
|
-
conversation_key: input.sessionKey,
|
|
915
|
-
thread_id: result.sessionId,
|
|
916
|
-
files_touched: threadSummary.files_touched,
|
|
917
|
-
});
|
|
918
|
-
}
|
|
919
|
-
await this.enforceSessionHistoryLimit(input.sessionKey, input.projectAlias);
|
|
920
|
-
await this.runStateStore.upsertRun(runId, {
|
|
921
|
-
queue_key: input.queueKey,
|
|
922
|
-
conversation_key: input.sessionKey,
|
|
923
|
-
project_alias: input.projectAlias,
|
|
924
|
-
chat_id: input.chatId,
|
|
925
|
-
actor_id: input.actorId,
|
|
926
|
-
session_id: result.sessionId,
|
|
927
|
-
project_root: projectRoot,
|
|
928
|
-
pid: activeRun.pid,
|
|
929
|
-
prompt_excerpt: truncateExcerpt(input.prompt),
|
|
930
|
-
status: 'success',
|
|
931
|
-
status_detail: undefined,
|
|
932
|
-
input_tokens: result.inputTokens,
|
|
933
|
-
output_tokens: result.outputTokens,
|
|
934
|
-
estimated_cost_usd: estimateCost(result.inputTokens, result.outputTokens, backend.name),
|
|
935
|
-
});
|
|
936
|
-
this.metrics?.recordCodexTurn('success', input.projectAlias, (Date.now() - startedAt) / 1000, runId);
|
|
937
|
-
// Record cost and token metrics
|
|
938
|
-
if (result.inputTokens || result.outputTokens) {
|
|
939
|
-
const costUsd = estimateCost(result.inputTokens, result.outputTokens, backend.name) ?? 0;
|
|
940
|
-
this.metrics?.recordCost(input.projectAlias, backend.name, costUsd);
|
|
941
|
-
this.metrics?.recordTokens(input.projectAlias, backend.name, result.inputTokens ?? 0, result.outputTokens ?? 0);
|
|
942
|
-
}
|
|
943
|
-
// Direction 5: Record trust outcome
|
|
944
|
-
try {
|
|
945
|
-
const trustState = await this.trustStore.getOrCreate(input.projectAlias);
|
|
946
|
-
const updated = recordRunOutcome(trustState, true, DEFAULT_TRUST_POLICY);
|
|
947
|
-
await this.trustStore.update(input.projectAlias, updated);
|
|
948
|
-
this.metrics?.recordTrustLevel(input.projectAlias, updated.current_level);
|
|
949
|
-
}
|
|
950
|
-
catch { /* trust tracking is best-effort */ }
|
|
951
|
-
// Proactive alerts: check if this run triggers any team alerts
|
|
952
|
-
try {
|
|
953
|
-
const completedRunState = await this.runStateStore.getRun(runId);
|
|
954
|
-
if (completedRunState) {
|
|
955
|
-
await this.checkAndSendAlerts(completedRunState);
|
|
956
|
-
}
|
|
957
|
-
}
|
|
958
|
-
catch { /* alerts are best-effort */ }
|
|
959
|
-
// Direction 2: Auto-extract knowledge
|
|
960
|
-
if (this.config.service.memory_enabled && excerpt.length >= 100) {
|
|
961
|
-
try {
|
|
962
|
-
const insight = extractInsights(input.prompt, excerpt, input.projectAlias);
|
|
963
|
-
if (insight) {
|
|
964
|
-
await this.memoryStore.saveProjectMemory({
|
|
965
|
-
project_alias: insight.project_alias,
|
|
966
|
-
title: insight.title,
|
|
967
|
-
content: insight.content,
|
|
968
|
-
tags: insight.tags,
|
|
969
|
-
source: 'auto',
|
|
970
|
-
created_by: input.actorId,
|
|
971
|
-
});
|
|
972
|
-
}
|
|
973
|
-
}
|
|
974
|
-
catch { /* auto-extraction is best-effort */ }
|
|
975
|
-
}
|
|
976
|
-
await this.sendOrUpdateRunOutcome({
|
|
977
|
-
input,
|
|
978
|
-
runId,
|
|
979
|
-
title: `${backendLabel} 已完成`,
|
|
980
|
-
body: finalExcerpt || `${backendLabel} 已完成,但没有返回可显示文本。`,
|
|
981
|
-
runStatus: 'success',
|
|
982
|
-
runPhase: '已完成',
|
|
983
|
-
cardSummary,
|
|
984
|
-
sessionId: result.sessionId,
|
|
985
|
-
});
|
|
986
|
-
}
|
|
987
|
-
catch (error) {
|
|
988
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
989
|
-
const cancelled = error instanceof Error && error.name === 'AbortError' && activeRun.cancelReason === 'user';
|
|
990
|
-
const status = cancelled ? 'cancelled' : 'failure';
|
|
991
|
-
if (!cancelled && error instanceof Error && error.name === 'AbortError') {
|
|
992
|
-
activeRun.cancelReason = 'timeout';
|
|
993
|
-
}
|
|
994
|
-
if (!cancelled && activeRun.cancelReason === 'timeout') {
|
|
995
|
-
this.metrics?.recordCodexTurn('failure', input.projectAlias, (Date.now() - startedAt) / 1000, runId);
|
|
996
|
-
}
|
|
997
|
-
else {
|
|
998
|
-
this.metrics?.recordCodexTurn(cancelled ? 'cancelled' : 'failure', input.projectAlias, (Date.now() - startedAt) / 1000, runId);
|
|
999
|
-
}
|
|
1000
|
-
await this.runStateStore.upsertRun(runId, {
|
|
1001
|
-
queue_key: input.queueKey,
|
|
1002
|
-
conversation_key: input.sessionKey,
|
|
1003
|
-
project_alias: input.projectAlias,
|
|
1004
|
-
chat_id: input.chatId,
|
|
1005
|
-
actor_id: input.actorId,
|
|
1006
|
-
session_id: currentSession?.thread_id,
|
|
1007
|
-
project_root: projectRoot,
|
|
1008
|
-
pid: activeRun.pid,
|
|
1009
|
-
prompt_excerpt: truncateExcerpt(input.prompt),
|
|
1010
|
-
status,
|
|
1011
|
-
status_detail: undefined,
|
|
1012
|
-
error: message,
|
|
1013
|
-
});
|
|
1014
|
-
await this.auditLog.append({
|
|
1015
|
-
type: cancelled ? 'codex.run.cancelled' : 'codex.run.failed',
|
|
1016
|
-
run_id: runId,
|
|
1017
|
-
chat_id: input.chatId,
|
|
1018
|
-
actor_id: input.actorId,
|
|
1019
|
-
project_alias: input.projectAlias,
|
|
1020
|
-
conversation_key: input.sessionKey,
|
|
1021
|
-
error: message,
|
|
1022
|
-
});
|
|
1023
|
-
await this.appendProjectAuditEvent(input.projectAlias, input.project, {
|
|
1024
|
-
type: cancelled ? 'codex.run.cancelled' : 'codex.run.failed',
|
|
1025
|
-
run_id: runId,
|
|
1026
|
-
chat_id: input.chatId,
|
|
1027
|
-
actor_id: input.actorId,
|
|
1028
|
-
error: message,
|
|
1029
|
-
});
|
|
1030
|
-
// Direction 5: Record trust failure (only for actual failures, not cancellations)
|
|
1031
|
-
if (!cancelled) {
|
|
1032
|
-
try {
|
|
1033
|
-
const trustState = await this.trustStore.getOrCreate(input.projectAlias);
|
|
1034
|
-
const updated = recordRunOutcome(trustState, false, DEFAULT_TRUST_POLICY);
|
|
1035
|
-
await this.trustStore.update(input.projectAlias, updated);
|
|
1036
|
-
}
|
|
1037
|
-
catch { /* trust tracking is best-effort */ }
|
|
1038
|
-
// Notify project chats about the failure
|
|
1039
|
-
await this.notifyProjectChats(input.projectAlias, `❌ 运行失败 [${input.projectAlias}]\n${message.slice(0, 200)}`);
|
|
1040
|
-
// Proactive alerts on failure
|
|
1041
|
-
try {
|
|
1042
|
-
const failedRunState = await this.runStateStore.getRun(runId);
|
|
1043
|
-
if (failedRunState) {
|
|
1044
|
-
await this.checkAndSendAlerts(failedRunState);
|
|
1045
|
-
}
|
|
1046
|
-
}
|
|
1047
|
-
catch { /* alerts are best-effort */ }
|
|
1048
|
-
}
|
|
1049
|
-
if (cancelled) {
|
|
1050
|
-
this.logger.warn({
|
|
1051
|
-
runId,
|
|
1052
|
-
queueKey: input.queueKey,
|
|
1053
|
-
sessionKey: input.sessionKey,
|
|
1054
|
-
projectAlias: input.projectAlias,
|
|
1055
|
-
durationMs: Date.now() - startedAt,
|
|
1056
|
-
}, 'Codex run cancelled');
|
|
1057
|
-
}
|
|
1058
|
-
else {
|
|
1059
|
-
this.logger.error({
|
|
1060
|
-
error,
|
|
1061
|
-
runId,
|
|
1062
|
-
queueKey: input.queueKey,
|
|
1063
|
-
sessionKey: input.sessionKey,
|
|
1064
|
-
projectAlias: input.projectAlias,
|
|
1065
|
-
durationMs: Date.now() - startedAt,
|
|
1066
|
-
}, 'Codex run failed');
|
|
1067
|
-
}
|
|
1068
|
-
await this.sendOrUpdateRunOutcome({
|
|
1069
|
-
input,
|
|
1070
|
-
runId,
|
|
1071
|
-
title: cancelled ? '运行已取消' : '执行失败',
|
|
1072
|
-
body: cancelled ? '当前运行已取消。' : ['执行失败。', '', friendlyErrorMessage(message)].join('\n'),
|
|
1073
|
-
runStatus: cancelled ? 'cancelled' : 'failure',
|
|
1074
|
-
runPhase: cancelled ? '已取消' : '失败',
|
|
1075
|
-
cardSummary: truncateForFeishuCard(cancelled ? '当前运行已取消。' : friendlyErrorMessage(message)),
|
|
1076
|
-
});
|
|
1077
|
-
}
|
|
1078
|
-
finally {
|
|
1079
|
-
this.activeRuns.delete(input.queueKey);
|
|
1080
|
-
this.runReplyTargets.delete(runId);
|
|
1081
|
-
}
|
|
488
|
+
return executePromptImpl(this, input);
|
|
1082
489
|
}
|
|
1083
490
|
async handleProjectCommand(context, selectionKey, alias, followupPrompt) {
|
|
1084
491
|
if (!alias) {
|
|
@@ -1095,7 +502,7 @@ export class FeiqueService {
|
|
|
1095
502
|
await this.sendTextReply(context.chat_id, `当前 chat_id 无权切换到项目 ${alias}。至少需要 ${describeMinimumRole('viewer')} 权限。`, context.message_id, context.text);
|
|
1096
503
|
return;
|
|
1097
504
|
}
|
|
1098
|
-
|
|
505
|
+
this.requireProject(alias); // throws if missing — validates the alias before switching
|
|
1099
506
|
const switched = await switchSharedProjectBinding(this.config, this.sessionStore, this.codexSessionIndex, {
|
|
1100
507
|
chatId: context.chat_id,
|
|
1101
508
|
actorId: context.actor_id,
|
|
@@ -1126,7 +533,7 @@ export class FeiqueService {
|
|
|
1126
533
|
await this.handleReadOnlyFollowupCommand(context, selectionKey, followupCommand, followupPrompt);
|
|
1127
534
|
return;
|
|
1128
535
|
}
|
|
1129
|
-
await this.handlePromptMessage(context, selectionKey, followupPrompt
|
|
536
|
+
await this.handlePromptMessage(context, selectionKey, followupPrompt);
|
|
1130
537
|
return;
|
|
1131
538
|
}
|
|
1132
539
|
await this.sendTextReply(context.chat_id, switched.text, context.message_id, context.text);
|
|
@@ -1175,13 +582,13 @@ export class FeiqueService {
|
|
|
1175
582
|
await this.handleAdminCommand(context, selectionKey, command);
|
|
1176
583
|
return;
|
|
1177
584
|
case 'prompt':
|
|
1178
|
-
await this.handlePromptMessage(context, selectionKey, followupPrompt
|
|
585
|
+
await this.handlePromptMessage(context, selectionKey, followupPrompt);
|
|
1179
586
|
return;
|
|
1180
587
|
default:
|
|
1181
|
-
await this.handlePromptMessage(context, selectionKey, followupPrompt
|
|
588
|
+
await this.handlePromptMessage(context, selectionKey, followupPrompt);
|
|
1182
589
|
}
|
|
1183
590
|
}
|
|
1184
|
-
async handlePromptMessage(context, selectionKey, rawPrompt
|
|
591
|
+
async handlePromptMessage(context, selectionKey, rawPrompt) {
|
|
1185
592
|
const prompt = normalizeIncomingText(rawPrompt) || (context.attachments.length > 0 ? '请结合这条飞书消息附带的多媒体信息继续处理。' : '');
|
|
1186
593
|
if (!prompt) {
|
|
1187
594
|
return;
|
|
@@ -1372,7 +779,6 @@ export class FeiqueService {
|
|
|
1372
779
|
}
|
|
1373
780
|
async handleSessionCommand(context, selectionKey, action, threadId) {
|
|
1374
781
|
const projectContext = await this.resolveProjectContext(context, selectionKey);
|
|
1375
|
-
const sessions = await this.sessionStore.listProjectSessions(projectContext.sessionKey, projectContext.projectAlias);
|
|
1376
782
|
const activeSessionId = (await this.sessionStore.getConversation(projectContext.sessionKey))?.projects[projectContext.projectAlias]?.thread_id;
|
|
1377
783
|
switch (action) {
|
|
1378
784
|
case 'list': {
|
|
@@ -1441,215 +847,39 @@ export class FeiqueService {
|
|
|
1441
847
|
// ── Direction 2: Knowledge Loop ──
|
|
1442
848
|
async handleLearnCommand(context, selectionKey, value) {
|
|
1443
849
|
const projectContext = await this.resolveProjectContext(context, selectionKey);
|
|
1444
|
-
|
|
1445
|
-
await this.memoryStore.saveProjectMemory({
|
|
1446
|
-
project_alias: input.project_alias,
|
|
1447
|
-
title: input.title,
|
|
1448
|
-
content: input.content,
|
|
1449
|
-
tags: input.tags,
|
|
1450
|
-
source: input.source,
|
|
1451
|
-
created_by: context.actor_id,
|
|
1452
|
-
});
|
|
1453
|
-
await this.auditLog.append({
|
|
1454
|
-
type: 'collaboration.knowledge.learned',
|
|
1455
|
-
project_alias: input.project_alias,
|
|
1456
|
-
actor_id: context.actor_id,
|
|
1457
|
-
title: input.title,
|
|
1458
|
-
});
|
|
1459
|
-
await this.sendTextReply(context.chat_id, `💡 团队知识已记录: "${input.title}"\n项目: ${input.project_alias}`, context.message_id, context.text);
|
|
850
|
+
return handleLearnCommandImpl(this, context, projectContext, value);
|
|
1460
851
|
}
|
|
1461
852
|
async handleRecallCommand(context, selectionKey, query) {
|
|
1462
853
|
const projectContext = await this.resolveProjectContext(context, selectionKey);
|
|
1463
|
-
|
|
1464
|
-
const text = formatRecallResults(memories, query);
|
|
1465
|
-
await this.sendTextReply(context.chat_id, text, context.message_id, context.text);
|
|
854
|
+
return handleRecallCommandImpl(this, context, projectContext, query);
|
|
1466
855
|
}
|
|
1467
|
-
// ── Direction 3: Handoff & Review ──
|
|
1468
856
|
async handleHandoffCommand(context, selectionKey, summary) {
|
|
1469
857
|
const projectContext = await this.resolveProjectContext(context, selectionKey);
|
|
1470
|
-
|
|
1471
|
-
const projectState = conversation?.projects[projectContext.projectAlias];
|
|
1472
|
-
const record = createHandoff({
|
|
1473
|
-
from_actor_id: context.actor_id ?? 'unknown',
|
|
1474
|
-
from_actor_name: context.actor_name,
|
|
1475
|
-
project_alias: projectContext.projectAlias,
|
|
1476
|
-
conversation_key: projectContext.sessionKey,
|
|
1477
|
-
thread_id: projectState?.active_thread_id ?? projectState?.thread_id,
|
|
1478
|
-
summary: summary ?? '会话交接',
|
|
1479
|
-
last_prompt: projectState?.last_prompt,
|
|
1480
|
-
last_response_excerpt: projectState?.last_response_excerpt,
|
|
1481
|
-
});
|
|
1482
|
-
await this.handoffStore.addHandoff(record);
|
|
1483
|
-
await this.auditLog.append({
|
|
1484
|
-
type: 'collaboration.handoff.created',
|
|
1485
|
-
handoff_id: record.id,
|
|
1486
|
-
from_actor_id: record.from_actor_id,
|
|
1487
|
-
project_alias: record.project_alias,
|
|
1488
|
-
});
|
|
1489
|
-
await this.sendTextReply(context.chat_id, formatHandoff(record), context.message_id, context.text);
|
|
858
|
+
return handleHandoffCommandImpl(this, context, projectContext, summary);
|
|
1490
859
|
}
|
|
1491
860
|
async handlePickupCommand(context, selectionKey, id) {
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
: await this.handoffStore.getPendingHandoffForActor(context.actor_id ?? '', undefined);
|
|
1495
|
-
if (id) {
|
|
1496
|
-
handoff = await this.handoffStore.getPendingHandoff();
|
|
1497
|
-
if (handoff && !handoff.id.startsWith(id)) {
|
|
1498
|
-
handoff = null;
|
|
1499
|
-
}
|
|
1500
|
-
}
|
|
1501
|
-
if (!handoff || handoff.status !== 'pending') {
|
|
1502
|
-
await this.sendTextReply(context.chat_id, '没有找到待接手的交接任务。', context.message_id, context.text);
|
|
1503
|
-
return;
|
|
1504
|
-
}
|
|
1505
|
-
const accepted = acceptHandoff(handoff, context.actor_id ?? 'unknown');
|
|
1506
|
-
await this.handoffStore.updateHandoff(handoff.id, {
|
|
1507
|
-
status: 'accepted',
|
|
1508
|
-
accepted_at: accepted.accepted_at,
|
|
1509
|
-
accepted_by: accepted.accepted_by,
|
|
1510
|
-
});
|
|
1511
|
-
// Adopt the session if there's a thread_id
|
|
1512
|
-
if (handoff.thread_id) {
|
|
1513
|
-
const projectContext = await this.resolveProjectContext(context, selectionKey);
|
|
1514
|
-
await this.sessionStore.setActiveProjectSession(projectContext.sessionKey, handoff.project_alias, handoff.thread_id);
|
|
1515
|
-
}
|
|
1516
|
-
await this.auditLog.append({
|
|
1517
|
-
type: 'collaboration.handoff.accepted',
|
|
1518
|
-
handoff_id: handoff.id,
|
|
1519
|
-
accepted_by: context.actor_id,
|
|
1520
|
-
project_alias: handoff.project_alias,
|
|
1521
|
-
});
|
|
1522
|
-
await this.sendTextReply(context.chat_id, `✅ 已接手 ${handoff.from_actor_name ?? handoff.from_actor_id} 的交接任务 [${handoff.project_alias}]\n摘要: ${handoff.summary}`, context.message_id, context.text);
|
|
861
|
+
const projectContext = await this.resolveProjectContext(context, selectionKey);
|
|
862
|
+
return handlePickupCommandImpl(this, context, projectContext, id);
|
|
1523
863
|
}
|
|
1524
864
|
async handleReviewCommand(context, selectionKey) {
|
|
1525
865
|
const projectContext = await this.resolveProjectContext(context, selectionKey);
|
|
1526
|
-
|
|
1527
|
-
const latestRun = runs.find((r) => r.project_alias === projectContext.projectAlias && (r.status === 'success' || r.status === 'failure'));
|
|
1528
|
-
if (!latestRun) {
|
|
1529
|
-
await this.sendTextReply(context.chat_id, '没有找到最近的运行结果可供评审。', context.message_id, context.text);
|
|
1530
|
-
return;
|
|
1531
|
-
}
|
|
1532
|
-
const review = createReview({
|
|
1533
|
-
run_id: latestRun.run_id,
|
|
1534
|
-
project_alias: projectContext.projectAlias,
|
|
1535
|
-
chat_id: context.chat_id,
|
|
1536
|
-
actor_id: context.actor_id ?? 'unknown',
|
|
1537
|
-
content_excerpt: latestRun.prompt_excerpt,
|
|
1538
|
-
});
|
|
1539
|
-
await this.handoffStore.addReview(review);
|
|
1540
|
-
await this.auditLog.append({
|
|
1541
|
-
type: 'collaboration.review.created',
|
|
1542
|
-
review_id: review.id,
|
|
1543
|
-
run_id: review.run_id,
|
|
1544
|
-
project_alias: review.project_alias,
|
|
1545
|
-
});
|
|
1546
|
-
await this.sendTextReply(context.chat_id, formatReview(review), context.message_id, context.text);
|
|
866
|
+
return handleReviewCommandImpl(this, context, projectContext);
|
|
1547
867
|
}
|
|
1548
868
|
async handleApproveCommand(context, comment) {
|
|
1549
|
-
|
|
1550
|
-
if (!pending) {
|
|
1551
|
-
await this.sendTextReply(context.chat_id, '当前没有待评审的内容。', context.message_id, context.text);
|
|
1552
|
-
return;
|
|
1553
|
-
}
|
|
1554
|
-
const resolved = resolveReview(pending, 'approved', context.actor_id ?? 'unknown', comment);
|
|
1555
|
-
await this.handoffStore.updateReview(pending.id, {
|
|
1556
|
-
status: 'approved',
|
|
1557
|
-
reviewer_id: resolved.reviewer_id,
|
|
1558
|
-
review_comment: resolved.review_comment,
|
|
1559
|
-
resolved_at: resolved.resolved_at,
|
|
1560
|
-
});
|
|
1561
|
-
await this.auditLog.append({
|
|
1562
|
-
type: 'collaboration.review.approved',
|
|
1563
|
-
review_id: pending.id,
|
|
1564
|
-
reviewer_id: context.actor_id,
|
|
1565
|
-
});
|
|
1566
|
-
await this.sendTextReply(context.chat_id, formatReviewResult(resolved), context.message_id, context.text);
|
|
869
|
+
return handleApproveCommandImpl(this, context, comment);
|
|
1567
870
|
}
|
|
1568
871
|
async handleRejectCommand(context, reason) {
|
|
1569
|
-
|
|
1570
|
-
if (!pending) {
|
|
1571
|
-
await this.sendTextReply(context.chat_id, '当前没有待评审的内容。', context.message_id, context.text);
|
|
1572
|
-
return;
|
|
1573
|
-
}
|
|
1574
|
-
const resolved = resolveReview(pending, 'rejected', context.actor_id ?? 'unknown', reason);
|
|
1575
|
-
await this.handoffStore.updateReview(pending.id, {
|
|
1576
|
-
status: 'rejected',
|
|
1577
|
-
reviewer_id: resolved.reviewer_id,
|
|
1578
|
-
review_comment: resolved.review_comment,
|
|
1579
|
-
resolved_at: resolved.resolved_at,
|
|
1580
|
-
});
|
|
1581
|
-
await this.auditLog.append({
|
|
1582
|
-
type: 'collaboration.review.rejected',
|
|
1583
|
-
review_id: pending.id,
|
|
1584
|
-
reviewer_id: context.actor_id,
|
|
1585
|
-
reason,
|
|
1586
|
-
});
|
|
1587
|
-
await this.sendTextReply(context.chat_id, formatReviewResult(resolved), context.message_id, context.text);
|
|
872
|
+
return handleRejectCommandImpl(this, context, reason);
|
|
1588
873
|
}
|
|
1589
|
-
// ── Direction 4: Insights ──
|
|
1590
874
|
async handleInsightsCommand(context) {
|
|
1591
|
-
|
|
1592
|
-
const auditEvents = await this.auditLog.tail(500);
|
|
1593
|
-
const insights = analyzeTeamHealth(runs, auditEvents);
|
|
1594
|
-
const text = formatInsightsReport(insights);
|
|
1595
|
-
await this.sendTextReply(context.chat_id, text, context.message_id, context.text);
|
|
875
|
+
return handleInsightsCommandImpl(this, context);
|
|
1596
876
|
}
|
|
1597
|
-
// ── Direction 5: Trust ──
|
|
1598
877
|
async handleTrustCommand(context, selectionKey, action, level) {
|
|
1599
878
|
const projectContext = await this.resolveProjectContext(context, selectionKey);
|
|
1600
|
-
|
|
1601
|
-
const TRUST_ORDER = ['observe', 'suggest', 'execute', 'autonomous'];
|
|
1602
|
-
const validLevels = [...TRUST_ORDER];
|
|
1603
|
-
const state = await this.trustStore.getOrCreate(projectContext.projectAlias);
|
|
1604
|
-
// Handle relative promote/demote from natural language
|
|
1605
|
-
let resolvedLevel = level;
|
|
1606
|
-
if (level === '_promote') {
|
|
1607
|
-
const idx = TRUST_ORDER.indexOf(state.current_level);
|
|
1608
|
-
if (idx >= TRUST_ORDER.length - 1) {
|
|
1609
|
-
await this.sendTextReply(context.chat_id, `已经是最高信任等级 (${state.current_level}),无法继续提升。`, context.message_id, context.text);
|
|
1610
|
-
return;
|
|
1611
|
-
}
|
|
1612
|
-
resolvedLevel = TRUST_ORDER[idx + 1];
|
|
1613
|
-
}
|
|
1614
|
-
else if (level === '_demote') {
|
|
1615
|
-
const idx = TRUST_ORDER.indexOf(state.current_level);
|
|
1616
|
-
if (idx <= 0) {
|
|
1617
|
-
await this.sendTextReply(context.chat_id, `已经是最低信任等级 (${state.current_level}),无法继续降低。`, context.message_id, context.text);
|
|
1618
|
-
return;
|
|
1619
|
-
}
|
|
1620
|
-
resolvedLevel = TRUST_ORDER[idx - 1];
|
|
1621
|
-
}
|
|
1622
|
-
if (!validLevels.includes(resolvedLevel)) {
|
|
1623
|
-
await this.sendTextReply(context.chat_id, `无效的信任等级。有效值: ${validLevels.join(', ')}`, context.message_id, context.text);
|
|
1624
|
-
return;
|
|
1625
|
-
}
|
|
1626
|
-
state.current_level = resolvedLevel;
|
|
1627
|
-
state.last_evaluated_at = new Date().toISOString();
|
|
1628
|
-
await this.trustStore.update(projectContext.projectAlias, state);
|
|
1629
|
-
this.metrics?.recordTrustLevel(projectContext.projectAlias, resolvedLevel);
|
|
1630
|
-
await this.auditLog.append({
|
|
1631
|
-
type: 'collaboration.trust.set',
|
|
1632
|
-
project_alias: projectContext.projectAlias,
|
|
1633
|
-
actor_id: context.actor_id,
|
|
1634
|
-
level,
|
|
1635
|
-
});
|
|
1636
|
-
await this.sendTextReply(context.chat_id, `🛡️ 项目 ${projectContext.projectAlias} 的信任等级已设置为: ${level}`, context.message_id, context.text);
|
|
1637
|
-
return;
|
|
1638
|
-
}
|
|
1639
|
-
const state = await this.trustStore.getOrCreate(projectContext.projectAlias);
|
|
1640
|
-
await this.sendTextReply(context.chat_id, formatTrustState(state), context.message_id, context.text);
|
|
879
|
+
return handleTrustCommandImpl(this, context, projectContext, action, level);
|
|
1641
880
|
}
|
|
1642
|
-
// ── Team Digest ──
|
|
1643
881
|
async handleDigestCommand(context) {
|
|
1644
|
-
|
|
1645
|
-
const runs = await this.runStateStore.listRuns();
|
|
1646
|
-
const memories = this.config.service.memory_enabled
|
|
1647
|
-
? await this.memoryStore.listRecentMemories({ scope: 'project', project_alias: '' }, 100)
|
|
1648
|
-
: [];
|
|
1649
|
-
const auditEvents = await this.auditLog.tail(500);
|
|
1650
|
-
const digest = buildTeamDigest(runs, memories, auditEvents, period);
|
|
1651
|
-
const text = formatTeamDigest(digest);
|
|
1652
|
-
await this.sendTextReply(context.chat_id, text, context.message_id, context.text);
|
|
882
|
+
return handleDigestCommandImpl(this, context);
|
|
1653
883
|
}
|
|
1654
884
|
// ── Proactive Alerts ──
|
|
1655
885
|
async checkAndSendAlerts(completedRun) {
|
|
@@ -1681,26 +911,12 @@ export class FeiqueService {
|
|
|
1681
911
|
}
|
|
1682
912
|
// ── Knowledge Gap Detection ──
|
|
1683
913
|
async handleGapsCommand(context) {
|
|
1684
|
-
|
|
1685
|
-
const memories = this.config.service.memory_enabled
|
|
1686
|
-
? await this.memoryStore.listRecentMemories({ scope: 'project', project_alias: '' }, 200)
|
|
1687
|
-
: [];
|
|
1688
|
-
const gaps = detectKnowledgeGaps(runs, memories);
|
|
1689
|
-
const text = formatKnowledgeGaps(gaps);
|
|
1690
|
-
await this.sendTextReply(context.chat_id, text, context.message_id, context.text);
|
|
914
|
+
return handleGapsCommandImpl(this, context);
|
|
1691
915
|
}
|
|
1692
916
|
// ── Direction 6: Timeline ──
|
|
1693
917
|
async handleTimelineCommand(context, selectionKey, projectArg) {
|
|
1694
918
|
const projectContext = await this.resolveProjectContext(context, selectionKey);
|
|
1695
|
-
|
|
1696
|
-
const runs = await this.runStateStore.listRuns();
|
|
1697
|
-
const auditEvents = await this.auditLog.tail(200);
|
|
1698
|
-
const memories = this.config.service.memory_enabled
|
|
1699
|
-
? await this.memoryStore.listRecentMemories({ scope: 'project', project_alias: projectAlias }, 20)
|
|
1700
|
-
: [];
|
|
1701
|
-
const timeline = buildProjectTimeline(runs, memories, auditEvents, projectAlias, 20);
|
|
1702
|
-
const text = formatTimeline(timeline);
|
|
1703
|
-
await this.sendTextReply(context.chat_id, text, context.message_id, context.text);
|
|
919
|
+
return handleTimelineCommandImpl(this, context, projectContext, projectArg);
|
|
1704
920
|
}
|
|
1705
921
|
async handleAdminCommand(context, selectionKey, command) {
|
|
1706
922
|
const runtimeConfigPath = this.runtimeControl?.configPath;
|
|
@@ -1758,37 +974,42 @@ export class FeiqueService {
|
|
|
1758
974
|
await this.sendTextReply(context.chat_id, this.buildProjectsAdminText(globalAdmin || serviceObserver ? undefined : new Set(projectOperatorAliases)), context.message_id, context.text);
|
|
1759
975
|
return;
|
|
1760
976
|
}
|
|
1761
|
-
if (command.action === 'add' || command.action === 'create') {
|
|
977
|
+
if (command.action === 'add' || command.action === 'create' || command.action === 'setup') {
|
|
1762
978
|
if (!(globalAdmin || globalConfigAdmin)) {
|
|
1763
979
|
await this.sendTextReply(context.chat_id, '当前 chat_id 无权动态接入项目。', context.message_id, context.text);
|
|
1764
980
|
return;
|
|
1765
981
|
}
|
|
1766
982
|
if (!command.alias || !command.value) {
|
|
1767
|
-
await this.sendTextReply(context.chat_id, command.action === '
|
|
983
|
+
await this.sendTextReply(context.chat_id, command.action === 'setup'
|
|
984
|
+
? '用法: /admin project setup <alias> <root>\n一键创建项目并将当前群设为 operator。'
|
|
985
|
+
: command.action === 'create' ? '用法: /admin project create <alias> <root>' : '用法: /admin project add <alias> <root>', context.message_id, context.text);
|
|
1768
986
|
return;
|
|
1769
987
|
}
|
|
1770
|
-
|
|
988
|
+
const isCreate = command.action === 'create' || command.action === 'setup';
|
|
989
|
+
if (isCreate && this.config.projects[command.alias]) {
|
|
1771
990
|
await this.sendTextReply(context.chat_id, `项目已存在: ${command.alias}`, context.message_id, context.text);
|
|
1772
991
|
return;
|
|
1773
992
|
}
|
|
1774
993
|
const resolvedRoot = path.resolve(expandHomePath(command.value));
|
|
1775
994
|
const snapshot = await this.snapshotConfigForAdminMutation(context, `project.${command.action}`, `${command.alias} -> ${resolvedRoot}`);
|
|
1776
|
-
if (
|
|
995
|
+
if (isCreate) {
|
|
1777
996
|
await createProjectAlias({ configPath: runtimeConfigPath, alias: command.alias, root: command.value });
|
|
1778
997
|
}
|
|
1779
998
|
else {
|
|
1780
999
|
await bindProjectAlias({ configPath: runtimeConfigPath, alias: command.alias, root: command.value });
|
|
1781
1000
|
}
|
|
1001
|
+
// For setup: auto-add current chat as operator + viewer
|
|
1002
|
+
const autoOperator = command.action === 'setup' ? [context.chat_id] : [];
|
|
1782
1003
|
this.config.projects[command.alias] = {
|
|
1783
1004
|
root: resolvedRoot,
|
|
1784
1005
|
session_scope: 'chat',
|
|
1785
1006
|
mention_required: true,
|
|
1786
1007
|
knowledge_paths: [],
|
|
1787
1008
|
wiki_space_ids: [],
|
|
1788
|
-
viewer_chat_ids: [],
|
|
1789
|
-
operator_chat_ids: [],
|
|
1009
|
+
viewer_chat_ids: [...autoOperator],
|
|
1010
|
+
operator_chat_ids: [...autoOperator],
|
|
1790
1011
|
admin_chat_ids: [],
|
|
1791
|
-
notification_chat_ids: [],
|
|
1012
|
+
notification_chat_ids: [...autoOperator],
|
|
1792
1013
|
session_operator_chat_ids: [],
|
|
1793
1014
|
run_operator_chat_ids: [],
|
|
1794
1015
|
config_admin_chat_ids: [],
|
|
@@ -1798,7 +1019,27 @@ export class FeiqueService {
|
|
|
1798
1019
|
chat_rate_limit_window_seconds: 60,
|
|
1799
1020
|
chat_rate_limit_max_runs: 20,
|
|
1800
1021
|
};
|
|
1801
|
-
|
|
1022
|
+
// Persist the auto-added chat_ids to config file for setup
|
|
1023
|
+
if (command.action === 'setup') {
|
|
1024
|
+
await updateProjectConfig(runtimeConfigPath, command.alias, {
|
|
1025
|
+
viewer_chat_ids: autoOperator,
|
|
1026
|
+
operator_chat_ids: autoOperator,
|
|
1027
|
+
notification_chat_ids: autoOperator,
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
const replyLines = [
|
|
1031
|
+
`${isCreate ? '已创建并接入项目' : '已接入项目'}: ${command.alias}`,
|
|
1032
|
+
`根目录: ${resolvedRoot}`,
|
|
1033
|
+
];
|
|
1034
|
+
if (command.action === 'setup') {
|
|
1035
|
+
replyLines.push(`已自动将当前群设为 operator + viewer + notification`);
|
|
1036
|
+
replyLines.push('');
|
|
1037
|
+
replyLines.push('可在其他群执行以下命令添加权限:');
|
|
1038
|
+
replyLines.push(`/admin project set ${command.alias} operator_chat_ids +<chat_id>`);
|
|
1039
|
+
replyLines.push('');
|
|
1040
|
+
replyLines.push(`切换到此项目: /project ${command.alias}`);
|
|
1041
|
+
}
|
|
1042
|
+
await this.sendTextReply(context.chat_id, replyLines.join('\n'), context.message_id, context.text);
|
|
1802
1043
|
await this.appendAdminAudit({
|
|
1803
1044
|
type: `admin.project.${command.action}`,
|
|
1804
1045
|
chat_id: context.chat_id,
|
|
@@ -1806,8 +1047,9 @@ export class FeiqueService {
|
|
|
1806
1047
|
project_alias: command.alias,
|
|
1807
1048
|
root: resolvedRoot,
|
|
1808
1049
|
snapshot_id: snapshot.id,
|
|
1050
|
+
auto_operator: command.action === 'setup' ? context.chat_id : undefined,
|
|
1809
1051
|
});
|
|
1810
|
-
this.logger.info({ alias: command.alias, root: resolvedRoot, actorId: context.actor_id, created: command.action === '
|
|
1052
|
+
this.logger.info({ alias: command.alias, root: resolvedRoot, actorId: context.actor_id, created: isCreate, setup: command.action === 'setup' }, command.action === 'setup' ? 'Project setup by Feishu admin' : isCreate ? 'Project created by Feishu admin' : 'Project added by Feishu admin');
|
|
1811
1053
|
return;
|
|
1812
1054
|
}
|
|
1813
1055
|
if (command.action === 'remove') {
|
|
@@ -1845,9 +1087,9 @@ export class FeiqueService {
|
|
|
1845
1087
|
await this.sendTextReply(context.chat_id, `当前 chat_id 无权修改项目 ${command.alias}。`, context.message_id, context.text);
|
|
1846
1088
|
return;
|
|
1847
1089
|
}
|
|
1848
|
-
const patch = this.
|
|
1090
|
+
const patch = parseProjectPatchImpl(this.config, command.field, command.value, command.alias);
|
|
1849
1091
|
if (!patch) {
|
|
1850
|
-
await this.sendTextReply(context.chat_id, '支持字段: root, profile, sandbox, session_scope, mention_required, description, viewer_chat_ids, operator_chat_ids, admin_chat_ids, session_operator_chat_ids, run_operator_chat_ids, config_admin_chat_ids, download_dir, temp_dir, cache_dir, log_dir, run_priority, chat_rate_limit_window_seconds, chat_rate_limit_max_runs', context.message_id, context.text);
|
|
1092
|
+
await this.sendTextReply(context.chat_id, '支持字段: root, profile, sandbox, session_scope, mention_required, description, viewer_chat_ids, operator_chat_ids, admin_chat_ids, notification_chat_ids, session_operator_chat_ids, run_operator_chat_ids, config_admin_chat_ids, download_dir, temp_dir, cache_dir, log_dir, run_priority, chat_rate_limit_window_seconds, chat_rate_limit_max_runs\n\n列表字段支持增量操作: +value 添加, -value 移除', context.message_id, context.text);
|
|
1851
1093
|
return;
|
|
1852
1094
|
}
|
|
1853
1095
|
const snapshot = await this.snapshotConfigForAdminMutation(context, 'project.set', `${command.alias}.${command.field}=${command.value}`);
|
|
@@ -1946,309 +1188,17 @@ export class FeiqueService {
|
|
|
1946
1188
|
await this.sendTextReply(context.chat_id, `项目 ${projectContext.projectAlias} 已切换到 ${label} 后端。\n下一条消息将使用 ${label} 执行。`, context.message_id, context.text);
|
|
1947
1189
|
}
|
|
1948
1190
|
async handleMemoryCommand(context, selectionKey, action, scope, value, filters) {
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
const activeThreadId = conversation?.projects[projectContext.projectAlias]?.thread_id;
|
|
1961
|
-
const groupMemoryAvailable = this.config.service.memory_group_enabled && context.chat_type === 'group';
|
|
1962
|
-
if (action === 'status') {
|
|
1963
|
-
if (scope === 'group') {
|
|
1964
|
-
const target = this.resolveMemoryTarget(context, 'group');
|
|
1965
|
-
const [count, pinnedCount] = await Promise.all([
|
|
1966
|
-
this.memoryStore.countGroupMemories(projectContext.projectAlias, target.chatId),
|
|
1967
|
-
this.memoryStore.countPinnedGroupMemories(projectContext.projectAlias, target.chatId),
|
|
1968
|
-
]);
|
|
1969
|
-
await this.sendTextReply(context.chat_id, [
|
|
1970
|
-
`项目: ${projectContext.projectAlias}`,
|
|
1971
|
-
`群共享记忆数: ${count}`,
|
|
1972
|
-
`Pinned 群共享记忆数: ${pinnedCount}`,
|
|
1973
|
-
`群 chat_id: ${target.chatId}`,
|
|
1974
|
-
].join('\n'), context.message_id, context.text);
|
|
1975
|
-
return;
|
|
1976
|
-
}
|
|
1977
|
-
const [count, pinnedCount, threadSummary, groupCount, groupPinnedCount] = await Promise.all([
|
|
1978
|
-
this.memoryStore.countProjectMemories(projectContext.projectAlias),
|
|
1979
|
-
this.memoryStore.countPinnedProjectMemories(projectContext.projectAlias),
|
|
1980
|
-
activeThreadId ? this.memoryStore.getThreadSummary(projectContext.sessionKey, projectContext.projectAlias, activeThreadId) : Promise.resolve(null),
|
|
1981
|
-
groupMemoryAvailable ? this.memoryStore.countGroupMemories(projectContext.projectAlias, context.chat_id) : Promise.resolve(0),
|
|
1982
|
-
groupMemoryAvailable ? this.memoryStore.countPinnedGroupMemories(projectContext.projectAlias, context.chat_id) : Promise.resolve(0),
|
|
1983
|
-
]);
|
|
1984
|
-
await this.sendTextReply(context.chat_id, [
|
|
1985
|
-
`项目: ${projectContext.projectAlias}`,
|
|
1986
|
-
`项目记忆数: ${count}`,
|
|
1987
|
-
`Pinned 项目记忆数: ${pinnedCount}`,
|
|
1988
|
-
...(groupMemoryAvailable ? [`群共享记忆数: ${groupCount}`, `Pinned 群共享记忆数: ${groupPinnedCount}`] : []),
|
|
1989
|
-
`当前会话: ${activeThreadId ?? '未开始'}`,
|
|
1990
|
-
'',
|
|
1991
|
-
threadSummary?.summary ?? '当前没有 thread summary。',
|
|
1992
|
-
].join('\n'), context.message_id, context.text);
|
|
1993
|
-
return;
|
|
1994
|
-
}
|
|
1995
|
-
if (action === 'stats') {
|
|
1996
|
-
const target = this.resolveMemoryTarget(context, scope);
|
|
1997
|
-
const stats = await this.memoryStore.getMemoryStats({
|
|
1998
|
-
scope: target.scope,
|
|
1999
|
-
project_alias: projectContext.projectAlias,
|
|
2000
|
-
chat_id: target.chatId,
|
|
2001
|
-
});
|
|
2002
|
-
await this.sendTextReply(context.chat_id, [
|
|
2003
|
-
`项目: ${projectContext.projectAlias}`,
|
|
2004
|
-
`${target.label}统计:`,
|
|
2005
|
-
`active_count: ${stats.active_count}`,
|
|
2006
|
-
`expired_count: ${stats.expired_count}`,
|
|
2007
|
-
`pinned_count: ${stats.pinned_count}`,
|
|
2008
|
-
`archived_count: ${stats.archived_count}`,
|
|
2009
|
-
`latest_accessed_at: ${stats.latest_accessed_at ?? '-'}`,
|
|
2010
|
-
`latest_updated_at: ${stats.latest_updated_at ?? '-'}`,
|
|
2011
|
-
`latest_archived_at: ${stats.latest_archived_at ?? '-'}`,
|
|
2012
|
-
].join('\n'), context.message_id, context.text);
|
|
2013
|
-
return;
|
|
2014
|
-
}
|
|
2015
|
-
if (action === 'recent') {
|
|
2016
|
-
const target = this.resolveMemoryTarget(context, scope);
|
|
2017
|
-
const recent = await this.memoryStore.listRecentMemories({ scope: target.scope, project_alias: projectContext.projectAlias, chat_id: target.chatId }, this.config.service.memory_recent_limit, filters);
|
|
2018
|
-
if (recent.length === 0) {
|
|
2019
|
-
await this.sendTextReply(context.chat_id, [
|
|
2020
|
-
`项目: ${projectContext.projectAlias}`,
|
|
2021
|
-
`当前没有可展示的${target.label}。`,
|
|
2022
|
-
...this.renderMemoryFilterLines(filters),
|
|
2023
|
-
].join('\n'), context.message_id, context.text);
|
|
2024
|
-
return;
|
|
2025
|
-
}
|
|
2026
|
-
await this.sendTextReply(context.chat_id, [
|
|
2027
|
-
`项目: ${projectContext.projectAlias}`,
|
|
2028
|
-
`最近${target.label}:`,
|
|
2029
|
-
...this.renderMemoryFilterLines(filters),
|
|
2030
|
-
'',
|
|
2031
|
-
...recent.map((item, index) => [
|
|
2032
|
-
`${index + 1}. ${item.title}${item.pinned ? ' [pinned]' : ''}`,
|
|
2033
|
-
` id: ${item.id}`,
|
|
2034
|
-
` source: ${item.source}`,
|
|
2035
|
-
...(item.created_by ? [` created_by: ${item.created_by}`] : []),
|
|
2036
|
-
...(item.tags.length > 0 ? [` tags: ${item.tags.join(', ')}`] : []),
|
|
2037
|
-
` updated_at: ${item.updated_at}`,
|
|
2038
|
-
...(item.last_accessed_at ? [` last_accessed_at: ${item.last_accessed_at}`] : []),
|
|
2039
|
-
...(item.expires_at ? [` expires_at: ${item.expires_at}`] : []),
|
|
2040
|
-
` ${truncateExcerpt(item.content, 180)}`,
|
|
2041
|
-
].join('\n')),
|
|
2042
|
-
].join('\n'), context.message_id, context.text);
|
|
2043
|
-
return;
|
|
2044
|
-
}
|
|
2045
|
-
if (action === 'search') {
|
|
2046
|
-
if (!value?.trim()) {
|
|
2047
|
-
await this.sendTextReply(context.chat_id, '用法: /memory search [--tag <tag>] [--source <source>] [--created-by <actor_id>] <query>', context.message_id, context.text);
|
|
2048
|
-
return;
|
|
2049
|
-
}
|
|
2050
|
-
const target = this.resolveMemoryTarget(context, scope);
|
|
2051
|
-
const hits = await this.memoryStore.searchMemories({ scope: target.scope, project_alias: projectContext.projectAlias, chat_id: target.chatId }, value, this.config.service.memory_search_limit, filters);
|
|
2052
|
-
await this.auditLog.append({
|
|
2053
|
-
type: 'memory.search',
|
|
2054
|
-
chat_id: context.chat_id,
|
|
2055
|
-
actor_id: context.actor_id,
|
|
2056
|
-
project_alias: projectContext.projectAlias,
|
|
2057
|
-
scope: target.scope,
|
|
2058
|
-
query: value,
|
|
2059
|
-
result_count: hits.length,
|
|
2060
|
-
});
|
|
2061
|
-
if (hits.length === 0) {
|
|
2062
|
-
await this.sendTextReply(context.chat_id, [`项目: ${projectContext.projectAlias}`, `${target.label}搜索: ${value}`, ...this.renderMemoryFilterLines(filters), '未找到匹配记忆。'].join('\n'), context.message_id, context.text);
|
|
2063
|
-
return;
|
|
2064
|
-
}
|
|
2065
|
-
await this.sendTextReply(context.chat_id, [
|
|
2066
|
-
`项目: ${projectContext.projectAlias}`,
|
|
2067
|
-
`${target.label}搜索: ${value}`,
|
|
2068
|
-
...this.renderMemoryFilterLines(filters),
|
|
2069
|
-
'',
|
|
2070
|
-
...hits.map((hit, index) => [
|
|
2071
|
-
`${index + 1}. ${hit.title}${hit.pinned ? ' [pinned]' : ''}`,
|
|
2072
|
-
` id: ${hit.id}`,
|
|
2073
|
-
` source: ${hit.source}`,
|
|
2074
|
-
...(hit.created_by ? [` created_by: ${hit.created_by}`] : []),
|
|
2075
|
-
...(hit.tags.length > 0 ? [` tags: ${hit.tags.join(', ')}`] : []),
|
|
2076
|
-
...(hit.last_accessed_at ? [` last_accessed_at: ${hit.last_accessed_at}`] : []),
|
|
2077
|
-
` ${truncateExcerpt(hit.content, 180)}`,
|
|
2078
|
-
].join('\n')),
|
|
2079
|
-
].join('\n'), context.message_id, context.text);
|
|
2080
|
-
return;
|
|
2081
|
-
}
|
|
2082
|
-
if (action === 'pin' || action === 'unpin' || action === 'forget' || action === 'restore') {
|
|
2083
|
-
if (!value?.trim()) {
|
|
2084
|
-
const usage = action === 'forget'
|
|
2085
|
-
? '用法: /memory forget <id> 或 /memory forget group <id>'
|
|
2086
|
-
: action === 'restore'
|
|
2087
|
-
? '用法: /memory restore <id> 或 /memory restore group <id>'
|
|
2088
|
-
: `用法: /memory ${action} <id> 或 /memory ${action} group <id>`;
|
|
2089
|
-
await this.sendTextReply(context.chat_id, usage, context.message_id, context.text);
|
|
2090
|
-
return;
|
|
2091
|
-
}
|
|
2092
|
-
const target = this.resolveMemoryTarget(context, scope);
|
|
2093
|
-
const selector = { scope: target.scope, project_alias: projectContext.projectAlias, chat_id: target.chatId };
|
|
2094
|
-
if (action === 'forget' && value === 'all-expired') {
|
|
2095
|
-
const cleaned = await this.memoryStore.cleanupExpiredMemories(selector);
|
|
2096
|
-
await this.auditLog.append({
|
|
2097
|
-
type: 'memory.archive.expired',
|
|
2098
|
-
chat_id: context.chat_id,
|
|
2099
|
-
actor_id: context.actor_id,
|
|
2100
|
-
project_alias: projectContext.projectAlias,
|
|
2101
|
-
scope: target.scope,
|
|
2102
|
-
count: cleaned,
|
|
2103
|
-
});
|
|
2104
|
-
await this.sendTextReply(context.chat_id, `${target.label}已归档过期项: ${cleaned}`, context.message_id, context.text);
|
|
2105
|
-
return;
|
|
2106
|
-
}
|
|
2107
|
-
const existing = await this.memoryStore.getMemoryById(selector, value, { includeArchived: action === 'restore', includeExpired: action === 'restore' });
|
|
2108
|
-
if (!existing) {
|
|
2109
|
-
await this.sendTextReply(context.chat_id, `未找到可更新的${target.label} ID: ${value}`, context.message_id, context.text);
|
|
2110
|
-
return;
|
|
2111
|
-
}
|
|
2112
|
-
if (action === 'forget') {
|
|
2113
|
-
const archived = await this.memoryStore.archiveMemory(selector, value, { archived_by: context.actor_id, reason: 'manual' });
|
|
2114
|
-
if (archived) {
|
|
2115
|
-
await this.auditLog.append({
|
|
2116
|
-
type: 'memory.archive',
|
|
2117
|
-
chat_id: context.chat_id,
|
|
2118
|
-
actor_id: context.actor_id,
|
|
2119
|
-
project_alias: projectContext.projectAlias,
|
|
2120
|
-
scope: target.scope,
|
|
2121
|
-
memory_id: value,
|
|
2122
|
-
});
|
|
2123
|
-
}
|
|
2124
|
-
await this.sendTextReply(context.chat_id, archived
|
|
2125
|
-
? [`${target.label}已归档: ${archived.title}`, `memory_id: ${archived.id}`, `可用 /memory restore${target.scope === 'group' ? ' group' : ''} ${archived.id} 恢复`].join('\n')
|
|
2126
|
-
: `未找到可归档的${target.label} ID: ${value}`, context.message_id, context.text);
|
|
2127
|
-
return;
|
|
2128
|
-
}
|
|
2129
|
-
if (action === 'restore') {
|
|
2130
|
-
const restored = await this.memoryStore.restoreMemory(selector, value, context.actor_id);
|
|
2131
|
-
if (restored) {
|
|
2132
|
-
await this.auditLog.append({
|
|
2133
|
-
type: 'memory.restore',
|
|
2134
|
-
chat_id: context.chat_id,
|
|
2135
|
-
actor_id: context.actor_id,
|
|
2136
|
-
project_alias: projectContext.projectAlias,
|
|
2137
|
-
scope: target.scope,
|
|
2138
|
-
memory_id: value,
|
|
2139
|
-
});
|
|
2140
|
-
}
|
|
2141
|
-
await this.sendTextReply(context.chat_id, restored ? `${target.label}已恢复: ${restored.title}\nmemory_id: ${restored.id}` : `未找到可恢复的${target.label} ID: ${value}`, context.message_id, context.text);
|
|
2142
|
-
return;
|
|
2143
|
-
}
|
|
2144
|
-
const pinned = action === 'pin';
|
|
2145
|
-
let agedOutMemoryTitle;
|
|
2146
|
-
let agedOutMemoryId;
|
|
2147
|
-
if (pinned && !existing.pinned) {
|
|
2148
|
-
const pinnedCount = await this.memoryStore.countPinnedMemories(selector);
|
|
2149
|
-
if (pinnedCount >= this.config.service.memory_max_pinned_per_scope) {
|
|
2150
|
-
if (this.config.service.memory_pin_overflow_strategy === 'age-out') {
|
|
2151
|
-
const oldest = await this.memoryStore.getOldestPinnedMemory(selector, this.config.service.memory_pin_age_basis);
|
|
2152
|
-
if (oldest && oldest.id !== existing.id) {
|
|
2153
|
-
await this.memoryStore.setMemoryPinned(selector, oldest.id, false);
|
|
2154
|
-
agedOutMemoryTitle = oldest.title;
|
|
2155
|
-
agedOutMemoryId = oldest.id;
|
|
2156
|
-
await this.auditLog.append({
|
|
2157
|
-
type: 'memory.pin.aged_out',
|
|
2158
|
-
chat_id: context.chat_id,
|
|
2159
|
-
actor_id: context.actor_id,
|
|
2160
|
-
project_alias: projectContext.projectAlias,
|
|
2161
|
-
scope: target.scope,
|
|
2162
|
-
memory_id: oldest.id,
|
|
2163
|
-
replaced_by: existing.id,
|
|
2164
|
-
});
|
|
2165
|
-
}
|
|
2166
|
-
else {
|
|
2167
|
-
await this.sendTextReply(context.chat_id, `${target.label}置顶数量已达上限 (${this.config.service.memory_max_pinned_per_scope})。请先取消置顶旧记录。`, context.message_id, context.text);
|
|
2168
|
-
return;
|
|
2169
|
-
}
|
|
2170
|
-
}
|
|
2171
|
-
else {
|
|
2172
|
-
await this.sendTextReply(context.chat_id, `${target.label}置顶数量已达上限 (${this.config.service.memory_max_pinned_per_scope})。请先取消置顶旧记录。`, context.message_id, context.text);
|
|
2173
|
-
return;
|
|
2174
|
-
}
|
|
2175
|
-
}
|
|
2176
|
-
}
|
|
2177
|
-
const updated = await this.memoryStore.setMemoryPinned(selector, value, pinned);
|
|
2178
|
-
if (!updated) {
|
|
2179
|
-
await this.sendTextReply(context.chat_id, `未找到可更新的${target.label} ID: ${value}`, context.message_id, context.text);
|
|
2180
|
-
return;
|
|
2181
|
-
}
|
|
2182
|
-
await this.auditLog.append({
|
|
2183
|
-
type: pinned ? 'memory.pin' : 'memory.unpin',
|
|
2184
|
-
chat_id: context.chat_id,
|
|
2185
|
-
actor_id: context.actor_id,
|
|
2186
|
-
project_alias: projectContext.projectAlias,
|
|
2187
|
-
scope: target.scope,
|
|
2188
|
-
memory_id: value,
|
|
2189
|
-
});
|
|
2190
|
-
await this.sendTextReply(context.chat_id, [
|
|
2191
|
-
`${target.label}${pinned ? '已置顶' : '已取消置顶'}: ${updated.title}`,
|
|
2192
|
-
`memory_id: ${updated.id}`,
|
|
2193
|
-
...(agedOutMemoryId ? [`已自动老化旧置顶: ${agedOutMemoryTitle} (${agedOutMemoryId})`] : []),
|
|
2194
|
-
].join('\n'), context.message_id, context.text);
|
|
2195
|
-
return;
|
|
2196
|
-
}
|
|
2197
|
-
const content = value?.trim();
|
|
2198
|
-
if (!content) {
|
|
2199
|
-
await this.sendTextReply(context.chat_id, '用法: /memory save <text> 或 /memory save group <text>', context.message_id, context.text);
|
|
2200
|
-
return;
|
|
2201
|
-
}
|
|
2202
|
-
const target = this.resolveMemoryTarget(context, scope);
|
|
2203
|
-
const title = truncateExcerpt(content.replace(/\s+/g, ' ').trim(), 60);
|
|
2204
|
-
const expiresAt = this.buildMemoryExpiresAt();
|
|
2205
|
-
const saved = await this.memoryStore.saveMemory({
|
|
2206
|
-
scope: target.scope,
|
|
2207
|
-
project_alias: projectContext.projectAlias,
|
|
2208
|
-
chat_id: target.chatId,
|
|
2209
|
-
title,
|
|
2210
|
-
content,
|
|
2211
|
-
tags: filters?.tag ? [filters.tag] : undefined,
|
|
2212
|
-
source: filters?.source ?? 'manual',
|
|
2213
|
-
created_by: context.actor_id,
|
|
2214
|
-
expires_at: expiresAt,
|
|
2215
|
-
});
|
|
2216
|
-
await this.auditLog.append({
|
|
2217
|
-
type: 'memory.save',
|
|
2218
|
-
chat_id: context.chat_id,
|
|
2219
|
-
actor_id: context.actor_id,
|
|
2220
|
-
project_alias: projectContext.projectAlias,
|
|
2221
|
-
scope: target.scope,
|
|
2222
|
-
memory_id: saved.id,
|
|
2223
|
-
title: saved.title,
|
|
2224
|
-
});
|
|
2225
|
-
await this.sendTextReply(context.chat_id, [
|
|
2226
|
-
`项目: ${projectContext.projectAlias}`,
|
|
2227
|
-
`已保存${target.label}: ${saved.title}`,
|
|
2228
|
-
`memory_id: ${saved.id}`,
|
|
2229
|
-
...(saved.expires_at ? [`expires_at: ${saved.expires_at}`] : []),
|
|
2230
|
-
].join('\n'), context.message_id, context.text);
|
|
2231
|
-
}
|
|
2232
|
-
catch (error) {
|
|
2233
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2234
|
-
await this.sendTextReply(context.chat_id, message, context.message_id, context.text);
|
|
2235
|
-
}
|
|
2236
|
-
}
|
|
2237
|
-
renderMemoryFilterLines(filters) {
|
|
2238
|
-
return [
|
|
2239
|
-
...(filters?.tag ? [`tag: ${filters.tag}`] : []),
|
|
2240
|
-
...(filters?.source ? [`source: ${filters.source}`] : []),
|
|
2241
|
-
...(filters?.created_by ? [`created_by: ${filters.created_by}`] : []),
|
|
2242
|
-
];
|
|
2243
|
-
}
|
|
2244
|
-
async handleKnowledgeCommand(context, selectionKey, action, query) {
|
|
2245
|
-
const projectContext = await this.resolveProjectContext(context, selectionKey);
|
|
2246
|
-
const roots = await resolveKnowledgeRoots(projectContext.project);
|
|
2247
|
-
if (action === 'status') {
|
|
2248
|
-
const message = roots.length
|
|
2249
|
-
? [`项目: ${projectContext.projectAlias}`, '知识库目录:', ...roots.map((root) => `- ${root}`)].join('\n')
|
|
2250
|
-
: [`项目: ${projectContext.projectAlias}`, '当前没有可用知识库目录。', '可在项目配置中设置 knowledge_paths,或在项目根下提供 docs/README。'].join('\n');
|
|
2251
|
-
await this.sendTextReply(context.chat_id, message, context.message_id, context.text);
|
|
1191
|
+
const projectContext = await this.resolveProjectContext(context, selectionKey);
|
|
1192
|
+
return handleMemoryCommandImpl(this, context, projectContext, action, scope, value, filters);
|
|
1193
|
+
}
|
|
1194
|
+
async handleKnowledgeCommand(context, selectionKey, action, query) {
|
|
1195
|
+
const projectContext = await this.resolveProjectContext(context, selectionKey);
|
|
1196
|
+
const roots = await resolveKnowledgeRoots(projectContext.project);
|
|
1197
|
+
if (action === 'status') {
|
|
1198
|
+
const message = roots.length
|
|
1199
|
+
? [`项目: ${projectContext.projectAlias}`, '知识库目录:', ...roots.map((root) => `- ${root}`)].join('\n')
|
|
1200
|
+
: [`项目: ${projectContext.projectAlias}`, '当前没有可用知识库目录。', '可在项目配置中设置 knowledge_paths,或在项目根下提供 docs/README。'].join('\n');
|
|
1201
|
+
await this.sendTextReply(context.chat_id, message, context.message_id, context.text);
|
|
2252
1202
|
return;
|
|
2253
1203
|
}
|
|
2254
1204
|
if (!query) {
|
|
@@ -2282,449 +1232,19 @@ export class FeiqueService {
|
|
|
2282
1232
|
}
|
|
2283
1233
|
async handleDocCommand(context, selectionKey, action, value, extra) {
|
|
2284
1234
|
const projectContext = await this.resolveProjectContext(context, selectionKey);
|
|
2285
|
-
|
|
2286
|
-
await this.sendTextReply(context.chat_id, `当前 chat_id 无权${action === 'create' ? '写入' : '读取'}项目 ${projectContext.projectAlias} 关联的飞书文档。`, context.message_id, context.text);
|
|
2287
|
-
return;
|
|
2288
|
-
}
|
|
2289
|
-
const docClient = new FeishuDocClient(this.feishuClient.createSdkClient());
|
|
2290
|
-
if (action === 'create') {
|
|
2291
|
-
const title = value?.trim();
|
|
2292
|
-
if (!title) {
|
|
2293
|
-
await this.sendTextReply(context.chat_id, '用法: /doc create <title>', context.message_id, context.text);
|
|
2294
|
-
return;
|
|
2295
|
-
}
|
|
2296
|
-
const created = await docClient.create(title, extra?.trim());
|
|
2297
|
-
await this.auditLog.append({
|
|
2298
|
-
type: 'doc.create',
|
|
2299
|
-
chat_id: context.chat_id,
|
|
2300
|
-
actor_id: context.actor_id,
|
|
2301
|
-
project_alias: projectContext.projectAlias,
|
|
2302
|
-
document_id: created.documentId,
|
|
2303
|
-
title: created.title,
|
|
2304
|
-
});
|
|
2305
|
-
await this.sendTextReply(context.chat_id, ['已创建飞书文档', `标题: ${created.title ?? title}`, `文档: ${created.documentId}`, ...(created.url ? [`链接: ${created.url}`] : [])].join('\n'), context.message_id, context.text);
|
|
2306
|
-
return;
|
|
2307
|
-
}
|
|
2308
|
-
if (!value) {
|
|
2309
|
-
await this.sendTextReply(context.chat_id, '用法: /doc read <url|token>', context.message_id, context.text);
|
|
2310
|
-
return;
|
|
2311
|
-
}
|
|
2312
|
-
const document = await docClient.read(value);
|
|
2313
|
-
await this.auditLog.append({
|
|
2314
|
-
type: 'doc.read',
|
|
2315
|
-
chat_id: context.chat_id,
|
|
2316
|
-
actor_id: context.actor_id,
|
|
2317
|
-
project_alias: projectContext.projectAlias,
|
|
2318
|
-
document_id: document.documentId,
|
|
2319
|
-
title: document.title,
|
|
2320
|
-
});
|
|
2321
|
-
await this.sendTextReply(context.chat_id, [
|
|
2322
|
-
`标题: ${document.title ?? '未知'}`,
|
|
2323
|
-
`文档: ${document.documentId}`,
|
|
2324
|
-
...(document.url ? [`链接: ${document.url}`] : []),
|
|
2325
|
-
'',
|
|
2326
|
-
truncateExcerpt(document.content?.replace(/\s+/g, ' ').trim() ?? '文档暂无可读取的纯文本内容。', 1200),
|
|
2327
|
-
].join('\n'), context.message_id, context.text);
|
|
1235
|
+
return handleDocCommandImpl(this, context, projectContext, action, value, extra);
|
|
2328
1236
|
}
|
|
2329
1237
|
async handleTaskCommand(context, selectionKey, action, value) {
|
|
2330
1238
|
const projectContext = await this.resolveProjectContext(context, selectionKey);
|
|
2331
|
-
|
|
2332
|
-
await this.sendTextReply(context.chat_id, `当前 chat_id 无权${action === 'create' || action === 'complete' ? '写入' : '查看'}项目 ${projectContext.projectAlias} 关联的飞书任务。`, context.message_id, context.text);
|
|
2333
|
-
return;
|
|
2334
|
-
}
|
|
2335
|
-
const taskClient = new FeishuTaskClient(this.feishuClient.createSdkClient());
|
|
2336
|
-
if (action === 'list') {
|
|
2337
|
-
const limit = clampListLimit(value, 10, 20);
|
|
2338
|
-
const tasks = await taskClient.list(limit);
|
|
2339
|
-
const lines = tasks.length > 0
|
|
2340
|
-
? tasks.map((task, index) => `${index + 1}. ${task.summary ?? '(无标题)'}\n guid: ${task.guid}\n status: ${task.status ?? 'unknown'}${task.url ? `\n url: ${task.url}` : ''}`)
|
|
2341
|
-
: ['当前没有可见任务。'];
|
|
2342
|
-
await this.sendTextReply(context.chat_id, ['最近任务', '', ...lines].join('\n'), context.message_id, context.text);
|
|
2343
|
-
return;
|
|
2344
|
-
}
|
|
2345
|
-
if (action === 'get') {
|
|
2346
|
-
if (!value) {
|
|
2347
|
-
await this.sendTextReply(context.chat_id, '用法: /task get <task_guid>', context.message_id, context.text);
|
|
2348
|
-
return;
|
|
2349
|
-
}
|
|
2350
|
-
const task = await taskClient.get(value);
|
|
2351
|
-
await this.auditLog.append({
|
|
2352
|
-
type: 'task.read',
|
|
2353
|
-
chat_id: context.chat_id,
|
|
2354
|
-
actor_id: context.actor_id,
|
|
2355
|
-
project_alias: projectContext.projectAlias,
|
|
2356
|
-
task_guid: task.guid,
|
|
2357
|
-
});
|
|
2358
|
-
await this.sendTextReply(context.chat_id, [
|
|
2359
|
-
`任务: ${task.summary ?? '(无标题)'}`,
|
|
2360
|
-
`guid: ${task.guid}`,
|
|
2361
|
-
`status: ${task.status ?? 'unknown'}`,
|
|
2362
|
-
...(task.url ? [`链接: ${task.url}`] : []),
|
|
2363
|
-
'',
|
|
2364
|
-
task.description ?? '无描述',
|
|
2365
|
-
].join('\n'), context.message_id, context.text);
|
|
2366
|
-
return;
|
|
2367
|
-
}
|
|
2368
|
-
if (action === 'create') {
|
|
2369
|
-
const summary = value?.trim();
|
|
2370
|
-
if (!summary) {
|
|
2371
|
-
await this.sendTextReply(context.chat_id, '用法: /task create <summary>', context.message_id, context.text);
|
|
2372
|
-
return;
|
|
2373
|
-
}
|
|
2374
|
-
const task = await taskClient.create(summary);
|
|
2375
|
-
await this.auditLog.append({
|
|
2376
|
-
type: 'task.create',
|
|
2377
|
-
chat_id: context.chat_id,
|
|
2378
|
-
actor_id: context.actor_id,
|
|
2379
|
-
project_alias: projectContext.projectAlias,
|
|
2380
|
-
task_guid: task.guid,
|
|
2381
|
-
summary: task.summary,
|
|
2382
|
-
});
|
|
2383
|
-
await this.sendTextReply(context.chat_id, [`已创建任务`, `标题: ${task.summary ?? summary}`, `guid: ${task.guid}`, ...(task.url ? [`链接: ${task.url}`] : [])].join('\n'), context.message_id, context.text);
|
|
2384
|
-
return;
|
|
2385
|
-
}
|
|
2386
|
-
if (!value) {
|
|
2387
|
-
await this.sendTextReply(context.chat_id, '用法: /task complete <task_guid>', context.message_id, context.text);
|
|
2388
|
-
return;
|
|
2389
|
-
}
|
|
2390
|
-
const task = await taskClient.complete(value);
|
|
2391
|
-
await this.auditLog.append({
|
|
2392
|
-
type: 'task.complete',
|
|
2393
|
-
chat_id: context.chat_id,
|
|
2394
|
-
actor_id: context.actor_id,
|
|
2395
|
-
project_alias: projectContext.projectAlias,
|
|
2396
|
-
task_guid: task.guid,
|
|
2397
|
-
summary: task.summary,
|
|
2398
|
-
});
|
|
2399
|
-
await this.sendTextReply(context.chat_id, [`已完成任务`, `标题: ${task.summary ?? '(无标题)'}`, `guid: ${task.guid}`, `status: ${task.status ?? 'unknown'}`].join('\n'), context.message_id, context.text);
|
|
1239
|
+
return handleTaskCommandImpl(this, context, projectContext, action, value);
|
|
2400
1240
|
}
|
|
2401
1241
|
async handleBaseCommand(context, selectionKey, action, appToken, tableId, recordId, value) {
|
|
2402
1242
|
const projectContext = await this.resolveProjectContext(context, selectionKey);
|
|
2403
|
-
|
|
2404
|
-
await this.sendTextReply(context.chat_id, `当前 chat_id 无权${action === 'create' || action === 'update' ? '写入' : '查看'}项目 ${projectContext.projectAlias} 关联的多维表格。`, context.message_id, context.text);
|
|
2405
|
-
return;
|
|
2406
|
-
}
|
|
2407
|
-
const baseClient = new FeishuBaseClient(this.feishuClient.createSdkClient());
|
|
2408
|
-
if (action === 'tables') {
|
|
2409
|
-
if (!appToken) {
|
|
2410
|
-
await this.sendTextReply(context.chat_id, '用法: /base tables <app_token>', context.message_id, context.text);
|
|
2411
|
-
return;
|
|
2412
|
-
}
|
|
2413
|
-
const tables = await baseClient.listTables(appToken, 20);
|
|
2414
|
-
const lines = tables.length > 0
|
|
2415
|
-
? tables.map((table, index) => `${index + 1}. ${table.name ?? '(未命名表)'}\n table_id: ${table.tableId}${table.revision !== undefined ? `\n revision: ${table.revision}` : ''}`)
|
|
2416
|
-
: ['当前 Base 中没有可见数据表。'];
|
|
2417
|
-
await this.sendTextReply(context.chat_id, [`Base: ${appToken}`, '', ...lines].join('\n'), context.message_id, context.text);
|
|
2418
|
-
return;
|
|
2419
|
-
}
|
|
2420
|
-
if (action === 'records') {
|
|
2421
|
-
if (!appToken || !tableId) {
|
|
2422
|
-
await this.sendTextReply(context.chat_id, '用法: /base records <app_token> <table_id> [limit]', context.message_id, context.text);
|
|
2423
|
-
return;
|
|
2424
|
-
}
|
|
2425
|
-
const limit = clampListLimit(value, 10, 20);
|
|
2426
|
-
const records = await baseClient.listRecords(appToken, tableId, limit);
|
|
2427
|
-
const lines = records.length > 0
|
|
2428
|
-
? records.map((record, index) => `${index + 1}. ${record.recordId}\n fields: ${truncateExcerpt(JSON.stringify(record.fields), 240)}${record.recordUrl ? `\n url: ${record.recordUrl}` : ''}`)
|
|
2429
|
-
: ['当前数据表没有可见记录。'];
|
|
2430
|
-
await this.sendTextReply(context.chat_id, [`Base: ${appToken}`, `Table: ${tableId}`, '', ...lines].join('\n'), context.message_id, context.text);
|
|
2431
|
-
return;
|
|
2432
|
-
}
|
|
2433
|
-
if (action === 'create') {
|
|
2434
|
-
if (!appToken || !tableId || !value) {
|
|
2435
|
-
await this.sendTextReply(context.chat_id, '用法: /base create <app_token> <table_id> <json>', context.message_id, context.text);
|
|
2436
|
-
return;
|
|
2437
|
-
}
|
|
2438
|
-
const fields = parseJsonObject(value);
|
|
2439
|
-
const record = await baseClient.createRecord(appToken, tableId, fields);
|
|
2440
|
-
await this.auditLog.append({
|
|
2441
|
-
type: 'base.record.create',
|
|
2442
|
-
chat_id: context.chat_id,
|
|
2443
|
-
actor_id: context.actor_id,
|
|
2444
|
-
project_alias: projectContext.projectAlias,
|
|
2445
|
-
app_token: appToken,
|
|
2446
|
-
table_id: tableId,
|
|
2447
|
-
record_id: record.recordId,
|
|
2448
|
-
});
|
|
2449
|
-
await this.sendTextReply(context.chat_id, [`已创建 Base 记录`, `app: ${appToken}`, `table: ${tableId}`, `record: ${record.recordId}`, `fields: ${truncateExcerpt(JSON.stringify(record.fields), 240)}`].join('\n'), context.message_id, context.text);
|
|
2450
|
-
return;
|
|
2451
|
-
}
|
|
2452
|
-
if (!appToken || !tableId || !recordId || !value) {
|
|
2453
|
-
await this.sendTextReply(context.chat_id, '用法: /base update <app_token> <table_id> <record_id> <json>', context.message_id, context.text);
|
|
2454
|
-
return;
|
|
2455
|
-
}
|
|
2456
|
-
const fields = parseJsonObject(value);
|
|
2457
|
-
const record = await baseClient.updateRecord(appToken, tableId, recordId, fields);
|
|
2458
|
-
await this.auditLog.append({
|
|
2459
|
-
type: 'base.record.update',
|
|
2460
|
-
chat_id: context.chat_id,
|
|
2461
|
-
actor_id: context.actor_id,
|
|
2462
|
-
project_alias: projectContext.projectAlias,
|
|
2463
|
-
app_token: appToken,
|
|
2464
|
-
table_id: tableId,
|
|
2465
|
-
record_id: record.recordId,
|
|
2466
|
-
});
|
|
2467
|
-
await this.sendTextReply(context.chat_id, [`已更新 Base 记录`, `app: ${appToken}`, `table: ${tableId}`, `record: ${record.recordId}`, `fields: ${truncateExcerpt(JSON.stringify(record.fields), 240)}`].join('\n'), context.message_id, context.text);
|
|
1243
|
+
return handleBaseCommandImpl(this, context, projectContext, action, appToken, tableId, recordId, value);
|
|
2468
1244
|
}
|
|
2469
1245
|
async handleWikiCommand(context, selectionKey, action, value, extra, target, role) {
|
|
2470
1246
|
const projectContext = await this.resolveProjectContext(context, selectionKey);
|
|
2471
|
-
|
|
2472
|
-
if (action === 'spaces') {
|
|
2473
|
-
const spaces = await wikiClient.listSpaces(10);
|
|
2474
|
-
const lines = spaces.length > 0
|
|
2475
|
-
? spaces.map((space) => `- ${space.name} (${space.id})${space.description ? ` | ${space.description}` : ''}`)
|
|
2476
|
-
: ['当前应用可访问的知识空间为空。请确认机器人已被加入目标空间。'];
|
|
2477
|
-
await this.sendTextReply(context.chat_id, [`项目: ${projectContext.projectAlias}`, `配置过滤空间数: ${projectContext.project.wiki_space_ids.length}`, '', ...lines].join('\n'), context.message_id, context.text);
|
|
2478
|
-
return;
|
|
2479
|
-
}
|
|
2480
|
-
if (action === 'search') {
|
|
2481
|
-
if (!value) {
|
|
2482
|
-
await this.sendTextReply(context.chat_id, '用法: /wiki search <query>', context.message_id, context.text);
|
|
2483
|
-
return;
|
|
2484
|
-
}
|
|
2485
|
-
const hits = await wikiClient.search(value, projectContext.project.wiki_space_ids, 5);
|
|
2486
|
-
await this.auditLog.append({
|
|
2487
|
-
type: 'wiki.search',
|
|
2488
|
-
chat_id: context.chat_id,
|
|
2489
|
-
actor_id: context.actor_id,
|
|
2490
|
-
project_alias: projectContext.projectAlias,
|
|
2491
|
-
query: value,
|
|
2492
|
-
result_count: hits.length,
|
|
2493
|
-
});
|
|
2494
|
-
if (hits.length === 0) {
|
|
2495
|
-
await this.sendTextReply(context.chat_id, [`项目: ${projectContext.projectAlias}`, `飞书知识库搜索: ${value}`, '未找到匹配结果。', '', '提示: 确认机器人有目标空间访问权限,或在项目配置里设置 wiki_space_ids。'].join('\n'), context.message_id, context.text);
|
|
2496
|
-
return;
|
|
2497
|
-
}
|
|
2498
|
-
const lines = hits.map((hit, index) => [
|
|
2499
|
-
`${index + 1}. ${hit.title}`,
|
|
2500
|
-
` space: ${hit.spaceId}`,
|
|
2501
|
-
` token: ${hit.objToken}`,
|
|
2502
|
-
...(hit.url ? [` url: ${hit.url}`] : []),
|
|
2503
|
-
].join('\n'));
|
|
2504
|
-
await this.sendTextReply(context.chat_id, [`项目: ${projectContext.projectAlias}`, `飞书知识库搜索: ${value}`, '', ...lines].join('\n'), context.message_id, context.text);
|
|
2505
|
-
return;
|
|
2506
|
-
}
|
|
2507
|
-
if (action === 'members') {
|
|
2508
|
-
const spaceId = value?.trim() || projectContext.project.wiki_space_ids[0];
|
|
2509
|
-
if (!spaceId) {
|
|
2510
|
-
await this.sendTextReply(context.chat_id, '用法: /wiki members [space_id],或先在项目配置里设置默认 wiki_space_ids。', context.message_id, context.text);
|
|
2511
|
-
return;
|
|
2512
|
-
}
|
|
2513
|
-
const members = await wikiClient.listMembers(spaceId, 20);
|
|
2514
|
-
await this.auditLog.append({
|
|
2515
|
-
type: 'wiki.members',
|
|
2516
|
-
chat_id: context.chat_id,
|
|
2517
|
-
actor_id: context.actor_id,
|
|
2518
|
-
project_alias: projectContext.projectAlias,
|
|
2519
|
-
space_id: spaceId,
|
|
2520
|
-
result_count: members.length,
|
|
2521
|
-
});
|
|
2522
|
-
const lines = members.length > 0
|
|
2523
|
-
? members.map((member, index) => `${index + 1}. ${member.memberId}\n member_type: ${member.memberType}\n role: ${member.memberRole}${member.type ? `\n type: ${member.type}` : ''}`)
|
|
2524
|
-
: ['当前知识空间没有可见成员,或机器人没有成员读取权限。'];
|
|
2525
|
-
await this.sendTextReply(context.chat_id, [`项目: ${projectContext.projectAlias}`, `知识空间成员: ${spaceId}`, '', ...lines].join('\n'), context.message_id, context.text);
|
|
2526
|
-
return;
|
|
2527
|
-
}
|
|
2528
|
-
if (action === 'create') {
|
|
2529
|
-
const defaultSpaceId = projectContext.project.wiki_space_ids[0];
|
|
2530
|
-
const spaceId = extra ?? defaultSpaceId;
|
|
2531
|
-
const title = value?.trim();
|
|
2532
|
-
if (!title) {
|
|
2533
|
-
await this.sendTextReply(context.chat_id, '用法: /wiki create <title> 或 /wiki create <space_id> <title>', context.message_id, context.text);
|
|
2534
|
-
return;
|
|
2535
|
-
}
|
|
2536
|
-
if (!spaceId) {
|
|
2537
|
-
await this.sendTextReply(context.chat_id, '当前项目未配置默认 wiki_space_ids,请使用 `/wiki create <space_id> <title>`。', context.message_id, context.text);
|
|
2538
|
-
return;
|
|
2539
|
-
}
|
|
2540
|
-
const created = await wikiClient.createDoc(spaceId, title);
|
|
2541
|
-
await this.auditLog.append({
|
|
2542
|
-
type: 'wiki.create',
|
|
2543
|
-
chat_id: context.chat_id,
|
|
2544
|
-
actor_id: context.actor_id,
|
|
2545
|
-
project_alias: projectContext.projectAlias,
|
|
2546
|
-
title,
|
|
2547
|
-
space_id: created.spaceId,
|
|
2548
|
-
obj_token: created.objToken,
|
|
2549
|
-
node_token: created.nodeToken,
|
|
2550
|
-
});
|
|
2551
|
-
await this.sendTextReply(context.chat_id, [
|
|
2552
|
-
`项目: ${projectContext.projectAlias}`,
|
|
2553
|
-
`已创建飞书文档: ${created.title ?? title}`,
|
|
2554
|
-
`空间: ${created.spaceId ?? spaceId}`,
|
|
2555
|
-
...(created.nodeToken ? [`节点: ${created.nodeToken}`] : []),
|
|
2556
|
-
...(created.objToken ? [`文档: ${created.objToken}`] : []),
|
|
2557
|
-
].join('\n'), context.message_id, context.text);
|
|
2558
|
-
return;
|
|
2559
|
-
}
|
|
2560
|
-
if (action === 'grant') {
|
|
2561
|
-
const spaceId = extra?.trim();
|
|
2562
|
-
const memberType = target?.trim();
|
|
2563
|
-
const memberId = value?.trim();
|
|
2564
|
-
const memberRole = role?.trim() || 'member';
|
|
2565
|
-
if (!spaceId || !memberType || !memberId) {
|
|
2566
|
-
await this.sendTextReply(context.chat_id, '用法: /wiki grant <space_id> <member_type> <member_id> [member|admin]', context.message_id, context.text);
|
|
2567
|
-
return;
|
|
2568
|
-
}
|
|
2569
|
-
const granted = await wikiClient.addMember(spaceId, memberType, memberId, memberRole);
|
|
2570
|
-
await this.auditLog.append({
|
|
2571
|
-
type: 'wiki.member.grant',
|
|
2572
|
-
chat_id: context.chat_id,
|
|
2573
|
-
actor_id: context.actor_id,
|
|
2574
|
-
project_alias: projectContext.projectAlias,
|
|
2575
|
-
space_id: spaceId,
|
|
2576
|
-
member_id: granted.memberId,
|
|
2577
|
-
member_type: granted.memberType,
|
|
2578
|
-
member_role: granted.memberRole,
|
|
2579
|
-
});
|
|
2580
|
-
await this.sendTextReply(context.chat_id, [
|
|
2581
|
-
`项目: ${projectContext.projectAlias}`,
|
|
2582
|
-
'已添加知识空间成员',
|
|
2583
|
-
`空间: ${spaceId}`,
|
|
2584
|
-
`member_type: ${granted.memberType}`,
|
|
2585
|
-
`member_id: ${granted.memberId}`,
|
|
2586
|
-
`role: ${granted.memberRole}`,
|
|
2587
|
-
].join('\n'), context.message_id, context.text);
|
|
2588
|
-
return;
|
|
2589
|
-
}
|
|
2590
|
-
if (action === 'rename') {
|
|
2591
|
-
const nodeToken = extra?.trim();
|
|
2592
|
-
const title = value?.trim();
|
|
2593
|
-
if (!nodeToken || !title) {
|
|
2594
|
-
await this.sendTextReply(context.chat_id, '用法: /wiki rename <node_token> <title>', context.message_id, context.text);
|
|
2595
|
-
return;
|
|
2596
|
-
}
|
|
2597
|
-
await wikiClient.renameNode(nodeToken, title, projectContext.project.wiki_space_ids[0]);
|
|
2598
|
-
await this.auditLog.append({
|
|
2599
|
-
type: 'wiki.rename',
|
|
2600
|
-
chat_id: context.chat_id,
|
|
2601
|
-
actor_id: context.actor_id,
|
|
2602
|
-
project_alias: projectContext.projectAlias,
|
|
2603
|
-
node_token: nodeToken,
|
|
2604
|
-
title,
|
|
2605
|
-
});
|
|
2606
|
-
await this.sendTextReply(context.chat_id, [`项目: ${projectContext.projectAlias}`, `已更新知识库节点标题`, `节点: ${nodeToken}`, `标题: ${title}`].join('\n'), context.message_id, context.text);
|
|
2607
|
-
return;
|
|
2608
|
-
}
|
|
2609
|
-
if (action === 'copy') {
|
|
2610
|
-
const nodeToken = value?.trim();
|
|
2611
|
-
const targetSpaceId = extra?.trim() || projectContext.project.wiki_space_ids[0];
|
|
2612
|
-
if (!nodeToken) {
|
|
2613
|
-
await this.sendTextReply(context.chat_id, '用法: /wiki copy <node_token> [target_space_id]', context.message_id, context.text);
|
|
2614
|
-
return;
|
|
2615
|
-
}
|
|
2616
|
-
if (!targetSpaceId) {
|
|
2617
|
-
await this.sendTextReply(context.chat_id, '当前项目未配置默认 wiki_space_ids,请显式传入 target_space_id。', context.message_id, context.text);
|
|
2618
|
-
return;
|
|
2619
|
-
}
|
|
2620
|
-
const copied = await wikiClient.copyNode(nodeToken, targetSpaceId);
|
|
2621
|
-
await this.auditLog.append({
|
|
2622
|
-
type: 'wiki.copy',
|
|
2623
|
-
chat_id: context.chat_id,
|
|
2624
|
-
actor_id: context.actor_id,
|
|
2625
|
-
project_alias: projectContext.projectAlias,
|
|
2626
|
-
node_token: nodeToken,
|
|
2627
|
-
target_space_id: copied.spaceId,
|
|
2628
|
-
obj_token: copied.objToken,
|
|
2629
|
-
});
|
|
2630
|
-
await this.sendTextReply(context.chat_id, [
|
|
2631
|
-
`项目: ${projectContext.projectAlias}`,
|
|
2632
|
-
`已复制知识库节点`,
|
|
2633
|
-
`源节点: ${nodeToken}`,
|
|
2634
|
-
`目标空间: ${copied.spaceId ?? targetSpaceId}`,
|
|
2635
|
-
...(copied.nodeToken ? [`新节点: ${copied.nodeToken}`] : []),
|
|
2636
|
-
...(copied.objToken ? [`对象: ${copied.objToken}`] : []),
|
|
2637
|
-
].join('\n'), context.message_id, context.text);
|
|
2638
|
-
return;
|
|
2639
|
-
}
|
|
2640
|
-
if (action === 'move') {
|
|
2641
|
-
const sourceSpaceId = extra?.trim();
|
|
2642
|
-
const nodeToken = value?.trim();
|
|
2643
|
-
const targetSpaceId = target?.trim() || projectContext.project.wiki_space_ids[0];
|
|
2644
|
-
if (!sourceSpaceId || !nodeToken) {
|
|
2645
|
-
await this.sendTextReply(context.chat_id, '用法: /wiki move <source_space_id> <node_token> [target_space_id]', context.message_id, context.text);
|
|
2646
|
-
return;
|
|
2647
|
-
}
|
|
2648
|
-
if (!targetSpaceId) {
|
|
2649
|
-
await this.sendTextReply(context.chat_id, '当前项目未配置默认 wiki_space_ids,请显式传入 target_space_id。', context.message_id, context.text);
|
|
2650
|
-
return;
|
|
2651
|
-
}
|
|
2652
|
-
const moved = await wikiClient.moveNode(sourceSpaceId, nodeToken, targetSpaceId);
|
|
2653
|
-
await this.auditLog.append({
|
|
2654
|
-
type: 'wiki.move',
|
|
2655
|
-
chat_id: context.chat_id,
|
|
2656
|
-
actor_id: context.actor_id,
|
|
2657
|
-
project_alias: projectContext.projectAlias,
|
|
2658
|
-
node_token: nodeToken,
|
|
2659
|
-
source_space_id: sourceSpaceId,
|
|
2660
|
-
target_space_id: moved.spaceId,
|
|
2661
|
-
obj_token: moved.objToken,
|
|
2662
|
-
});
|
|
2663
|
-
await this.sendTextReply(context.chat_id, [
|
|
2664
|
-
`项目: ${projectContext.projectAlias}`,
|
|
2665
|
-
`已移动知识库节点`,
|
|
2666
|
-
`源空间: ${sourceSpaceId}`,
|
|
2667
|
-
`源节点: ${nodeToken}`,
|
|
2668
|
-
`目标空间: ${moved.spaceId ?? targetSpaceId}`,
|
|
2669
|
-
...(moved.nodeToken ? [`当前节点: ${moved.nodeToken}`] : []),
|
|
2670
|
-
].join('\n'), context.message_id, context.text);
|
|
2671
|
-
return;
|
|
2672
|
-
}
|
|
2673
|
-
if (action === 'revoke') {
|
|
2674
|
-
const spaceId = extra?.trim();
|
|
2675
|
-
const memberType = target?.trim();
|
|
2676
|
-
const memberId = value?.trim();
|
|
2677
|
-
const memberRole = role?.trim() || 'member';
|
|
2678
|
-
if (!spaceId || !memberType || !memberId) {
|
|
2679
|
-
await this.sendTextReply(context.chat_id, '用法: /wiki revoke <space_id> <member_type> <member_id> [member|admin]', context.message_id, context.text);
|
|
2680
|
-
return;
|
|
2681
|
-
}
|
|
2682
|
-
const revoked = await wikiClient.removeMember(spaceId, memberType, memberId, memberRole);
|
|
2683
|
-
await this.auditLog.append({
|
|
2684
|
-
type: 'wiki.member.revoke',
|
|
2685
|
-
chat_id: context.chat_id,
|
|
2686
|
-
actor_id: context.actor_id,
|
|
2687
|
-
project_alias: projectContext.projectAlias,
|
|
2688
|
-
space_id: spaceId,
|
|
2689
|
-
member_id: revoked.memberId,
|
|
2690
|
-
member_type: revoked.memberType,
|
|
2691
|
-
member_role: revoked.memberRole,
|
|
2692
|
-
});
|
|
2693
|
-
await this.sendTextReply(context.chat_id, [
|
|
2694
|
-
`项目: ${projectContext.projectAlias}`,
|
|
2695
|
-
'已移除知识空间成员',
|
|
2696
|
-
`空间: ${spaceId}`,
|
|
2697
|
-
`member_type: ${revoked.memberType}`,
|
|
2698
|
-
`member_id: ${revoked.memberId}`,
|
|
2699
|
-
`role: ${revoked.memberRole}`,
|
|
2700
|
-
].join('\n'), context.message_id, context.text);
|
|
2701
|
-
return;
|
|
2702
|
-
}
|
|
2703
|
-
if (!value) {
|
|
2704
|
-
await this.sendTextReply(context.chat_id, '用法: /wiki read <url|token>', context.message_id, context.text);
|
|
2705
|
-
return;
|
|
2706
|
-
}
|
|
2707
|
-
const result = await wikiClient.read(value);
|
|
2708
|
-
await this.auditLog.append({
|
|
2709
|
-
type: 'wiki.read',
|
|
2710
|
-
chat_id: context.chat_id,
|
|
2711
|
-
actor_id: context.actor_id,
|
|
2712
|
-
project_alias: projectContext.projectAlias,
|
|
2713
|
-
target: value,
|
|
2714
|
-
obj_type: result.objType,
|
|
2715
|
-
obj_token: result.objToken,
|
|
2716
|
-
});
|
|
2717
|
-
const summary = result.content ? truncateExcerpt(result.content.replace(/\s+/g, ' ').trim(), 1200) : '当前对象不是 docx 文档,暂不支持直接拉取纯文本内容。';
|
|
2718
|
-
await this.sendTextReply(context.chat_id, [
|
|
2719
|
-
`项目: ${projectContext.projectAlias}`,
|
|
2720
|
-
`标题: ${result.title ?? '未知'}`,
|
|
2721
|
-
`类型: ${result.objType ?? '未知'}`,
|
|
2722
|
-
...(result.spaceId ? [`空间: ${result.spaceId}`] : []),
|
|
2723
|
-
...(result.objToken ? [`对象: ${result.objToken}`] : []),
|
|
2724
|
-
...(result.url ? [`链接: ${result.url}`] : []),
|
|
2725
|
-
'',
|
|
2726
|
-
summary,
|
|
2727
|
-
].join('\n'), context.message_id, context.text);
|
|
1247
|
+
return handleWikiCommandImpl(this, context, projectContext, action, value, extra, target, role);
|
|
2728
1248
|
}
|
|
2729
1249
|
async buildProjectsText(selectionKey, chatId) {
|
|
2730
1250
|
const selected = await this.resolveProjectAlias(selectionKey);
|
|
@@ -2787,11 +1307,11 @@ export class FeiqueService {
|
|
|
2787
1307
|
const session = conversation?.projects[projectAlias];
|
|
2788
1308
|
const sessionCount = Object.keys(session?.sessions ?? {}).length;
|
|
2789
1309
|
const isExecutableRun = activeRun ? isExecutionRunStatus(activeRun.status) : false;
|
|
2790
|
-
const includeActions = this.
|
|
1310
|
+
const includeActions = supportsInteractiveCardActions(this.config);
|
|
2791
1311
|
const actionChatId = conversation?.chat_id ?? activeRun?.chat_id ?? fallbackChatId;
|
|
2792
1312
|
return buildStatusCard({
|
|
2793
1313
|
title: '当前会话状态',
|
|
2794
|
-
summary:
|
|
1314
|
+
summary: buildRunStatusSummary(session?.last_response_excerpt, activeRun),
|
|
2795
1315
|
projectAlias,
|
|
2796
1316
|
sessionId: session?.thread_id,
|
|
2797
1317
|
runStatus: activeRun?.status,
|
|
@@ -2962,32 +1482,6 @@ export class FeiqueService {
|
|
|
2962
1482
|
shouldRequireMention(project) {
|
|
2963
1483
|
return project.mention_required || this.config.security.require_group_mentions;
|
|
2964
1484
|
}
|
|
2965
|
-
resolveMemoryTarget(context, requestedScope) {
|
|
2966
|
-
if (requestedScope === 'group') {
|
|
2967
|
-
if (!this.config.service.memory_group_enabled) {
|
|
2968
|
-
throw new Error('群共享记忆未启用。请在配置中设置 `service.memory_group_enabled = true`。');
|
|
2969
|
-
}
|
|
2970
|
-
if (context.chat_type !== 'group') {
|
|
2971
|
-
throw new Error('群共享记忆只能在群聊中使用。');
|
|
2972
|
-
}
|
|
2973
|
-
return {
|
|
2974
|
-
scope: 'group',
|
|
2975
|
-
chatId: context.chat_id,
|
|
2976
|
-
label: '群共享记忆',
|
|
2977
|
-
};
|
|
2978
|
-
}
|
|
2979
|
-
return {
|
|
2980
|
-
scope: 'project',
|
|
2981
|
-
label: '项目记忆',
|
|
2982
|
-
};
|
|
2983
|
-
}
|
|
2984
|
-
buildMemoryExpiresAt() {
|
|
2985
|
-
const ttlDays = this.config.service.memory_default_ttl_days;
|
|
2986
|
-
if (!ttlDays) {
|
|
2987
|
-
return undefined;
|
|
2988
|
-
}
|
|
2989
|
-
return new Date(Date.now() + ttlDays * 24 * 60 * 60 * 1000).toISOString();
|
|
2990
|
-
}
|
|
2991
1485
|
async cancelActiveRun(queueKey, reason) {
|
|
2992
1486
|
const live = this.activeRuns.get(queueKey);
|
|
2993
1487
|
if (live) {
|
|
@@ -3017,112 +1511,7 @@ export class FeiqueService {
|
|
|
3017
1511
|
return terminateProcess(persisted.pid, 'SIGTERM');
|
|
3018
1512
|
}
|
|
3019
1513
|
async scheduleProjectExecution(projectContext, metadata, task) {
|
|
3020
|
-
|
|
3021
|
-
const queued = await this.prepareQueuedExecution(projectContext, metadata, runId);
|
|
3022
|
-
const rootKey = buildProjectRootQueueKey(projectContext.project.root);
|
|
3023
|
-
const startGate = createDeferred();
|
|
3024
|
-
// Record queue depth when enqueuing
|
|
3025
|
-
this.metrics?.recordQueueDepth(projectContext.projectAlias, this.queue.getPendingCount(projectContext.queueKey) + 1);
|
|
3026
|
-
return {
|
|
3027
|
-
runId,
|
|
3028
|
-
queued,
|
|
3029
|
-
release: () => startGate.resolve(),
|
|
3030
|
-
completion: this.queue.run(projectContext.queueKey, async () => {
|
|
3031
|
-
await this.projectRootQueue.run(rootKey, async () => {
|
|
3032
|
-
await startGate.promise;
|
|
3033
|
-
await task(runId);
|
|
3034
|
-
}, { priority: projectContext.project.run_priority });
|
|
3035
|
-
// Record queue depth after dequeue
|
|
3036
|
-
this.metrics?.recordQueueDepth(projectContext.projectAlias, this.queue.getPendingCount(projectContext.queueKey));
|
|
3037
|
-
}),
|
|
3038
|
-
};
|
|
3039
|
-
}
|
|
3040
|
-
async prepareQueuedExecution(projectContext, metadata, runId) {
|
|
3041
|
-
const queuePending = this.queue.getPendingCount(projectContext.queueKey);
|
|
3042
|
-
const rootKey = buildProjectRootQueueKey(projectContext.project.root);
|
|
3043
|
-
const rootPending = this.projectRootQueue.getPendingCount(rootKey);
|
|
3044
|
-
if (queuePending <= 0 && rootPending <= 0) {
|
|
3045
|
-
return null;
|
|
3046
|
-
}
|
|
3047
|
-
const projectRoot = this.resolveProjectRoot(projectContext.project);
|
|
3048
|
-
const reason = queuePending > 0 ? 'project' : 'project-root';
|
|
3049
|
-
const frontCount = reason === 'project' ? queuePending : rootPending;
|
|
3050
|
-
const blockingRun = reason === 'project'
|
|
3051
|
-
? await this.runStateStore.getActiveRun(projectContext.queueKey)
|
|
3052
|
-
: await this.runStateStore.getExecutionRunByProjectRoot(projectRoot);
|
|
3053
|
-
const detail = this.buildQueuedStatusDetail(projectContext.projectAlias, reason, frontCount, blockingRun);
|
|
3054
|
-
await this.runStateStore.upsertRun(runId, {
|
|
3055
|
-
queue_key: projectContext.queueKey,
|
|
3056
|
-
conversation_key: projectContext.sessionKey,
|
|
3057
|
-
project_alias: projectContext.projectAlias,
|
|
3058
|
-
chat_id: metadata.chatId,
|
|
3059
|
-
actor_id: metadata.actorId,
|
|
3060
|
-
actor_name: metadata.actorName,
|
|
3061
|
-
project_root: projectRoot,
|
|
3062
|
-
prompt_excerpt: truncateExcerpt(metadata.prompt),
|
|
3063
|
-
status: 'queued',
|
|
3064
|
-
status_detail: detail,
|
|
3065
|
-
});
|
|
3066
|
-
await this.auditLog.append({
|
|
3067
|
-
type: 'codex.run.queued',
|
|
3068
|
-
run_id: runId,
|
|
3069
|
-
chat_id: metadata.chatId,
|
|
3070
|
-
actor_id: metadata.actorId,
|
|
3071
|
-
project_alias: projectContext.projectAlias,
|
|
3072
|
-
conversation_key: projectContext.sessionKey,
|
|
3073
|
-
project_root: projectRoot,
|
|
3074
|
-
queue_reason: reason,
|
|
3075
|
-
blocking_run_id: blockingRun?.run_id,
|
|
3076
|
-
front_count: frontCount,
|
|
3077
|
-
});
|
|
3078
|
-
this.logger.warn({
|
|
3079
|
-
runId,
|
|
3080
|
-
queueKey: projectContext.queueKey,
|
|
3081
|
-
sessionKey: projectContext.sessionKey,
|
|
3082
|
-
projectAlias: projectContext.projectAlias,
|
|
3083
|
-
projectRoot,
|
|
3084
|
-
reason,
|
|
3085
|
-
frontCount,
|
|
3086
|
-
blockingStatus: blockingRun?.status,
|
|
3087
|
-
blockingProjectAlias: blockingRun?.project_alias,
|
|
3088
|
-
}, 'Codex run queued');
|
|
3089
|
-
return {
|
|
3090
|
-
runId,
|
|
3091
|
-
detail,
|
|
3092
|
-
reason,
|
|
3093
|
-
};
|
|
3094
|
-
}
|
|
3095
|
-
buildAcknowledgedRunReply(projectAlias, phase, detail, mode) {
|
|
3096
|
-
if (mode === 'text') {
|
|
3097
|
-
return [`项目: ${projectAlias}`, `状态: ${phase}`, '', detail].join('\n');
|
|
3098
|
-
}
|
|
3099
|
-
return detail;
|
|
3100
|
-
}
|
|
3101
|
-
buildQueuedStatusDetail(projectAlias, reason, frontCount, blockingRun) {
|
|
3102
|
-
const lines = [
|
|
3103
|
-
reason === 'project' ? `当前项目 ${projectAlias} 已有任务在处理,已进入排队。` : '当前仓库正在被其他会话操作,已进入排队。',
|
|
3104
|
-
frontCount > 0 ? `前方还有 ${frontCount} 个任务。` : null,
|
|
3105
|
-
];
|
|
3106
|
-
if (blockingRun) {
|
|
3107
|
-
const actorName = blockingRun.actor_name ?? blockingRun.actor_id ?? '其他成员';
|
|
3108
|
-
lines.push(`当前执行: ${actorName}`);
|
|
3109
|
-
const elapsedMs = Date.now() - new Date(blockingRun.started_at).getTime();
|
|
3110
|
-
const elapsedMin = Math.round(elapsedMs / 60_000);
|
|
3111
|
-
if (elapsedMin > 0) {
|
|
3112
|
-
lines.push(`已运行: ${elapsedMin} 分钟`);
|
|
3113
|
-
}
|
|
3114
|
-
if (reason === 'project-root' && blockingRun.project_alias && blockingRun.project_alias !== projectAlias) {
|
|
3115
|
-
lines.push(`占用项目: ${blockingRun.project_alias}`);
|
|
3116
|
-
}
|
|
3117
|
-
}
|
|
3118
|
-
lines.push(`排队时间: ${new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })}`);
|
|
3119
|
-
return lines.filter(Boolean).join('\n');
|
|
3120
|
-
}
|
|
3121
|
-
buildRunStatusSummary(lastResponseExcerpt, activeRun) {
|
|
3122
|
-
if (activeRun?.status === 'queued' && activeRun.status_detail) {
|
|
3123
|
-
return [activeRun.status_detail, lastResponseExcerpt ? `\n上一轮摘要:\n${lastResponseExcerpt}` : null].filter(Boolean).join('\n');
|
|
3124
|
-
}
|
|
3125
|
-
return lastResponseExcerpt ?? '暂无会话摘要。';
|
|
1514
|
+
return scheduleProjectExecutionImpl(this, projectContext, metadata, task);
|
|
3126
1515
|
}
|
|
3127
1516
|
isAdminChat(chatId) {
|
|
3128
1517
|
return this.config.security.admin_chat_ids.includes(chatId);
|
|
@@ -3236,9 +1625,6 @@ export class FeiqueService {
|
|
|
3236
1625
|
canMutateRuntimeConfig(chatId) {
|
|
3237
1626
|
return canAccessGlobalCapability(this.config, chatId, 'config:mutate');
|
|
3238
1627
|
}
|
|
3239
|
-
canReadConfigHistory(chatId) {
|
|
3240
|
-
return canAccessGlobalCapability(this.config, chatId, 'config:history') || this.canMutateRuntimeConfig(chatId);
|
|
3241
|
-
}
|
|
3242
1628
|
canAccessAdminCommand(command, currentProjectAlias, authorizedProjectAliases, operatorProjectAliases, globalCapabilities) {
|
|
3243
1629
|
if (command.resource === 'project') {
|
|
3244
1630
|
if (command.action === 'list') {
|
|
@@ -3267,56 +1653,7 @@ export class FeiqueService {
|
|
|
3267
1653
|
return true;
|
|
3268
1654
|
}
|
|
3269
1655
|
async handleAdminConfigCommand(context, command) {
|
|
3270
|
-
|
|
3271
|
-
await this.sendTextReply(context.chat_id, '当前 chat_id 无权查看配置历史。', context.message_id, context.text);
|
|
3272
|
-
return;
|
|
3273
|
-
}
|
|
3274
|
-
if (command.action === 'rollback' && !this.canMutateRuntimeConfig(context.chat_id)) {
|
|
3275
|
-
await this.sendTextReply(context.chat_id, '当前 chat_id 无权回滚配置。', context.message_id, context.text);
|
|
3276
|
-
return;
|
|
3277
|
-
}
|
|
3278
|
-
if (!this.runtimeControl?.configPath) {
|
|
3279
|
-
await this.sendTextReply(context.chat_id, '当前运行实例没有可写配置路径,无法执行配置历史操作。', context.message_id, context.text);
|
|
3280
|
-
return;
|
|
3281
|
-
}
|
|
3282
|
-
if (command.action === 'history') {
|
|
3283
|
-
const snapshots = await this.configHistoryStore.listSnapshots();
|
|
3284
|
-
if (snapshots.length === 0) {
|
|
3285
|
-
await this.sendTextReply(context.chat_id, '当前没有可回滚的配置快照。', context.message_id, context.text);
|
|
3286
|
-
return;
|
|
3287
|
-
}
|
|
3288
|
-
const lines = ['最近配置快照:'];
|
|
3289
|
-
for (const snapshot of snapshots) {
|
|
3290
|
-
lines.push(`- ${snapshot.id} | ${snapshot.at} | ${snapshot.action}${snapshot.summary ? ` | ${snapshot.summary}` : ''}`);
|
|
3291
|
-
}
|
|
3292
|
-
await this.sendTextReply(context.chat_id, lines.join('\n'), context.message_id, context.text);
|
|
3293
|
-
return;
|
|
3294
|
-
}
|
|
3295
|
-
const target = await this.configHistoryStore.getSnapshot(command.value);
|
|
3296
|
-
if (!target) {
|
|
3297
|
-
await this.sendTextReply(context.chat_id, '未找到指定配置快照。可先执行 `/admin config history`。', context.message_id, context.text);
|
|
3298
|
-
return;
|
|
3299
|
-
}
|
|
3300
|
-
const rollbackSnapshot = await this.snapshotConfigForAdminMutation(context, 'config.rollback', `rollback -> ${target.id}`);
|
|
3301
|
-
const previousContent = rollbackSnapshot.content;
|
|
3302
|
-
try {
|
|
3303
|
-
await writeUtf8Atomic(this.runtimeControl.configPath, target.content);
|
|
3304
|
-
await this.reloadRuntimeConfigFromDisk(this.runtimeControl.configPath);
|
|
3305
|
-
}
|
|
3306
|
-
catch (error) {
|
|
3307
|
-
await writeUtf8Atomic(this.runtimeControl.configPath, previousContent);
|
|
3308
|
-
await this.reloadRuntimeConfigFromDisk(this.runtimeControl.configPath);
|
|
3309
|
-
throw error;
|
|
3310
|
-
}
|
|
3311
|
-
await this.appendAdminAudit({
|
|
3312
|
-
type: 'admin.config.rollback',
|
|
3313
|
-
chat_id: context.chat_id,
|
|
3314
|
-
actor_id: context.actor_id,
|
|
3315
|
-
target_snapshot_id: target.id,
|
|
3316
|
-
snapshot_id: rollbackSnapshot.id,
|
|
3317
|
-
config_path: this.runtimeControl.configPath,
|
|
3318
|
-
});
|
|
3319
|
-
await this.sendTextReply(context.chat_id, `已回滚配置。\n目标快照: ${target.id}\n回滚前快照: ${rollbackSnapshot.id}\n如需生效到某些运行时状态,请再执行 /admin service restart。`, context.message_id, context.text);
|
|
1656
|
+
return handleAdminConfigCommandImpl(this, context, command);
|
|
3320
1657
|
}
|
|
3321
1658
|
async snapshotConfigForAdminMutation(context, action, summary) {
|
|
3322
1659
|
if (!this.runtimeControl?.configPath) {
|
|
@@ -3343,65 +1680,6 @@ export class FeiqueService {
|
|
|
3343
1680
|
replaceObject(this.config.feishu, nextConfig.feishu);
|
|
3344
1681
|
replaceProjects(this.config.projects, nextConfig.projects);
|
|
3345
1682
|
}
|
|
3346
|
-
parseProjectPatch(field, value) {
|
|
3347
|
-
switch (field) {
|
|
3348
|
-
case 'root':
|
|
3349
|
-
return { root: value };
|
|
3350
|
-
case 'profile':
|
|
3351
|
-
return { profile: value };
|
|
3352
|
-
case 'sandbox':
|
|
3353
|
-
if (value === 'read-only' || value === 'workspace-write' || value === 'danger-full-access') {
|
|
3354
|
-
return { sandbox: value };
|
|
3355
|
-
}
|
|
3356
|
-
return null;
|
|
3357
|
-
case 'session_scope':
|
|
3358
|
-
if (value === 'chat' || value === 'chat-user') {
|
|
3359
|
-
return { session_scope: value };
|
|
3360
|
-
}
|
|
3361
|
-
return null;
|
|
3362
|
-
case 'mention_required':
|
|
3363
|
-
if (value === 'true' || value === 'false') {
|
|
3364
|
-
return { mention_required: value === 'true' };
|
|
3365
|
-
}
|
|
3366
|
-
return null;
|
|
3367
|
-
case 'description':
|
|
3368
|
-
return { description: value };
|
|
3369
|
-
case 'viewer_chat_ids':
|
|
3370
|
-
return { viewer_chat_ids: splitCommaSeparatedValues(value) };
|
|
3371
|
-
case 'operator_chat_ids':
|
|
3372
|
-
return { operator_chat_ids: splitCommaSeparatedValues(value) };
|
|
3373
|
-
case 'admin_chat_ids':
|
|
3374
|
-
return { admin_chat_ids: splitCommaSeparatedValues(value) };
|
|
3375
|
-
case 'session_operator_chat_ids':
|
|
3376
|
-
return { session_operator_chat_ids: splitCommaSeparatedValues(value) };
|
|
3377
|
-
case 'run_operator_chat_ids':
|
|
3378
|
-
return { run_operator_chat_ids: splitCommaSeparatedValues(value) };
|
|
3379
|
-
case 'config_admin_chat_ids':
|
|
3380
|
-
return { config_admin_chat_ids: splitCommaSeparatedValues(value) };
|
|
3381
|
-
case 'download_dir':
|
|
3382
|
-
return { download_dir: value };
|
|
3383
|
-
case 'temp_dir':
|
|
3384
|
-
return { temp_dir: value };
|
|
3385
|
-
case 'cache_dir':
|
|
3386
|
-
return { cache_dir: value };
|
|
3387
|
-
case 'log_dir':
|
|
3388
|
-
return { log_dir: value };
|
|
3389
|
-
case 'run_priority': {
|
|
3390
|
-
const parsed = Number(value);
|
|
3391
|
-
return Number.isInteger(parsed) && parsed >= 1 && parsed <= 1000 ? { run_priority: parsed } : null;
|
|
3392
|
-
}
|
|
3393
|
-
case 'chat_rate_limit_window_seconds': {
|
|
3394
|
-
const parsed = Number(value);
|
|
3395
|
-
return Number.isInteger(parsed) && parsed > 0 ? { chat_rate_limit_window_seconds: parsed } : null;
|
|
3396
|
-
}
|
|
3397
|
-
case 'chat_rate_limit_max_runs': {
|
|
3398
|
-
const parsed = Number(value);
|
|
3399
|
-
return Number.isInteger(parsed) && parsed > 0 ? { chat_rate_limit_max_runs: parsed } : null;
|
|
3400
|
-
}
|
|
3401
|
-
default:
|
|
3402
|
-
return null;
|
|
3403
|
-
}
|
|
3404
|
-
}
|
|
3405
1683
|
resolveProjectDownloadDir(projectAlias, project) {
|
|
3406
1684
|
return getProjectDownloadsDir(this.config.storage.dir, projectAlias, project);
|
|
3407
1685
|
}
|
|
@@ -3425,28 +1703,6 @@ export class FeiqueService {
|
|
|
3425
1703
|
catch { /* best-effort */ }
|
|
3426
1704
|
}
|
|
3427
1705
|
}
|
|
3428
|
-
listManagedAuditTargets() {
|
|
3429
|
-
const targets = [
|
|
3430
|
-
{
|
|
3431
|
-
stateDir: this.config.storage.dir,
|
|
3432
|
-
fileName: 'audit.jsonl',
|
|
3433
|
-
archiveDir: path.join(this.config.storage.dir, 'archive'),
|
|
3434
|
-
},
|
|
3435
|
-
{
|
|
3436
|
-
stateDir: this.config.storage.dir,
|
|
3437
|
-
fileName: 'admin-audit.jsonl',
|
|
3438
|
-
archiveDir: path.join(this.config.storage.dir, 'archive'),
|
|
3439
|
-
},
|
|
3440
|
-
];
|
|
3441
|
-
for (const [alias, project] of Object.entries(this.config.projects)) {
|
|
3442
|
-
targets.push({
|
|
3443
|
-
stateDir: getProjectAuditDir(this.config.storage.dir, alias, project),
|
|
3444
|
-
fileName: path.basename(getProjectAuditFile(this.config.storage.dir, alias, project)),
|
|
3445
|
-
archiveDir: getProjectArchiveDir(this.config.storage.dir, alias),
|
|
3446
|
-
});
|
|
3447
|
-
}
|
|
3448
|
-
return targets;
|
|
3449
|
-
}
|
|
3450
1706
|
checkAndConsumeChatRateLimit(projectAlias, project, chatId) {
|
|
3451
1707
|
const windowMs = project.chat_rate_limit_window_seconds * 1000;
|
|
3452
1708
|
const maxRuns = project.chat_rate_limit_max_runs;
|
|
@@ -3500,12 +1756,108 @@ export class FeiqueService {
|
|
|
3500
1756
|
}
|
|
3501
1757
|
this.config.feishu.allowed_chat_ids = values;
|
|
3502
1758
|
}
|
|
3503
|
-
resolveProjectRoot(project) {
|
|
3504
|
-
return path.resolve(project.root);
|
|
3505
|
-
}
|
|
3506
1759
|
resolveBackendByName(projectAlias, sessionOverride) {
|
|
3507
1760
|
return resolveProjectBackendWithOverride(this.config, projectAlias, sessionOverride, this.codexSessionIndex);
|
|
3508
1761
|
}
|
|
1762
|
+
/**
|
|
1763
|
+
* Called when resolveProjectBackendWithFailover returns a non-null failover.
|
|
1764
|
+
* Responsibilities:
|
|
1765
|
+
* - Log a warning with structured context
|
|
1766
|
+
* - Emit an audit event
|
|
1767
|
+
* - Notify admin chats (deduped by from→to direction for the process lifetime)
|
|
1768
|
+
* - Send a user-visible notice into the current chat so the user knows
|
|
1769
|
+
* why their run is on a different backend than expected
|
|
1770
|
+
*/
|
|
1771
|
+
async handleBackendFailover(chatId, projectAlias, runId, info) {
|
|
1772
|
+
this.logger.warn({ projectAlias, runId, from: info.from, to: info.to, reason: info.reason }, 'Backend failover: primary probe failed, switched to alternate');
|
|
1773
|
+
try {
|
|
1774
|
+
await this.auditLog.append({
|
|
1775
|
+
type: 'backend.failover',
|
|
1776
|
+
project_alias: projectAlias,
|
|
1777
|
+
run_id: runId,
|
|
1778
|
+
from: info.from,
|
|
1779
|
+
to: info.to,
|
|
1780
|
+
reason: info.reason,
|
|
1781
|
+
});
|
|
1782
|
+
}
|
|
1783
|
+
catch { /* best-effort */ }
|
|
1784
|
+
const userNotice = `⚠️ ${info.from} 不可用,已临时切换到 ${info.to} 运行本次请求。\n原因: ${info.reason}`;
|
|
1785
|
+
try {
|
|
1786
|
+
await this.feishuClient.sendText(chatId, userNotice);
|
|
1787
|
+
}
|
|
1788
|
+
catch { /* best-effort */ }
|
|
1789
|
+
const dedupeKey = `${info.from}->${info.to}`;
|
|
1790
|
+
if (this.failoverNotified.has(dedupeKey))
|
|
1791
|
+
return;
|
|
1792
|
+
this.failoverNotified.add(dedupeKey);
|
|
1793
|
+
const adminChatIds = this.config.security.admin_chat_ids ?? [];
|
|
1794
|
+
if (adminChatIds.length === 0)
|
|
1795
|
+
return;
|
|
1796
|
+
const adminText = `🔁 Backend failover 已触发\n\n` +
|
|
1797
|
+
`方向: ${info.from} → ${info.to}\n` +
|
|
1798
|
+
`项目: ${projectAlias}\n` +
|
|
1799
|
+
`原因: ${info.reason}\n\n` +
|
|
1800
|
+
`后续同方向的切换不会重复通知,直到服务重启。`;
|
|
1801
|
+
for (const adminChat of adminChatIds) {
|
|
1802
|
+
try {
|
|
1803
|
+
await this.feishuClient.sendText(adminChat, adminText);
|
|
1804
|
+
}
|
|
1805
|
+
catch { /* best-effort */ }
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
/**
|
|
1809
|
+
* Called by the transport layer when an incoming chat is rejected by the
|
|
1810
|
+
* allowlist (`feishu.allowed_chat_ids` / `allowed_group_ids`).
|
|
1811
|
+
*
|
|
1812
|
+
* Replaces the previous "silent drop" behavior with a pairing-style
|
|
1813
|
+
* experience: tell the user what their chat_id is so an admin can add it,
|
|
1814
|
+
* notify admins that a new chat is knocking, and record the rejection in
|
|
1815
|
+
* the audit log. Deduped per chat_id for the process lifetime so repeated
|
|
1816
|
+
* attempts from the same unauthorized chat do not spam either side.
|
|
1817
|
+
*
|
|
1818
|
+
* This is best-effort: any send failure is swallowed. We never want a
|
|
1819
|
+
* rejection flow to throw out of the transport dispatcher.
|
|
1820
|
+
*/
|
|
1821
|
+
async handleRejectedChat(chatId, chatType) {
|
|
1822
|
+
if (this.rejectedChatNotified.has(chatId))
|
|
1823
|
+
return;
|
|
1824
|
+
this.rejectedChatNotified.add(chatId);
|
|
1825
|
+
try {
|
|
1826
|
+
await this.auditLog.append({
|
|
1827
|
+
type: 'chat.rejected',
|
|
1828
|
+
chat_id: chatId,
|
|
1829
|
+
chat_type: chatType,
|
|
1830
|
+
});
|
|
1831
|
+
}
|
|
1832
|
+
catch { /* best-effort */ }
|
|
1833
|
+
const listHint = chatType === 'group'
|
|
1834
|
+
? '`feishu.allowed_group_ids`(群聊)'
|
|
1835
|
+
: '`feishu.allowed_chat_ids`(私聊)';
|
|
1836
|
+
const userText = `抱歉,该 chat 未被授权访问本 bot。\n` +
|
|
1837
|
+
`你的 chat_id: ${chatId}\n\n` +
|
|
1838
|
+
`请联系管理员并附上这个 id,请求加入 ${listHint}。`;
|
|
1839
|
+
try {
|
|
1840
|
+
await this.feishuClient.sendText(chatId, userText);
|
|
1841
|
+
}
|
|
1842
|
+
catch { /* best-effort */ }
|
|
1843
|
+
const adminChatIds = this.config.security.admin_chat_ids ?? [];
|
|
1844
|
+
if (adminChatIds.length === 0)
|
|
1845
|
+
return;
|
|
1846
|
+
const adminCommand = chatType === 'group'
|
|
1847
|
+
? `/admin group add ${chatId}`
|
|
1848
|
+
: `/admin chat add ${chatId}`;
|
|
1849
|
+
const adminText = `🔔 新 chat 请求接入\n\n` +
|
|
1850
|
+
`chat_id: ${chatId}\n` +
|
|
1851
|
+
`类型: ${chatType}\n\n` +
|
|
1852
|
+
`如需授权,运行:\n${adminCommand}\n\n` +
|
|
1853
|
+
`后续来自同一 chat 的请求不会重复通知,直到服务重启。`;
|
|
1854
|
+
for (const adminChat of adminChatIds) {
|
|
1855
|
+
try {
|
|
1856
|
+
await this.feishuClient.sendText(adminChat, adminText);
|
|
1857
|
+
}
|
|
1858
|
+
catch { /* best-effort */ }
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
3509
1861
|
async enforceSessionHistoryLimit(conversationKey, projectAlias) {
|
|
3510
1862
|
const sessions = await this.sessionStore.listProjectSessions(conversationKey, projectAlias);
|
|
3511
1863
|
const overflow = sessions.slice(this.config.service.session_history_limit);
|
|
@@ -3522,11 +1874,11 @@ export class FeiqueService {
|
|
|
3522
1874
|
mentionPrefix = `<at user_id="${actor.actor_id}">${displayName}</at>\n`;
|
|
3523
1875
|
}
|
|
3524
1876
|
const bodyWithMention = mentionPrefix ? mentionPrefix + body : body;
|
|
3525
|
-
const title =
|
|
1877
|
+
const title = buildReplyTitle(sanitizeUserVisibleReply(body));
|
|
3526
1878
|
// Card mode uses replyToMessageId for threading — @mention tags render as
|
|
3527
1879
|
// literal text inside card JSON, so use the clean body for cards.
|
|
3528
|
-
const formattedBodyClean =
|
|
3529
|
-
const formattedBodyWithMention =
|
|
1880
|
+
const formattedBodyClean = sanitizeUserVisibleReply(formatQuotedReply(body, originalText));
|
|
1881
|
+
const formattedBodyWithMention = sanitizeUserVisibleReply(formatQuotedReply(bodyWithMention, originalText));
|
|
3530
1882
|
if (this.config.service.reply_mode === 'card') {
|
|
3531
1883
|
const card = buildMessageCard({
|
|
3532
1884
|
title,
|
|
@@ -3568,7 +1920,7 @@ export class FeiqueService {
|
|
|
3568
1920
|
return response;
|
|
3569
1921
|
}
|
|
3570
1922
|
if (this.config.service.reply_quote_user_message && replyToMessageId) {
|
|
3571
|
-
const response = await this.feishuClient.sendText(chatId,
|
|
1923
|
+
const response = await this.feishuClient.sendText(chatId, sanitizeUserVisibleReply(bodyWithMention), { replyToMessageId });
|
|
3572
1924
|
await this.auditLog.append({
|
|
3573
1925
|
type: 'message.replied',
|
|
3574
1926
|
chat_id: chatId,
|
|
@@ -3594,10 +1946,10 @@ export class FeiqueService {
|
|
|
3594
1946
|
return this.feishuClient.sendCard(chatId, card);
|
|
3595
1947
|
}
|
|
3596
1948
|
async sendRunLifecycleReply(input) {
|
|
3597
|
-
const lifecycleMode = this.
|
|
1949
|
+
const lifecycleMode = resolveRunLifecycleReplyMode(this.config);
|
|
3598
1950
|
const lifecycleReplyOptions = input.replyToMessageId ? { replyToMessageId: input.replyToMessageId } : undefined;
|
|
3599
1951
|
if (lifecycleMode === 'card') {
|
|
3600
|
-
const card =
|
|
1952
|
+
const card = buildRunLifecycleCard({
|
|
3601
1953
|
title: input.title,
|
|
3602
1954
|
body: input.body,
|
|
3603
1955
|
projectAlias: input.projectAlias,
|
|
@@ -3618,8 +1970,8 @@ export class FeiqueService {
|
|
|
3618
1970
|
return response;
|
|
3619
1971
|
}
|
|
3620
1972
|
if (lifecycleMode === 'post') {
|
|
3621
|
-
const postBody =
|
|
3622
|
-
const title =
|
|
1973
|
+
const postBody = sanitizeUserVisibleReply(formatQuotedReply(input.body, input.originalText));
|
|
1974
|
+
const title = buildReplyTitle(postBody);
|
|
3623
1975
|
const post = buildFeishuPost(title, postBody);
|
|
3624
1976
|
const response = lifecycleReplyOptions
|
|
3625
1977
|
? await this.feishuClient.sendPost(input.chatId, post, lifecycleReplyOptions)
|
|
@@ -3635,7 +1987,7 @@ export class FeiqueService {
|
|
|
3635
1987
|
return response;
|
|
3636
1988
|
}
|
|
3637
1989
|
const response = lifecycleReplyOptions
|
|
3638
|
-
? await this.feishuClient.sendText(input.chatId,
|
|
1990
|
+
? await this.feishuClient.sendText(input.chatId, sanitizeUserVisibleReply(formatQuotedReply(input.body, input.originalText)), lifecycleReplyOptions)
|
|
3639
1991
|
: await this.sendTextReply(input.chatId, input.body, input.replyToMessageId, input.originalText);
|
|
3640
1992
|
await this.auditLog.append({
|
|
3641
1993
|
type: 'codex.run.replied',
|
|
@@ -3651,20 +2003,20 @@ export class FeiqueService {
|
|
|
3651
2003
|
if (queued) {
|
|
3652
2004
|
return {
|
|
3653
2005
|
title: '已加入排队',
|
|
3654
|
-
body:
|
|
2006
|
+
body: buildAcknowledgedRunReply(projectAlias, '排队中', queued.detail, mode),
|
|
3655
2007
|
runStatus: 'queued',
|
|
3656
2008
|
runPhase: '排队中',
|
|
3657
2009
|
};
|
|
3658
2010
|
}
|
|
3659
2011
|
return {
|
|
3660
2012
|
title: '已接收请求',
|
|
3661
|
-
body:
|
|
2013
|
+
body: buildAcknowledgedRunReply(projectAlias, '已接收', '已收到你的消息,正在准备处理。', mode),
|
|
3662
2014
|
runStatus: 'running',
|
|
3663
2015
|
runPhase: '已接收',
|
|
3664
2016
|
};
|
|
3665
2017
|
}
|
|
3666
2018
|
async sendInitialRunLifecycleReply(input) {
|
|
3667
|
-
const lifecycleMode = this.
|
|
2019
|
+
const lifecycleMode = resolveRunLifecycleReplyMode(this.config);
|
|
3668
2020
|
const draft = this.buildInitialRunLifecycleReply(input.projectAlias, input.queued, lifecycleMode);
|
|
3669
2021
|
try {
|
|
3670
2022
|
const response = await this.sendRunLifecycleReply({
|
|
@@ -3684,7 +2036,7 @@ export class FeiqueService {
|
|
|
3684
2036
|
this.logger.warn({ error, runId: input.runId, projectAlias: input.projectAlias }, 'Failed to send initial lifecycle reply');
|
|
3685
2037
|
}
|
|
3686
2038
|
}
|
|
3687
|
-
async rememberRunReplyTarget(runId, response, mode = this.
|
|
2039
|
+
async rememberRunReplyTarget(runId, response, mode = resolveRunLifecycleReplyMode(this.config)) {
|
|
3688
2040
|
this.runReplyTargets.set(runId, {
|
|
3689
2041
|
messageId: response.message_id,
|
|
3690
2042
|
mode,
|
|
@@ -3696,7 +2048,7 @@ export class FeiqueService {
|
|
|
3696
2048
|
return;
|
|
3697
2049
|
}
|
|
3698
2050
|
const label = backendLabel ?? 'AI';
|
|
3699
|
-
const body =
|
|
2051
|
+
const body = buildAcknowledgedRunReply(projectAlias, '处理中', '桥接器已开始处理你的请求。', target.mode);
|
|
3700
2052
|
await this.updateRunLifecycleReply({
|
|
3701
2053
|
chatId,
|
|
3702
2054
|
projectAlias,
|
|
@@ -3714,7 +2066,7 @@ export class FeiqueService {
|
|
|
3714
2066
|
}
|
|
3715
2067
|
const label = backendLabel ?? 'AI';
|
|
3716
2068
|
const body = [
|
|
3717
|
-
|
|
2069
|
+
buildAcknowledgedRunReply(input.projectAlias, '处理中', '桥接器正在持续处理你的请求。', target.mode),
|
|
3718
2070
|
'最新进展:',
|
|
3719
2071
|
progress,
|
|
3720
2072
|
]
|
|
@@ -3766,10 +2118,10 @@ export class FeiqueService {
|
|
|
3766
2118
|
if (!target?.messageId) {
|
|
3767
2119
|
return false;
|
|
3768
2120
|
}
|
|
3769
|
-
const sanitizedBody =
|
|
2121
|
+
const sanitizedBody = sanitizeUserVisibleReply(input.body);
|
|
3770
2122
|
if (target.mode === 'card') {
|
|
3771
|
-
const includeActions = input.runStatus === 'success' && this.
|
|
3772
|
-
await this.feishuClient.updateCard(target.messageId,
|
|
2123
|
+
const includeActions = input.runStatus === 'success' && supportsInteractiveCardActions(this.config) && input.sessionKey !== undefined;
|
|
2124
|
+
await this.feishuClient.updateCard(target.messageId, buildRunLifecycleCard({
|
|
3773
2125
|
title: input.title,
|
|
3774
2126
|
body: input.body,
|
|
3775
2127
|
projectAlias: input.projectAlias,
|
|
@@ -3804,7 +2156,7 @@ export class FeiqueService {
|
|
|
3804
2156
|
}));
|
|
3805
2157
|
}
|
|
3806
2158
|
else if (target.mode === 'post') {
|
|
3807
|
-
const title =
|
|
2159
|
+
const title = buildReplyTitle(sanitizedBody);
|
|
3808
2160
|
await this.feishuClient.updatePost(target.messageId, buildFeishuPost(title, sanitizedBody));
|
|
3809
2161
|
}
|
|
3810
2162
|
else {
|
|
@@ -3830,332 +2182,9 @@ export class FeiqueService {
|
|
|
3830
2182
|
});
|
|
3831
2183
|
return true;
|
|
3832
2184
|
}
|
|
3833
|
-
formatQuotedReply(body, originalText) {
|
|
3834
|
-
return body;
|
|
3835
|
-
}
|
|
3836
|
-
buildReplyTitle(body) {
|
|
3837
|
-
const firstLine = body
|
|
3838
|
-
.split(/\r?\n/)
|
|
3839
|
-
.map((line) => line.trim())
|
|
3840
|
-
.find(Boolean);
|
|
3841
|
-
return truncateExcerpt(firstLine ?? '飞鹊 (Feique)', 40);
|
|
3842
|
-
}
|
|
3843
|
-
sanitizeUserVisibleReply(body) {
|
|
3844
|
-
return body
|
|
3845
|
-
.split(/\r?\n/)
|
|
3846
|
-
.filter((line) => !/^(运行|当前运行|阻塞运行|run[_ -]?id|session[_ -]?id|conversation[_ -]?key|chat[_ -]?id|tenant[_ -]?key|project[_ -]?root|pid):/i.test(line.trim()))
|
|
3847
|
-
.join('\n')
|
|
3848
|
-
.replace(/\n{3,}/g, '\n\n')
|
|
3849
|
-
.trim();
|
|
3850
|
-
}
|
|
3851
|
-
supportsInteractiveCardActions() {
|
|
3852
|
-
return this.config.feishu.transport === 'webhook';
|
|
3853
|
-
}
|
|
3854
|
-
resolveRunLifecycleReplyMode() {
|
|
3855
|
-
if (this.config.service.reply_mode === 'post') {
|
|
3856
|
-
return 'card';
|
|
3857
|
-
}
|
|
3858
|
-
return this.config.service.reply_mode;
|
|
3859
|
-
}
|
|
3860
|
-
buildRunLifecycleCard(input) {
|
|
3861
|
-
const sanitizedBody = this.sanitizeUserVisibleReply(input.body);
|
|
3862
|
-
if (input.includeActions) {
|
|
3863
|
-
return buildStatusCard({
|
|
3864
|
-
title: input.title,
|
|
3865
|
-
summary: input.cardSummary ?? truncateForFeishuCard(this.stripLifecycleMetadata(sanitizedBody)),
|
|
3866
|
-
projectAlias: input.projectAlias,
|
|
3867
|
-
runStatus: input.runStatus,
|
|
3868
|
-
runPhase: input.runPhase,
|
|
3869
|
-
includeActions: true,
|
|
3870
|
-
rerunPayload: input.rerunPayload,
|
|
3871
|
-
newSessionPayload: input.newSessionPayload,
|
|
3872
|
-
statusPayload: input.statusPayload,
|
|
3873
|
-
cancelPayload: input.cancelPayload,
|
|
3874
|
-
});
|
|
3875
|
-
}
|
|
3876
|
-
return buildMessageCard({
|
|
3877
|
-
title: input.title,
|
|
3878
|
-
body: this.stripLifecycleMetadata(sanitizedBody),
|
|
3879
|
-
status: input.runStatus,
|
|
3880
|
-
phase: input.runPhase,
|
|
3881
|
-
projectAlias: input.projectAlias,
|
|
3882
|
-
});
|
|
3883
|
-
}
|
|
3884
|
-
stripLifecycleMetadata(body) {
|
|
3885
|
-
return body
|
|
3886
|
-
.split(/\r?\n/)
|
|
3887
|
-
.filter((line) => !/^(项目|处理状态|会话|当前会话|已保存会话数):/.test(line.trim()))
|
|
3888
|
-
.join('\n')
|
|
3889
|
-
.replace(/\n{3,}/g, '\n\n')
|
|
3890
|
-
.trim();
|
|
3891
|
-
}
|
|
3892
|
-
}
|
|
3893
|
-
export function buildQueueKey(conversationKey, projectAlias) {
|
|
3894
|
-
return `${conversationKey}::project::${projectAlias}`;
|
|
3895
|
-
}
|
|
3896
|
-
export function buildProjectRootQueueKey(projectRoot) {
|
|
3897
|
-
return `root::${path.resolve(projectRoot)}`;
|
|
3898
|
-
}
|
|
3899
|
-
function isExecutionRunStatus(status) {
|
|
3900
|
-
return status === 'running' || status === 'orphaned';
|
|
3901
|
-
}
|
|
3902
|
-
function isVisibleRunStatus(status) {
|
|
3903
|
-
return status === 'queued' || isExecutionRunStatus(status);
|
|
3904
|
-
}
|
|
3905
|
-
function buildMessageDedupeKey(context) {
|
|
3906
|
-
return ['message', context.tenant_key ?? 'tenant', context.chat_id, context.message_id].join('::');
|
|
3907
|
-
}
|
|
3908
|
-
function buildCardDedupeKey(context, action) {
|
|
3909
|
-
if (!context.open_message_id) {
|
|
3910
|
-
return null;
|
|
3911
|
-
}
|
|
3912
|
-
return ['card', context.tenant_key ?? 'tenant', context.chat_id ?? 'chat', context.actor_id ?? 'actor', context.open_message_id, action].join('::');
|
|
3913
|
-
}
|
|
3914
|
-
/**
|
|
3915
|
-
* Extract [SEND_FILE:/path/to/file] markers from AI response text.
|
|
3916
|
-
* Returns cleaned text (markers removed) and list of file paths.
|
|
3917
|
-
*/
|
|
3918
|
-
function extractFileMarkers(text) {
|
|
3919
|
-
const FILE_MARKER_RE = /\[SEND_FILE:([^\]]+)\]/g;
|
|
3920
|
-
const filePaths = [];
|
|
3921
|
-
let match;
|
|
3922
|
-
while ((match = FILE_MARKER_RE.exec(text)) !== null) {
|
|
3923
|
-
const filePath = match[1]?.trim();
|
|
3924
|
-
if (filePath) {
|
|
3925
|
-
filePaths.push(filePath);
|
|
3926
|
-
}
|
|
3927
|
-
}
|
|
3928
|
-
const cleanText = text.replace(FILE_MARKER_RE, '').replace(/\n{3,}/g, '\n\n').trim();
|
|
3929
|
-
return { cleanText, filePaths };
|
|
3930
|
-
}
|
|
3931
|
-
/**
|
|
3932
|
-
* Shallow diff two BridgeConfig objects, returning human-readable change descriptions.
|
|
3933
|
-
*/
|
|
3934
|
-
function diffConfigs(oldConfig, newConfig) {
|
|
3935
|
-
const changes = [];
|
|
3936
|
-
// Projects added/removed
|
|
3937
|
-
const oldProjects = new Set(Object.keys(oldConfig.projects));
|
|
3938
|
-
const newProjects = new Set(Object.keys(newConfig.projects));
|
|
3939
|
-
for (const p of newProjects) {
|
|
3940
|
-
if (!oldProjects.has(p))
|
|
3941
|
-
changes.push(`项目新增: ${p}`);
|
|
3942
|
-
}
|
|
3943
|
-
for (const p of oldProjects) {
|
|
3944
|
-
if (!newProjects.has(p))
|
|
3945
|
-
changes.push(`项目移除: ${p}`);
|
|
3946
|
-
}
|
|
3947
|
-
// Project-level changes
|
|
3948
|
-
for (const alias of newProjects) {
|
|
3949
|
-
if (!oldProjects.has(alias))
|
|
3950
|
-
continue;
|
|
3951
|
-
const oldP = oldConfig.projects[alias];
|
|
3952
|
-
const newP = newConfig.projects[alias];
|
|
3953
|
-
if (!oldP || !newP)
|
|
3954
|
-
continue;
|
|
3955
|
-
const fields = ['root', 'backend', 'persona', 'codex_model', 'claude_model', 'mention_required', 'description', 'session_scope'];
|
|
3956
|
-
for (const f of fields) {
|
|
3957
|
-
const ov = String(oldP[f] ?? '');
|
|
3958
|
-
const nv = String(newP[f] ?? '');
|
|
3959
|
-
if (ov !== nv)
|
|
3960
|
-
changes.push(`${alias}.${f}: ${ov || '(空)'} → ${nv || '(空)'}`);
|
|
3961
|
-
}
|
|
3962
|
-
// Array fields
|
|
3963
|
-
const arrayFields = ['skills', 'admin_chat_ids', 'operator_chat_ids', 'notification_chat_ids'];
|
|
3964
|
-
for (const f of arrayFields) {
|
|
3965
|
-
const ov = JSON.stringify(oldP[f] ?? []);
|
|
3966
|
-
const nv = JSON.stringify(newP[f] ?? []);
|
|
3967
|
-
if (ov !== nv)
|
|
3968
|
-
changes.push(`${alias}.${f} 变更`);
|
|
3969
|
-
}
|
|
3970
|
-
}
|
|
3971
|
-
// Service-level changes
|
|
3972
|
-
const serviceFields = ['default_project', 'reply_mode', 'persona', 'team_digest_enabled', 'intent_classifier_enabled'];
|
|
3973
|
-
for (const f of serviceFields) {
|
|
3974
|
-
const ov = String(oldConfig.service[f] ?? '');
|
|
3975
|
-
const nv = String(newConfig.service[f] ?? '');
|
|
3976
|
-
if (ov !== nv)
|
|
3977
|
-
changes.push(`service.${f}: ${ov || '(空)'} → ${nv || '(空)'}`);
|
|
3978
|
-
}
|
|
3979
|
-
// Backend default
|
|
3980
|
-
if (oldConfig.backend?.default !== newConfig.backend?.default) {
|
|
3981
|
-
changes.push(`backend.default: ${oldConfig.backend?.default ?? 'codex'} → ${newConfig.backend?.default ?? 'codex'}`);
|
|
3982
|
-
}
|
|
3983
|
-
// Security admin changes
|
|
3984
|
-
if (JSON.stringify(oldConfig.security.admin_chat_ids) !== JSON.stringify(newConfig.security.admin_chat_ids)) {
|
|
3985
|
-
changes.push('security.admin_chat_ids 变更');
|
|
3986
|
-
}
|
|
3987
|
-
// Embedding provider
|
|
3988
|
-
if (oldConfig.embedding.provider !== newConfig.embedding.provider) {
|
|
3989
|
-
changes.push(`embedding.provider: ${oldConfig.embedding.provider} → ${newConfig.embedding.provider}`);
|
|
3990
|
-
}
|
|
3991
|
-
if (oldConfig.embedding.ollama_model !== newConfig.embedding.ollama_model) {
|
|
3992
|
-
changes.push(`embedding.ollama_model: ${oldConfig.embedding.ollama_model} → ${newConfig.embedding.ollama_model}`);
|
|
3993
|
-
}
|
|
3994
|
-
return changes;
|
|
3995
|
-
}
|
|
3996
|
-
function truncateExcerpt(text, limit = 160) {
|
|
3997
|
-
return text.length > limit ? `${text.slice(0, limit)}...` : text;
|
|
3998
|
-
}
|
|
3999
|
-
/** Map raw error strings to user-friendly Chinese messages. */
|
|
4000
|
-
function friendlyErrorMessage(error) {
|
|
4001
|
-
const lower = error.toLowerCase();
|
|
4002
|
-
if (lower.includes('econnrefused') || lower.includes('enotfound') || lower.includes('econnreset')) {
|
|
4003
|
-
return '网络连接失败,请检查网络或稍后重试';
|
|
4004
|
-
}
|
|
4005
|
-
if (lower.includes('enoent') || lower.includes('enotdir')) {
|
|
4006
|
-
return '文件路径不存在,请检查项目配置';
|
|
4007
|
-
}
|
|
4008
|
-
if (lower.includes('permission denied') || lower.includes('eacces')) {
|
|
4009
|
-
return '权限不足,请检查文件权限';
|
|
4010
|
-
}
|
|
4011
|
-
if (lower.includes('timeout') || lower.includes('timed out') || lower.includes('etimedout')) {
|
|
4012
|
-
return '执行超时,请尝试拆分为更小的任务';
|
|
4013
|
-
}
|
|
4014
|
-
if (lower.includes('rate limit') || lower.includes('429') || lower.includes('too many requests')) {
|
|
4015
|
-
return 'API 频率限制,请稍后重试';
|
|
4016
|
-
}
|
|
4017
|
-
if (lower.includes('enomem') || lower.includes('out of memory')) {
|
|
4018
|
-
return '内存不足,请关闭其他程序或减少并发任务';
|
|
4019
|
-
}
|
|
4020
|
-
// Default: show a truncated version of the raw error
|
|
4021
|
-
const truncated = error.length > 100 ? error.slice(0, 100) + '...' : error;
|
|
4022
|
-
return `执行异常: ${truncated}。如需帮助请联系管理员。`;
|
|
4023
|
-
}
|
|
4024
|
-
function splitCommaSeparatedValues(value) {
|
|
4025
|
-
return value
|
|
4026
|
-
.split(',')
|
|
4027
|
-
.map((item) => item.trim())
|
|
4028
|
-
.filter(Boolean);
|
|
4029
|
-
}
|
|
4030
|
-
function resolveAdminListTarget(resource) {
|
|
4031
|
-
switch (resource) {
|
|
4032
|
-
case 'viewer':
|
|
4033
|
-
return { section: 'security', key: 'viewer_chat_ids' };
|
|
4034
|
-
case 'operator':
|
|
4035
|
-
return { section: 'security', key: 'operator_chat_ids' };
|
|
4036
|
-
case 'admin':
|
|
4037
|
-
return { section: 'security', key: 'admin_chat_ids' };
|
|
4038
|
-
case 'service-observer':
|
|
4039
|
-
return { section: 'security', key: 'service_observer_chat_ids' };
|
|
4040
|
-
case 'service-restart':
|
|
4041
|
-
return { section: 'security', key: 'service_restart_chat_ids' };
|
|
4042
|
-
case 'config-admin':
|
|
4043
|
-
return { section: 'security', key: 'config_admin_chat_ids' };
|
|
4044
|
-
case 'group':
|
|
4045
|
-
return { section: 'feishu', key: 'allowed_group_ids' };
|
|
4046
|
-
case 'chat':
|
|
4047
|
-
return { section: 'feishu', key: 'allowed_chat_ids' };
|
|
4048
|
-
}
|
|
4049
|
-
}
|
|
4050
|
-
function buildConversationKeyForConversation(conversation) {
|
|
4051
|
-
return buildConversationKey({
|
|
4052
|
-
tenantKey: conversation.tenant_key,
|
|
4053
|
-
chatId: conversation.chat_id,
|
|
4054
|
-
actorId: conversation.actor_id,
|
|
4055
|
-
scope: conversation.scope,
|
|
4056
|
-
});
|
|
4057
|
-
}
|
|
4058
|
-
function renderMemorySection(title, items, budget) {
|
|
4059
|
-
if (items.length === 0) {
|
|
4060
|
-
return [];
|
|
4061
|
-
}
|
|
4062
|
-
const lines = ['', title];
|
|
4063
|
-
let used = 0;
|
|
4064
|
-
for (const item of items) {
|
|
4065
|
-
const line = `- ${item.title}${item.pinned ? ' [pinned]' : ''}: ${item.content}`;
|
|
4066
|
-
if (used + line.length > budget) {
|
|
4067
|
-
break;
|
|
4068
|
-
}
|
|
4069
|
-
lines.push(truncateExcerpt(line, 280));
|
|
4070
|
-
used += line.length;
|
|
4071
|
-
}
|
|
4072
|
-
return lines.length > 2 ? lines : [];
|
|
4073
|
-
}
|
|
4074
|
-
function formatAgeFromNow(isoTimestamp) {
|
|
4075
|
-
const deltaMs = Date.now() - Date.parse(isoTimestamp);
|
|
4076
|
-
if (!Number.isFinite(deltaMs) || deltaMs < 0) {
|
|
4077
|
-
return '0s';
|
|
4078
|
-
}
|
|
4079
|
-
const totalSeconds = Math.floor(deltaMs / 1000);
|
|
4080
|
-
if (totalSeconds < 60) {
|
|
4081
|
-
return `${totalSeconds}s`;
|
|
4082
|
-
}
|
|
4083
|
-
const totalMinutes = Math.floor(totalSeconds / 60);
|
|
4084
|
-
if (totalMinutes < 60) {
|
|
4085
|
-
return `${totalMinutes}m`;
|
|
4086
|
-
}
|
|
4087
|
-
const totalHours = Math.floor(totalMinutes / 60);
|
|
4088
|
-
if (totalHours < 24) {
|
|
4089
|
-
return `${totalHours}h`;
|
|
4090
|
-
}
|
|
4091
|
-
return `${Math.floor(totalHours / 24)}d`;
|
|
4092
|
-
}
|
|
4093
|
-
function parseJsonObject(input) {
|
|
4094
|
-
try {
|
|
4095
|
-
const parsed = JSON.parse(input);
|
|
4096
|
-
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
4097
|
-
throw new Error('JSON payload must be an object.');
|
|
4098
|
-
}
|
|
4099
|
-
return parsed;
|
|
4100
|
-
}
|
|
4101
|
-
catch (error) {
|
|
4102
|
-
throw new Error(`JSON 解析失败: ${error instanceof Error ? error.message : String(error)}`);
|
|
4103
|
-
}
|
|
4104
|
-
}
|
|
4105
|
-
function clampListLimit(input, fallback, max) {
|
|
4106
|
-
const parsed = Number(input ?? fallback);
|
|
4107
|
-
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
4108
|
-
return fallback;
|
|
4109
|
-
}
|
|
4110
|
-
return Math.min(Math.trunc(parsed), max);
|
|
4111
|
-
}
|
|
4112
|
-
function mapRunStatusToPhase(status) {
|
|
4113
|
-
switch (status) {
|
|
4114
|
-
case 'queued':
|
|
4115
|
-
return '排队中';
|
|
4116
|
-
case 'running':
|
|
4117
|
-
return '执行中';
|
|
4118
|
-
case 'success':
|
|
4119
|
-
return '已完成';
|
|
4120
|
-
case 'failure':
|
|
4121
|
-
return '失败';
|
|
4122
|
-
case 'cancelled':
|
|
4123
|
-
return '已取消';
|
|
4124
|
-
case 'stale':
|
|
4125
|
-
return '中断';
|
|
4126
|
-
case 'orphaned':
|
|
4127
|
-
return '恢复中';
|
|
4128
|
-
default:
|
|
4129
|
-
return status;
|
|
4130
|
-
}
|
|
4131
|
-
}
|
|
4132
|
-
function replaceObject(target, next) {
|
|
4133
|
-
for (const key of Object.keys(target)) {
|
|
4134
|
-
if (!(key in next)) {
|
|
4135
|
-
delete target[key];
|
|
4136
|
-
}
|
|
4137
|
-
}
|
|
4138
|
-
for (const [key, value] of Object.entries(next)) {
|
|
4139
|
-
target[key] = value;
|
|
4140
|
-
}
|
|
4141
|
-
}
|
|
4142
|
-
function replaceProjects(target, next) {
|
|
4143
|
-
for (const key of Object.keys(target)) {
|
|
4144
|
-
if (!(key in next)) {
|
|
4145
|
-
delete target[key];
|
|
4146
|
-
}
|
|
4147
|
-
}
|
|
4148
|
-
for (const [alias, project] of Object.entries(next)) {
|
|
4149
|
-
target[alias] = project;
|
|
4150
|
-
}
|
|
4151
|
-
}
|
|
4152
|
-
function createDeferred() {
|
|
4153
|
-
let resolve;
|
|
4154
|
-
let reject;
|
|
4155
|
-
const promise = new Promise((innerResolve, innerReject) => {
|
|
4156
|
-
resolve = innerResolve;
|
|
4157
|
-
reject = innerReject;
|
|
4158
|
-
});
|
|
4159
|
-
return { promise, resolve, reject };
|
|
4160
2185
|
}
|
|
2186
|
+
// Module-level helpers extracted to service-utils.ts (β step 1).
|
|
2187
|
+
// Re-exported here for backward compatibility (src/mcp/server.ts and
|
|
2188
|
+
// src/index.ts depend on these names being importable from this module).
|
|
2189
|
+
export { buildQueueKey, buildProjectRootQueueKey } from './service-utils.js';
|
|
4161
2190
|
//# sourceMappingURL=service.js.map
|