feique 1.3.3 → 1.5.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.d.ts +2 -0
- package/dist/backend/claude.js +57 -54
- package/dist/backend/claude.js.map +1 -1
- package/dist/backend/codex.d.ts +2 -0
- package/dist/backend/codex.js +27 -0
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/factory.d.ts +43 -0
- package/dist/backend/factory.js +109 -29
- package/dist/backend/factory.js.map +1 -1
- package/dist/backend/probe.d.ts +27 -0
- package/dist/backend/probe.js +85 -0
- package/dist/backend/probe.js.map +1 -0
- package/dist/backend/qwen.d.ts +59 -0
- package/dist/backend/qwen.js +372 -0
- package/dist/backend/qwen.js.map +1 -0
- package/dist/backend/registry.d.ts +58 -0
- package/dist/backend/registry.js +23 -0
- package/dist/backend/registry.js.map +1 -0
- package/dist/backend/types.d.ts +6 -1
- 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 +10 -6
- 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/intent-classifier.d.ts +7 -2
- package/dist/bridge/intent-classifier.js +1 -1
- package/dist/bridge/intent-classifier.js.map +1 -1
- 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 +453 -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 +230 -2199
- 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 +50 -16
- package/dist/config/schema.js +41 -2
- 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,35 @@ 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 { getBackendDefinition, listBackendNames } from '../backend/registry.js';
|
|
18
|
+
import { buildQueueKey, isExecutionRunStatus, isVisibleRunStatus, mapRunStatusToPhase, buildMessageDedupeKey, buildCardDedupeKey, truncateExcerpt, friendlyErrorMessage, resolveAdminListTarget, buildConversationKeyForConversation, renderMemorySection, formatAgeFromNow, replaceObject, replaceProjects, } from './service-utils.js';
|
|
19
|
+
import { handleDocCommand as handleDocCommandImpl, handleTaskCommand as handleTaskCommandImpl, handleBaseCommand as handleBaseCommandImpl, handleWikiCommand as handleWikiCommandImpl, } from './feishu-commands.js';
|
|
20
|
+
import { handleMemoryCommand as handleMemoryCommandImpl } from './memory-commands.js';
|
|
21
|
+
import { handleAdminConfigCommand as handleAdminConfigCommandImpl, parseProjectPatch as parseProjectPatchImpl, } from './admin-config.js';
|
|
22
|
+
import { scheduleProjectExecution as scheduleProjectExecutionImpl, buildAcknowledgedRunReply, buildRunStatusSummary, } from './run-scheduler.js';
|
|
23
|
+
import { formatQuotedReply, buildReplyTitle, sanitizeUserVisibleReply, supportsInteractiveCardActions, resolveRunLifecycleReplyMode, buildRunLifecycleCard, } from './reply-builders.js';
|
|
24
|
+
import { executePrompt as executePromptImpl } from './run-pipeline.js';
|
|
25
|
+
import { recoverRuntimeState as recoverRuntimeStateImpl, reloadConfig as reloadConfigImpl, runDigestCycle as runDigestCycleImpl, runMemoryMaintenance as runMemoryMaintenanceImpl, runAuditMaintenance as runAuditMaintenanceImpl, runMaintenanceCycle as runMaintenanceCycleImpl, } from './lifecycle.js';
|
|
26
|
+
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
27
|
import { bindProjectAlias, createProjectAlias, removeProjectAlias, updateProjectConfig, updateStringList } from '../config/mutate.js';
|
|
25
|
-
import { buildFeishuPost
|
|
28
|
+
import { buildFeishuPost } from '../feishu/text.js';
|
|
26
29
|
import { ConfigHistoryStore } from '../state/config-history-store.js';
|
|
27
30
|
import { loadBridgeConfigFile } from '../config/load.js';
|
|
28
|
-
import { writeUtf8Atomic } from '../utils/fs.js';
|
|
29
31
|
import { expandHomePath } from '../utils/path.js';
|
|
30
32
|
import { canAccessGlobalCapability, canAccessProject, canAccessProjectCapability, describeMinimumRole, filterAccessibleProjects, resolveProjectAccessRole } from '../security/access.js';
|
|
31
33
|
import { adoptProjectSession as adoptSharedProjectSession, listBridgeSessions as listSharedBridgeSessions, switchProjectBinding as switchSharedProjectBinding } from '../control-plane/project-session.js';
|
|
32
|
-
import {
|
|
34
|
+
import { getProjectAuditDir, getProjectCacheDir, getProjectDownloadsDir, getProjectTempDir } from '../projects/paths.js';
|
|
33
35
|
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';
|
|
36
|
+
import { createReview } from '../collaboration/handoff.js';
|
|
37
|
+
import { classifyOperation, enforceTrustBoundary } from '../collaboration/trust.js';
|
|
39
38
|
import { HandoffStore } from '../state/handoff-store.js';
|
|
40
39
|
import { TrustStore } from '../state/trust-store.js';
|
|
41
40
|
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';
|
|
41
|
+
import { checkRunAlerts, formatAlert, DEFAULT_ALERT_RULES } from '../collaboration/proactive-alerts.js';
|
|
46
42
|
export class FeiqueService {
|
|
47
43
|
config;
|
|
48
44
|
feishuClient;
|
|
@@ -64,6 +60,10 @@ export class FeiqueService {
|
|
|
64
60
|
activeRuns = new Map();
|
|
65
61
|
runReplyTargets = new Map();
|
|
66
62
|
chatRateWindows = new Map();
|
|
63
|
+
/** Dedupe admin notifications for backend failover: one alert per (from→to) direction per process lifetime. */
|
|
64
|
+
failoverNotified = new Set();
|
|
65
|
+
/** Dedupe rejected-chat notifications: one reply + one admin alert per chat_id per process lifetime. */
|
|
66
|
+
rejectedChatNotified = new Set();
|
|
67
67
|
maintenanceTimer;
|
|
68
68
|
digestTimer;
|
|
69
69
|
configWatcher;
|
|
@@ -102,79 +102,14 @@ export class FeiqueService {
|
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
104
|
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;
|
|
105
|
+
return recoverRuntimeStateImpl(this);
|
|
117
106
|
}
|
|
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
107
|
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 };
|
|
108
|
+
const result = await reloadConfigImpl(this, configPath);
|
|
109
|
+
if (result.newConfig) {
|
|
110
|
+
this.config = result.newConfig;
|
|
150
111
|
}
|
|
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 };
|
|
112
|
+
return { ok: result.ok, ...(result.error ? { error: result.error } : {}), ...(result.changes ? { changes: result.changes } : {}) };
|
|
178
113
|
}
|
|
179
114
|
/**
|
|
180
115
|
* Watch config file for changes and auto-reload with validation.
|
|
@@ -247,120 +182,16 @@ export class FeiqueService {
|
|
|
247
182
|
this.digestTimer.unref?.();
|
|
248
183
|
}
|
|
249
184
|
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
|
-
}
|
|
185
|
+
return runDigestCycleImpl(this);
|
|
301
186
|
}
|
|
302
187
|
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;
|
|
188
|
+
return runMemoryMaintenanceImpl(this);
|
|
315
189
|
}
|
|
316
190
|
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 };
|
|
191
|
+
return runAuditMaintenanceImpl(this);
|
|
342
192
|
}
|
|
343
193
|
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 */ }
|
|
194
|
+
return runMaintenanceCycleImpl(this);
|
|
364
195
|
}
|
|
365
196
|
async handleIncomingMessage(context) {
|
|
366
197
|
this.currentMessageContext = context;
|
|
@@ -510,7 +341,7 @@ export class FeiqueService {
|
|
|
510
341
|
await this.handleGapsCommand(context);
|
|
511
342
|
return;
|
|
512
343
|
case 'prompt':
|
|
513
|
-
await this.handlePromptMessage(context, selectionKey, command.prompt
|
|
344
|
+
await this.handlePromptMessage(context, selectionKey, command.prompt);
|
|
514
345
|
return;
|
|
515
346
|
}
|
|
516
347
|
}
|
|
@@ -655,430 +486,7 @@ export class FeiqueService {
|
|
|
655
486
|
return this.runStateStore.listRuns();
|
|
656
487
|
}
|
|
657
488
|
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
|
-
}
|
|
489
|
+
return executePromptImpl(this, input);
|
|
1082
490
|
}
|
|
1083
491
|
async handleProjectCommand(context, selectionKey, alias, followupPrompt) {
|
|
1084
492
|
if (!alias) {
|
|
@@ -1095,7 +503,7 @@ export class FeiqueService {
|
|
|
1095
503
|
await this.sendTextReply(context.chat_id, `当前 chat_id 无权切换到项目 ${alias}。至少需要 ${describeMinimumRole('viewer')} 权限。`, context.message_id, context.text);
|
|
1096
504
|
return;
|
|
1097
505
|
}
|
|
1098
|
-
|
|
506
|
+
this.requireProject(alias); // throws if missing — validates the alias before switching
|
|
1099
507
|
const switched = await switchSharedProjectBinding(this.config, this.sessionStore, this.codexSessionIndex, {
|
|
1100
508
|
chatId: context.chat_id,
|
|
1101
509
|
actorId: context.actor_id,
|
|
@@ -1126,7 +534,7 @@ export class FeiqueService {
|
|
|
1126
534
|
await this.handleReadOnlyFollowupCommand(context, selectionKey, followupCommand, followupPrompt);
|
|
1127
535
|
return;
|
|
1128
536
|
}
|
|
1129
|
-
await this.handlePromptMessage(context, selectionKey, followupPrompt
|
|
537
|
+
await this.handlePromptMessage(context, selectionKey, followupPrompt);
|
|
1130
538
|
return;
|
|
1131
539
|
}
|
|
1132
540
|
await this.sendTextReply(context.chat_id, switched.text, context.message_id, context.text);
|
|
@@ -1175,13 +583,13 @@ export class FeiqueService {
|
|
|
1175
583
|
await this.handleAdminCommand(context, selectionKey, command);
|
|
1176
584
|
return;
|
|
1177
585
|
case 'prompt':
|
|
1178
|
-
await this.handlePromptMessage(context, selectionKey, followupPrompt
|
|
586
|
+
await this.handlePromptMessage(context, selectionKey, followupPrompt);
|
|
1179
587
|
return;
|
|
1180
588
|
default:
|
|
1181
|
-
await this.handlePromptMessage(context, selectionKey, followupPrompt
|
|
589
|
+
await this.handlePromptMessage(context, selectionKey, followupPrompt);
|
|
1182
590
|
}
|
|
1183
591
|
}
|
|
1184
|
-
async handlePromptMessage(context, selectionKey, rawPrompt
|
|
592
|
+
async handlePromptMessage(context, selectionKey, rawPrompt) {
|
|
1185
593
|
const prompt = normalizeIncomingText(rawPrompt) || (context.attachments.length > 0 ? '请结合这条飞书消息附带的多媒体信息继续处理。' : '');
|
|
1186
594
|
if (!prompt) {
|
|
1187
595
|
return;
|
|
@@ -1372,7 +780,6 @@ export class FeiqueService {
|
|
|
1372
780
|
}
|
|
1373
781
|
async handleSessionCommand(context, selectionKey, action, threadId) {
|
|
1374
782
|
const projectContext = await this.resolveProjectContext(context, selectionKey);
|
|
1375
|
-
const sessions = await this.sessionStore.listProjectSessions(projectContext.sessionKey, projectContext.projectAlias);
|
|
1376
783
|
const activeSessionId = (await this.sessionStore.getConversation(projectContext.sessionKey))?.projects[projectContext.projectAlias]?.thread_id;
|
|
1377
784
|
switch (action) {
|
|
1378
785
|
case 'list': {
|
|
@@ -1441,215 +848,39 @@ export class FeiqueService {
|
|
|
1441
848
|
// ── Direction 2: Knowledge Loop ──
|
|
1442
849
|
async handleLearnCommand(context, selectionKey, value) {
|
|
1443
850
|
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);
|
|
851
|
+
return handleLearnCommandImpl(this, context, projectContext, value);
|
|
1460
852
|
}
|
|
1461
853
|
async handleRecallCommand(context, selectionKey, query) {
|
|
1462
854
|
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);
|
|
855
|
+
return handleRecallCommandImpl(this, context, projectContext, query);
|
|
1466
856
|
}
|
|
1467
|
-
// ── Direction 3: Handoff & Review ──
|
|
1468
857
|
async handleHandoffCommand(context, selectionKey, summary) {
|
|
1469
858
|
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);
|
|
859
|
+
return handleHandoffCommandImpl(this, context, projectContext, summary);
|
|
1490
860
|
}
|
|
1491
861
|
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);
|
|
862
|
+
const projectContext = await this.resolveProjectContext(context, selectionKey);
|
|
863
|
+
return handlePickupCommandImpl(this, context, projectContext, id);
|
|
1523
864
|
}
|
|
1524
865
|
async handleReviewCommand(context, selectionKey) {
|
|
1525
866
|
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);
|
|
867
|
+
return handleReviewCommandImpl(this, context, projectContext);
|
|
1547
868
|
}
|
|
1548
869
|
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);
|
|
870
|
+
return handleApproveCommandImpl(this, context, comment);
|
|
1567
871
|
}
|
|
1568
872
|
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);
|
|
873
|
+
return handleRejectCommandImpl(this, context, reason);
|
|
1588
874
|
}
|
|
1589
|
-
// ── Direction 4: Insights ──
|
|
1590
875
|
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);
|
|
876
|
+
return handleInsightsCommandImpl(this, context);
|
|
1596
877
|
}
|
|
1597
|
-
// ── Direction 5: Trust ──
|
|
1598
878
|
async handleTrustCommand(context, selectionKey, action, level) {
|
|
1599
879
|
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);
|
|
880
|
+
return handleTrustCommandImpl(this, context, projectContext, action, level);
|
|
1641
881
|
}
|
|
1642
|
-
// ── Team Digest ──
|
|
1643
882
|
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);
|
|
883
|
+
return handleDigestCommandImpl(this, context);
|
|
1653
884
|
}
|
|
1654
885
|
// ── Proactive Alerts ──
|
|
1655
886
|
async checkAndSendAlerts(completedRun) {
|
|
@@ -1681,26 +912,12 @@ export class FeiqueService {
|
|
|
1681
912
|
}
|
|
1682
913
|
// ── Knowledge Gap Detection ──
|
|
1683
914
|
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);
|
|
915
|
+
return handleGapsCommandImpl(this, context);
|
|
1691
916
|
}
|
|
1692
917
|
// ── Direction 6: Timeline ──
|
|
1693
918
|
async handleTimelineCommand(context, selectionKey, projectArg) {
|
|
1694
919
|
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);
|
|
920
|
+
return handleTimelineCommandImpl(this, context, projectContext, projectArg);
|
|
1704
921
|
}
|
|
1705
922
|
async handleAdminCommand(context, selectionKey, command) {
|
|
1706
923
|
const runtimeConfigPath = this.runtimeControl?.configPath;
|
|
@@ -1758,37 +975,42 @@ export class FeiqueService {
|
|
|
1758
975
|
await this.sendTextReply(context.chat_id, this.buildProjectsAdminText(globalAdmin || serviceObserver ? undefined : new Set(projectOperatorAliases)), context.message_id, context.text);
|
|
1759
976
|
return;
|
|
1760
977
|
}
|
|
1761
|
-
if (command.action === 'add' || command.action === 'create') {
|
|
978
|
+
if (command.action === 'add' || command.action === 'create' || command.action === 'setup') {
|
|
1762
979
|
if (!(globalAdmin || globalConfigAdmin)) {
|
|
1763
980
|
await this.sendTextReply(context.chat_id, '当前 chat_id 无权动态接入项目。', context.message_id, context.text);
|
|
1764
981
|
return;
|
|
1765
982
|
}
|
|
1766
983
|
if (!command.alias || !command.value) {
|
|
1767
|
-
await this.sendTextReply(context.chat_id, command.action === '
|
|
984
|
+
await this.sendTextReply(context.chat_id, command.action === 'setup'
|
|
985
|
+
? '用法: /admin project setup <alias> <root>\n一键创建项目并将当前群设为 operator。'
|
|
986
|
+
: command.action === 'create' ? '用法: /admin project create <alias> <root>' : '用法: /admin project add <alias> <root>', context.message_id, context.text);
|
|
1768
987
|
return;
|
|
1769
988
|
}
|
|
1770
|
-
|
|
989
|
+
const isCreate = command.action === 'create' || command.action === 'setup';
|
|
990
|
+
if (isCreate && this.config.projects[command.alias]) {
|
|
1771
991
|
await this.sendTextReply(context.chat_id, `项目已存在: ${command.alias}`, context.message_id, context.text);
|
|
1772
992
|
return;
|
|
1773
993
|
}
|
|
1774
994
|
const resolvedRoot = path.resolve(expandHomePath(command.value));
|
|
1775
995
|
const snapshot = await this.snapshotConfigForAdminMutation(context, `project.${command.action}`, `${command.alias} -> ${resolvedRoot}`);
|
|
1776
|
-
if (
|
|
996
|
+
if (isCreate) {
|
|
1777
997
|
await createProjectAlias({ configPath: runtimeConfigPath, alias: command.alias, root: command.value });
|
|
1778
998
|
}
|
|
1779
999
|
else {
|
|
1780
1000
|
await bindProjectAlias({ configPath: runtimeConfigPath, alias: command.alias, root: command.value });
|
|
1781
1001
|
}
|
|
1002
|
+
// For setup: auto-add current chat as operator + viewer
|
|
1003
|
+
const autoOperator = command.action === 'setup' ? [context.chat_id] : [];
|
|
1782
1004
|
this.config.projects[command.alias] = {
|
|
1783
1005
|
root: resolvedRoot,
|
|
1784
1006
|
session_scope: 'chat',
|
|
1785
1007
|
mention_required: true,
|
|
1786
1008
|
knowledge_paths: [],
|
|
1787
1009
|
wiki_space_ids: [],
|
|
1788
|
-
viewer_chat_ids: [],
|
|
1789
|
-
operator_chat_ids: [],
|
|
1010
|
+
viewer_chat_ids: [...autoOperator],
|
|
1011
|
+
operator_chat_ids: [...autoOperator],
|
|
1790
1012
|
admin_chat_ids: [],
|
|
1791
|
-
notification_chat_ids: [],
|
|
1013
|
+
notification_chat_ids: [...autoOperator],
|
|
1792
1014
|
session_operator_chat_ids: [],
|
|
1793
1015
|
run_operator_chat_ids: [],
|
|
1794
1016
|
config_admin_chat_ids: [],
|
|
@@ -1798,7 +1020,27 @@ export class FeiqueService {
|
|
|
1798
1020
|
chat_rate_limit_window_seconds: 60,
|
|
1799
1021
|
chat_rate_limit_max_runs: 20,
|
|
1800
1022
|
};
|
|
1801
|
-
|
|
1023
|
+
// Persist the auto-added chat_ids to config file for setup
|
|
1024
|
+
if (command.action === 'setup') {
|
|
1025
|
+
await updateProjectConfig(runtimeConfigPath, command.alias, {
|
|
1026
|
+
viewer_chat_ids: autoOperator,
|
|
1027
|
+
operator_chat_ids: autoOperator,
|
|
1028
|
+
notification_chat_ids: autoOperator,
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
const replyLines = [
|
|
1032
|
+
`${isCreate ? '已创建并接入项目' : '已接入项目'}: ${command.alias}`,
|
|
1033
|
+
`根目录: ${resolvedRoot}`,
|
|
1034
|
+
];
|
|
1035
|
+
if (command.action === 'setup') {
|
|
1036
|
+
replyLines.push(`已自动将当前群设为 operator + viewer + notification`);
|
|
1037
|
+
replyLines.push('');
|
|
1038
|
+
replyLines.push('可在其他群执行以下命令添加权限:');
|
|
1039
|
+
replyLines.push(`/admin project set ${command.alias} operator_chat_ids +<chat_id>`);
|
|
1040
|
+
replyLines.push('');
|
|
1041
|
+
replyLines.push(`切换到此项目: /project ${command.alias}`);
|
|
1042
|
+
}
|
|
1043
|
+
await this.sendTextReply(context.chat_id, replyLines.join('\n'), context.message_id, context.text);
|
|
1802
1044
|
await this.appendAdminAudit({
|
|
1803
1045
|
type: `admin.project.${command.action}`,
|
|
1804
1046
|
chat_id: context.chat_id,
|
|
@@ -1806,8 +1048,9 @@ export class FeiqueService {
|
|
|
1806
1048
|
project_alias: command.alias,
|
|
1807
1049
|
root: resolvedRoot,
|
|
1808
1050
|
snapshot_id: snapshot.id,
|
|
1051
|
+
auto_operator: command.action === 'setup' ? context.chat_id : undefined,
|
|
1809
1052
|
});
|
|
1810
|
-
this.logger.info({ alias: command.alias, root: resolvedRoot, actorId: context.actor_id, created: command.action === '
|
|
1053
|
+
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
1054
|
return;
|
|
1812
1055
|
}
|
|
1813
1056
|
if (command.action === 'remove') {
|
|
@@ -1845,9 +1088,9 @@ export class FeiqueService {
|
|
|
1845
1088
|
await this.sendTextReply(context.chat_id, `当前 chat_id 无权修改项目 ${command.alias}。`, context.message_id, context.text);
|
|
1846
1089
|
return;
|
|
1847
1090
|
}
|
|
1848
|
-
const patch = this.
|
|
1091
|
+
const patch = parseProjectPatchImpl(this.config, command.field, command.value, command.alias);
|
|
1849
1092
|
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);
|
|
1093
|
+
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
1094
|
return;
|
|
1852
1095
|
}
|
|
1853
1096
|
const snapshot = await this.snapshotConfigForAdminMutation(context, 'project.set', `${command.alias}.${command.field}=${command.value}`);
|
|
@@ -1928,8 +1171,9 @@ export class FeiqueService {
|
|
|
1928
1171
|
return;
|
|
1929
1172
|
}
|
|
1930
1173
|
const normalized = name.toLowerCase();
|
|
1931
|
-
if (normalized
|
|
1932
|
-
|
|
1174
|
+
if (!getBackendDefinition(normalized)) {
|
|
1175
|
+
const known = listBackendNames().join(' | ');
|
|
1176
|
+
await this.sendTextReply(context.chat_id, `未知后端: ${name}\n可选值: ${known}`, context.message_id, context.text);
|
|
1933
1177
|
return;
|
|
1934
1178
|
}
|
|
1935
1179
|
const backendName = normalized;
|
|
@@ -1942,313 +1186,21 @@ export class FeiqueService {
|
|
|
1942
1186
|
conversation_key: projectContext.sessionKey,
|
|
1943
1187
|
backend: backendName,
|
|
1944
1188
|
});
|
|
1945
|
-
const label = backendName === 'claude' ? 'Claude Code' : 'Codex';
|
|
1189
|
+
const label = backendName === 'claude' ? 'Claude Code' : backendName === 'qwen' ? 'Qwen Code' : 'Codex';
|
|
1946
1190
|
await this.sendTextReply(context.chat_id, `项目 ${projectContext.projectAlias} 已切换到 ${label} 后端。\n下一条消息将使用 ${label} 执行。`, context.message_id, context.text);
|
|
1947
1191
|
}
|
|
1948
1192
|
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);
|
|
1193
|
+
const projectContext = await this.resolveProjectContext(context, selectionKey);
|
|
1194
|
+
return handleMemoryCommandImpl(this, context, projectContext, action, scope, value, filters);
|
|
1195
|
+
}
|
|
1196
|
+
async handleKnowledgeCommand(context, selectionKey, action, query) {
|
|
1197
|
+
const projectContext = await this.resolveProjectContext(context, selectionKey);
|
|
1198
|
+
const roots = await resolveKnowledgeRoots(projectContext.project);
|
|
1199
|
+
if (action === 'status') {
|
|
1200
|
+
const message = roots.length
|
|
1201
|
+
? [`项目: ${projectContext.projectAlias}`, '知识库目录:', ...roots.map((root) => `- ${root}`)].join('\n')
|
|
1202
|
+
: [`项目: ${projectContext.projectAlias}`, '当前没有可用知识库目录。', '可在项目配置中设置 knowledge_paths,或在项目根下提供 docs/README。'].join('\n');
|
|
1203
|
+
await this.sendTextReply(context.chat_id, message, context.message_id, context.text);
|
|
2252
1204
|
return;
|
|
2253
1205
|
}
|
|
2254
1206
|
if (!query) {
|
|
@@ -2282,449 +1234,19 @@ export class FeiqueService {
|
|
|
2282
1234
|
}
|
|
2283
1235
|
async handleDocCommand(context, selectionKey, action, value, extra) {
|
|
2284
1236
|
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);
|
|
1237
|
+
return handleDocCommandImpl(this, context, projectContext, action, value, extra);
|
|
2328
1238
|
}
|
|
2329
1239
|
async handleTaskCommand(context, selectionKey, action, value) {
|
|
2330
1240
|
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);
|
|
1241
|
+
return handleTaskCommandImpl(this, context, projectContext, action, value);
|
|
2400
1242
|
}
|
|
2401
1243
|
async handleBaseCommand(context, selectionKey, action, appToken, tableId, recordId, value) {
|
|
2402
1244
|
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);
|
|
1245
|
+
return handleBaseCommandImpl(this, context, projectContext, action, appToken, tableId, recordId, value);
|
|
2468
1246
|
}
|
|
2469
1247
|
async handleWikiCommand(context, selectionKey, action, value, extra, target, role) {
|
|
2470
1248
|
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);
|
|
1249
|
+
return handleWikiCommandImpl(this, context, projectContext, action, value, extra, target, role);
|
|
2728
1250
|
}
|
|
2729
1251
|
async buildProjectsText(selectionKey, chatId) {
|
|
2730
1252
|
const selected = await this.resolveProjectAlias(selectionKey);
|
|
@@ -2787,11 +1309,11 @@ export class FeiqueService {
|
|
|
2787
1309
|
const session = conversation?.projects[projectAlias];
|
|
2788
1310
|
const sessionCount = Object.keys(session?.sessions ?? {}).length;
|
|
2789
1311
|
const isExecutableRun = activeRun ? isExecutionRunStatus(activeRun.status) : false;
|
|
2790
|
-
const includeActions = this.
|
|
1312
|
+
const includeActions = supportsInteractiveCardActions(this.config);
|
|
2791
1313
|
const actionChatId = conversation?.chat_id ?? activeRun?.chat_id ?? fallbackChatId;
|
|
2792
1314
|
return buildStatusCard({
|
|
2793
1315
|
title: '当前会话状态',
|
|
2794
|
-
summary:
|
|
1316
|
+
summary: buildRunStatusSummary(session?.last_response_excerpt, activeRun),
|
|
2795
1317
|
projectAlias,
|
|
2796
1318
|
sessionId: session?.thread_id,
|
|
2797
1319
|
runStatus: activeRun?.status,
|
|
@@ -2962,32 +1484,6 @@ export class FeiqueService {
|
|
|
2962
1484
|
shouldRequireMention(project) {
|
|
2963
1485
|
return project.mention_required || this.config.security.require_group_mentions;
|
|
2964
1486
|
}
|
|
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
1487
|
async cancelActiveRun(queueKey, reason) {
|
|
2992
1488
|
const live = this.activeRuns.get(queueKey);
|
|
2993
1489
|
if (live) {
|
|
@@ -3017,112 +1513,7 @@ export class FeiqueService {
|
|
|
3017
1513
|
return terminateProcess(persisted.pid, 'SIGTERM');
|
|
3018
1514
|
}
|
|
3019
1515
|
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 ?? '暂无会话摘要。';
|
|
1516
|
+
return scheduleProjectExecutionImpl(this, projectContext, metadata, task);
|
|
3126
1517
|
}
|
|
3127
1518
|
isAdminChat(chatId) {
|
|
3128
1519
|
return this.config.security.admin_chat_ids.includes(chatId);
|
|
@@ -3236,9 +1627,6 @@ export class FeiqueService {
|
|
|
3236
1627
|
canMutateRuntimeConfig(chatId) {
|
|
3237
1628
|
return canAccessGlobalCapability(this.config, chatId, 'config:mutate');
|
|
3238
1629
|
}
|
|
3239
|
-
canReadConfigHistory(chatId) {
|
|
3240
|
-
return canAccessGlobalCapability(this.config, chatId, 'config:history') || this.canMutateRuntimeConfig(chatId);
|
|
3241
|
-
}
|
|
3242
1630
|
canAccessAdminCommand(command, currentProjectAlias, authorizedProjectAliases, operatorProjectAliases, globalCapabilities) {
|
|
3243
1631
|
if (command.resource === 'project') {
|
|
3244
1632
|
if (command.action === 'list') {
|
|
@@ -3267,56 +1655,7 @@ export class FeiqueService {
|
|
|
3267
1655
|
return true;
|
|
3268
1656
|
}
|
|
3269
1657
|
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);
|
|
1658
|
+
return handleAdminConfigCommandImpl(this, context, command);
|
|
3320
1659
|
}
|
|
3321
1660
|
async snapshotConfigForAdminMutation(context, action, summary) {
|
|
3322
1661
|
if (!this.runtimeControl?.configPath) {
|
|
@@ -3343,65 +1682,6 @@ export class FeiqueService {
|
|
|
3343
1682
|
replaceObject(this.config.feishu, nextConfig.feishu);
|
|
3344
1683
|
replaceProjects(this.config.projects, nextConfig.projects);
|
|
3345
1684
|
}
|
|
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
1685
|
resolveProjectDownloadDir(projectAlias, project) {
|
|
3406
1686
|
return getProjectDownloadsDir(this.config.storage.dir, projectAlias, project);
|
|
3407
1687
|
}
|
|
@@ -3425,28 +1705,6 @@ export class FeiqueService {
|
|
|
3425
1705
|
catch { /* best-effort */ }
|
|
3426
1706
|
}
|
|
3427
1707
|
}
|
|
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
1708
|
checkAndConsumeChatRateLimit(projectAlias, project, chatId) {
|
|
3451
1709
|
const windowMs = project.chat_rate_limit_window_seconds * 1000;
|
|
3452
1710
|
const maxRuns = project.chat_rate_limit_max_runs;
|
|
@@ -3500,12 +1758,108 @@ export class FeiqueService {
|
|
|
3500
1758
|
}
|
|
3501
1759
|
this.config.feishu.allowed_chat_ids = values;
|
|
3502
1760
|
}
|
|
3503
|
-
resolveProjectRoot(project) {
|
|
3504
|
-
return path.resolve(project.root);
|
|
3505
|
-
}
|
|
3506
1761
|
resolveBackendByName(projectAlias, sessionOverride) {
|
|
3507
1762
|
return resolveProjectBackendWithOverride(this.config, projectAlias, sessionOverride, this.codexSessionIndex);
|
|
3508
1763
|
}
|
|
1764
|
+
/**
|
|
1765
|
+
* Called when resolveProjectBackendWithFailover returns a non-null failover.
|
|
1766
|
+
* Responsibilities:
|
|
1767
|
+
* - Log a warning with structured context
|
|
1768
|
+
* - Emit an audit event
|
|
1769
|
+
* - Notify admin chats (deduped by from→to direction for the process lifetime)
|
|
1770
|
+
* - Send a user-visible notice into the current chat so the user knows
|
|
1771
|
+
* why their run is on a different backend than expected
|
|
1772
|
+
*/
|
|
1773
|
+
async handleBackendFailover(chatId, projectAlias, runId, info) {
|
|
1774
|
+
this.logger.warn({ projectAlias, runId, from: info.from, to: info.to, reason: info.reason }, 'Backend failover: primary probe failed, switched to alternate');
|
|
1775
|
+
try {
|
|
1776
|
+
await this.auditLog.append({
|
|
1777
|
+
type: 'backend.failover',
|
|
1778
|
+
project_alias: projectAlias,
|
|
1779
|
+
run_id: runId,
|
|
1780
|
+
from: info.from,
|
|
1781
|
+
to: info.to,
|
|
1782
|
+
reason: info.reason,
|
|
1783
|
+
});
|
|
1784
|
+
}
|
|
1785
|
+
catch { /* best-effort */ }
|
|
1786
|
+
const userNotice = `⚠️ ${info.from} 不可用,已临时切换到 ${info.to} 运行本次请求。\n原因: ${info.reason}`;
|
|
1787
|
+
try {
|
|
1788
|
+
await this.feishuClient.sendText(chatId, userNotice);
|
|
1789
|
+
}
|
|
1790
|
+
catch { /* best-effort */ }
|
|
1791
|
+
const dedupeKey = `${info.from}->${info.to}`;
|
|
1792
|
+
if (this.failoverNotified.has(dedupeKey))
|
|
1793
|
+
return;
|
|
1794
|
+
this.failoverNotified.add(dedupeKey);
|
|
1795
|
+
const adminChatIds = this.config.security.admin_chat_ids ?? [];
|
|
1796
|
+
if (adminChatIds.length === 0)
|
|
1797
|
+
return;
|
|
1798
|
+
const adminText = `🔁 Backend failover 已触发\n\n` +
|
|
1799
|
+
`方向: ${info.from} → ${info.to}\n` +
|
|
1800
|
+
`项目: ${projectAlias}\n` +
|
|
1801
|
+
`原因: ${info.reason}\n\n` +
|
|
1802
|
+
`后续同方向的切换不会重复通知,直到服务重启。`;
|
|
1803
|
+
for (const adminChat of adminChatIds) {
|
|
1804
|
+
try {
|
|
1805
|
+
await this.feishuClient.sendText(adminChat, adminText);
|
|
1806
|
+
}
|
|
1807
|
+
catch { /* best-effort */ }
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
/**
|
|
1811
|
+
* Called by the transport layer when an incoming chat is rejected by the
|
|
1812
|
+
* allowlist (`feishu.allowed_chat_ids` / `allowed_group_ids`).
|
|
1813
|
+
*
|
|
1814
|
+
* Replaces the previous "silent drop" behavior with a pairing-style
|
|
1815
|
+
* experience: tell the user what their chat_id is so an admin can add it,
|
|
1816
|
+
* notify admins that a new chat is knocking, and record the rejection in
|
|
1817
|
+
* the audit log. Deduped per chat_id for the process lifetime so repeated
|
|
1818
|
+
* attempts from the same unauthorized chat do not spam either side.
|
|
1819
|
+
*
|
|
1820
|
+
* This is best-effort: any send failure is swallowed. We never want a
|
|
1821
|
+
* rejection flow to throw out of the transport dispatcher.
|
|
1822
|
+
*/
|
|
1823
|
+
async handleRejectedChat(chatId, chatType) {
|
|
1824
|
+
if (this.rejectedChatNotified.has(chatId))
|
|
1825
|
+
return;
|
|
1826
|
+
this.rejectedChatNotified.add(chatId);
|
|
1827
|
+
try {
|
|
1828
|
+
await this.auditLog.append({
|
|
1829
|
+
type: 'chat.rejected',
|
|
1830
|
+
chat_id: chatId,
|
|
1831
|
+
chat_type: chatType,
|
|
1832
|
+
});
|
|
1833
|
+
}
|
|
1834
|
+
catch { /* best-effort */ }
|
|
1835
|
+
const listHint = chatType === 'group'
|
|
1836
|
+
? '`feishu.allowed_group_ids`(群聊)'
|
|
1837
|
+
: '`feishu.allowed_chat_ids`(私聊)';
|
|
1838
|
+
const userText = `抱歉,该 chat 未被授权访问本 bot。\n` +
|
|
1839
|
+
`你的 chat_id: ${chatId}\n\n` +
|
|
1840
|
+
`请联系管理员并附上这个 id,请求加入 ${listHint}。`;
|
|
1841
|
+
try {
|
|
1842
|
+
await this.feishuClient.sendText(chatId, userText);
|
|
1843
|
+
}
|
|
1844
|
+
catch { /* best-effort */ }
|
|
1845
|
+
const adminChatIds = this.config.security.admin_chat_ids ?? [];
|
|
1846
|
+
if (adminChatIds.length === 0)
|
|
1847
|
+
return;
|
|
1848
|
+
const adminCommand = chatType === 'group'
|
|
1849
|
+
? `/admin group add ${chatId}`
|
|
1850
|
+
: `/admin chat add ${chatId}`;
|
|
1851
|
+
const adminText = `🔔 新 chat 请求接入\n\n` +
|
|
1852
|
+
`chat_id: ${chatId}\n` +
|
|
1853
|
+
`类型: ${chatType}\n\n` +
|
|
1854
|
+
`如需授权,运行:\n${adminCommand}\n\n` +
|
|
1855
|
+
`后续来自同一 chat 的请求不会重复通知,直到服务重启。`;
|
|
1856
|
+
for (const adminChat of adminChatIds) {
|
|
1857
|
+
try {
|
|
1858
|
+
await this.feishuClient.sendText(adminChat, adminText);
|
|
1859
|
+
}
|
|
1860
|
+
catch { /* best-effort */ }
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
3509
1863
|
async enforceSessionHistoryLimit(conversationKey, projectAlias) {
|
|
3510
1864
|
const sessions = await this.sessionStore.listProjectSessions(conversationKey, projectAlias);
|
|
3511
1865
|
const overflow = sessions.slice(this.config.service.session_history_limit);
|
|
@@ -3522,11 +1876,11 @@ export class FeiqueService {
|
|
|
3522
1876
|
mentionPrefix = `<at user_id="${actor.actor_id}">${displayName}</at>\n`;
|
|
3523
1877
|
}
|
|
3524
1878
|
const bodyWithMention = mentionPrefix ? mentionPrefix + body : body;
|
|
3525
|
-
const title =
|
|
1879
|
+
const title = buildReplyTitle(sanitizeUserVisibleReply(body));
|
|
3526
1880
|
// Card mode uses replyToMessageId for threading — @mention tags render as
|
|
3527
1881
|
// literal text inside card JSON, so use the clean body for cards.
|
|
3528
|
-
const formattedBodyClean =
|
|
3529
|
-
const formattedBodyWithMention =
|
|
1882
|
+
const formattedBodyClean = sanitizeUserVisibleReply(formatQuotedReply(body, originalText));
|
|
1883
|
+
const formattedBodyWithMention = sanitizeUserVisibleReply(formatQuotedReply(bodyWithMention, originalText));
|
|
3530
1884
|
if (this.config.service.reply_mode === 'card') {
|
|
3531
1885
|
const card = buildMessageCard({
|
|
3532
1886
|
title,
|
|
@@ -3568,7 +1922,7 @@ export class FeiqueService {
|
|
|
3568
1922
|
return response;
|
|
3569
1923
|
}
|
|
3570
1924
|
if (this.config.service.reply_quote_user_message && replyToMessageId) {
|
|
3571
|
-
const response = await this.feishuClient.sendText(chatId,
|
|
1925
|
+
const response = await this.feishuClient.sendText(chatId, sanitizeUserVisibleReply(bodyWithMention), { replyToMessageId });
|
|
3572
1926
|
await this.auditLog.append({
|
|
3573
1927
|
type: 'message.replied',
|
|
3574
1928
|
chat_id: chatId,
|
|
@@ -3594,10 +1948,10 @@ export class FeiqueService {
|
|
|
3594
1948
|
return this.feishuClient.sendCard(chatId, card);
|
|
3595
1949
|
}
|
|
3596
1950
|
async sendRunLifecycleReply(input) {
|
|
3597
|
-
const lifecycleMode = this.
|
|
1951
|
+
const lifecycleMode = resolveRunLifecycleReplyMode(this.config);
|
|
3598
1952
|
const lifecycleReplyOptions = input.replyToMessageId ? { replyToMessageId: input.replyToMessageId } : undefined;
|
|
3599
1953
|
if (lifecycleMode === 'card') {
|
|
3600
|
-
const card =
|
|
1954
|
+
const card = buildRunLifecycleCard({
|
|
3601
1955
|
title: input.title,
|
|
3602
1956
|
body: input.body,
|
|
3603
1957
|
projectAlias: input.projectAlias,
|
|
@@ -3618,8 +1972,8 @@ export class FeiqueService {
|
|
|
3618
1972
|
return response;
|
|
3619
1973
|
}
|
|
3620
1974
|
if (lifecycleMode === 'post') {
|
|
3621
|
-
const postBody =
|
|
3622
|
-
const title =
|
|
1975
|
+
const postBody = sanitizeUserVisibleReply(formatQuotedReply(input.body, input.originalText));
|
|
1976
|
+
const title = buildReplyTitle(postBody);
|
|
3623
1977
|
const post = buildFeishuPost(title, postBody);
|
|
3624
1978
|
const response = lifecycleReplyOptions
|
|
3625
1979
|
? await this.feishuClient.sendPost(input.chatId, post, lifecycleReplyOptions)
|
|
@@ -3635,7 +1989,7 @@ export class FeiqueService {
|
|
|
3635
1989
|
return response;
|
|
3636
1990
|
}
|
|
3637
1991
|
const response = lifecycleReplyOptions
|
|
3638
|
-
? await this.feishuClient.sendText(input.chatId,
|
|
1992
|
+
? await this.feishuClient.sendText(input.chatId, sanitizeUserVisibleReply(formatQuotedReply(input.body, input.originalText)), lifecycleReplyOptions)
|
|
3639
1993
|
: await this.sendTextReply(input.chatId, input.body, input.replyToMessageId, input.originalText);
|
|
3640
1994
|
await this.auditLog.append({
|
|
3641
1995
|
type: 'codex.run.replied',
|
|
@@ -3651,20 +2005,20 @@ export class FeiqueService {
|
|
|
3651
2005
|
if (queued) {
|
|
3652
2006
|
return {
|
|
3653
2007
|
title: '已加入排队',
|
|
3654
|
-
body:
|
|
2008
|
+
body: buildAcknowledgedRunReply(projectAlias, '排队中', queued.detail, mode),
|
|
3655
2009
|
runStatus: 'queued',
|
|
3656
2010
|
runPhase: '排队中',
|
|
3657
2011
|
};
|
|
3658
2012
|
}
|
|
3659
2013
|
return {
|
|
3660
2014
|
title: '已接收请求',
|
|
3661
|
-
body:
|
|
2015
|
+
body: buildAcknowledgedRunReply(projectAlias, '已接收', '已收到你的消息,正在准备处理。', mode),
|
|
3662
2016
|
runStatus: 'running',
|
|
3663
2017
|
runPhase: '已接收',
|
|
3664
2018
|
};
|
|
3665
2019
|
}
|
|
3666
2020
|
async sendInitialRunLifecycleReply(input) {
|
|
3667
|
-
const lifecycleMode = this.
|
|
2021
|
+
const lifecycleMode = resolveRunLifecycleReplyMode(this.config);
|
|
3668
2022
|
const draft = this.buildInitialRunLifecycleReply(input.projectAlias, input.queued, lifecycleMode);
|
|
3669
2023
|
try {
|
|
3670
2024
|
const response = await this.sendRunLifecycleReply({
|
|
@@ -3684,7 +2038,7 @@ export class FeiqueService {
|
|
|
3684
2038
|
this.logger.warn({ error, runId: input.runId, projectAlias: input.projectAlias }, 'Failed to send initial lifecycle reply');
|
|
3685
2039
|
}
|
|
3686
2040
|
}
|
|
3687
|
-
async rememberRunReplyTarget(runId, response, mode = this.
|
|
2041
|
+
async rememberRunReplyTarget(runId, response, mode = resolveRunLifecycleReplyMode(this.config)) {
|
|
3688
2042
|
this.runReplyTargets.set(runId, {
|
|
3689
2043
|
messageId: response.message_id,
|
|
3690
2044
|
mode,
|
|
@@ -3696,7 +2050,7 @@ export class FeiqueService {
|
|
|
3696
2050
|
return;
|
|
3697
2051
|
}
|
|
3698
2052
|
const label = backendLabel ?? 'AI';
|
|
3699
|
-
const body =
|
|
2053
|
+
const body = buildAcknowledgedRunReply(projectAlias, '处理中', '桥接器已开始处理你的请求。', target.mode);
|
|
3700
2054
|
await this.updateRunLifecycleReply({
|
|
3701
2055
|
chatId,
|
|
3702
2056
|
projectAlias,
|
|
@@ -3714,7 +2068,7 @@ export class FeiqueService {
|
|
|
3714
2068
|
}
|
|
3715
2069
|
const label = backendLabel ?? 'AI';
|
|
3716
2070
|
const body = [
|
|
3717
|
-
|
|
2071
|
+
buildAcknowledgedRunReply(input.projectAlias, '处理中', '桥接器正在持续处理你的请求。', target.mode),
|
|
3718
2072
|
'最新进展:',
|
|
3719
2073
|
progress,
|
|
3720
2074
|
]
|
|
@@ -3766,10 +2120,10 @@ export class FeiqueService {
|
|
|
3766
2120
|
if (!target?.messageId) {
|
|
3767
2121
|
return false;
|
|
3768
2122
|
}
|
|
3769
|
-
const sanitizedBody =
|
|
2123
|
+
const sanitizedBody = sanitizeUserVisibleReply(input.body);
|
|
3770
2124
|
if (target.mode === 'card') {
|
|
3771
|
-
const includeActions = input.runStatus === 'success' && this.
|
|
3772
|
-
await this.feishuClient.updateCard(target.messageId,
|
|
2125
|
+
const includeActions = input.runStatus === 'success' && supportsInteractiveCardActions(this.config) && input.sessionKey !== undefined;
|
|
2126
|
+
await this.feishuClient.updateCard(target.messageId, buildRunLifecycleCard({
|
|
3773
2127
|
title: input.title,
|
|
3774
2128
|
body: input.body,
|
|
3775
2129
|
projectAlias: input.projectAlias,
|
|
@@ -3804,7 +2158,7 @@ export class FeiqueService {
|
|
|
3804
2158
|
}));
|
|
3805
2159
|
}
|
|
3806
2160
|
else if (target.mode === 'post') {
|
|
3807
|
-
const title =
|
|
2161
|
+
const title = buildReplyTitle(sanitizedBody);
|
|
3808
2162
|
await this.feishuClient.updatePost(target.messageId, buildFeishuPost(title, sanitizedBody));
|
|
3809
2163
|
}
|
|
3810
2164
|
else {
|
|
@@ -3830,332 +2184,9 @@ export class FeiqueService {
|
|
|
3830
2184
|
});
|
|
3831
2185
|
return true;
|
|
3832
2186
|
}
|
|
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
2187
|
}
|
|
2188
|
+
// Module-level helpers extracted to service-utils.ts (β step 1).
|
|
2189
|
+
// Re-exported here for backward compatibility (src/mcp/server.ts and
|
|
2190
|
+
// src/index.ts depend on these names being importable from this module).
|
|
2191
|
+
export { buildQueueKey, buildProjectRootQueueKey } from './service-utils.js';
|
|
4161
2192
|
//# sourceMappingURL=service.js.map
|