feique 1.3.3 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +3 -2
- package/README.md +3 -2
- package/dist/backend/claude.js +28 -54
- package/dist/backend/claude.js.map +1 -1
- package/dist/backend/factory.d.ts +28 -0
- package/dist/backend/factory.js +61 -0
- package/dist/backend/factory.js.map +1 -1
- package/dist/backend/probe.d.ts +29 -0
- package/dist/backend/probe.js +99 -0
- package/dist/backend/probe.js.map +1 -0
- package/dist/bridge/admin-config.d.ts +47 -0
- package/dist/bridge/admin-config.js +141 -0
- package/dist/bridge/admin-config.js.map +1 -0
- package/dist/bridge/collab-commands.d.ts +42 -0
- package/dist/bridge/collab-commands.js +254 -0
- package/dist/bridge/collab-commands.js.map +1 -0
- package/dist/bridge/commands.d.ts +1 -1
- package/dist/bridge/commands.js +3 -0
- package/dist/bridge/commands.js.map +1 -1
- package/dist/bridge/feishu-commands.d.ts +27 -0
- package/dist/bridge/feishu-commands.js +462 -0
- package/dist/bridge/feishu-commands.js.map +1 -0
- package/dist/bridge/lifecycle.d.ts +46 -0
- package/dist/bridge/lifecycle.js +228 -0
- package/dist/bridge/lifecycle.js.map +1 -0
- package/dist/bridge/memory-commands.d.ts +26 -0
- package/dist/bridge/memory-commands.js +330 -0
- package/dist/bridge/memory-commands.js.map +1 -0
- package/dist/bridge/reply-builders.d.ts +30 -0
- package/dist/bridge/reply-builders.js +72 -0
- package/dist/bridge/reply-builders.js.map +1 -0
- package/dist/bridge/run-pipeline.d.ts +86 -0
- package/dist/bridge/run-pipeline.js +442 -0
- package/dist/bridge/run-pipeline.js.map +1 -0
- package/dist/bridge/run-scheduler.d.ts +47 -0
- package/dist/bridge/run-scheduler.js +121 -0
- package/dist/bridge/run-scheduler.js.map +1 -0
- package/dist/bridge/service-utils.d.ts +47 -0
- package/dist/bridge/service-utils.js +309 -0
- package/dist/bridge/service-utils.js.map +1 -0
- package/dist/bridge/service.d.ts +114 -66
- package/dist/bridge/service.js +225 -2196
- package/dist/bridge/service.js.map +1 -1
- package/dist/config/load.js +1 -1
- package/dist/config/load.js.map +1 -1
- package/dist/config/paths.js +1 -20
- package/dist/config/paths.js.map +1 -1
- package/dist/config/schema.d.ts +3 -0
- package/dist/config/schema.js +3 -1
- package/dist/config/schema.js.map +1 -1
- package/dist/feishu/long-connection.js +1 -0
- package/dist/feishu/long-connection.js.map +1 -1
- package/dist/feishu/webhook.js +1 -0
- package/dist/feishu/webhook.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { BridgeConfig } from '../config/schema.js';
|
|
2
|
+
/**
|
|
3
|
+
* Pure reply / card builders pulled out of FeiqueService.
|
|
4
|
+
*
|
|
5
|
+
* Nothing in this module touches instance state — they are all pure
|
|
6
|
+
* functions of their inputs (plus `config` for the two functions that
|
|
7
|
+
* consult service settings). The stateful reply orchestration
|
|
8
|
+
* (runReplyTargets map, sendTextReply, updateRunLifecycleReply) stays
|
|
9
|
+
* on FeiqueService for now; that's a future β step.
|
|
10
|
+
*/
|
|
11
|
+
export declare function formatQuotedReply(body: string, _originalText?: string): string;
|
|
12
|
+
export declare function buildReplyTitle(body: string): string;
|
|
13
|
+
export declare function sanitizeUserVisibleReply(body: string): string;
|
|
14
|
+
export declare function stripLifecycleMetadata(body: string): string;
|
|
15
|
+
export declare function supportsInteractiveCardActions(config: BridgeConfig): boolean;
|
|
16
|
+
export declare function resolveRunLifecycleReplyMode(config: BridgeConfig): BridgeConfig['service']['reply_mode'];
|
|
17
|
+
export interface RunLifecycleCardInput {
|
|
18
|
+
title: string;
|
|
19
|
+
body: string;
|
|
20
|
+
projectAlias: string;
|
|
21
|
+
runStatus?: string;
|
|
22
|
+
runPhase?: string;
|
|
23
|
+
cardSummary?: string;
|
|
24
|
+
includeActions?: boolean;
|
|
25
|
+
rerunPayload?: Record<string, unknown>;
|
|
26
|
+
newSessionPayload?: Record<string, unknown>;
|
|
27
|
+
statusPayload?: Record<string, unknown>;
|
|
28
|
+
cancelPayload?: Record<string, unknown>;
|
|
29
|
+
}
|
|
30
|
+
export declare function buildRunLifecycleCard(input: RunLifecycleCardInput): Record<string, unknown>;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { buildMessageCard, buildStatusCard } from '../feishu/cards.js';
|
|
2
|
+
import { truncateForFeishuCard } from '../feishu/text.js';
|
|
3
|
+
import { truncateExcerpt } from './service-utils.js';
|
|
4
|
+
/**
|
|
5
|
+
* Pure reply / card builders pulled out of FeiqueService.
|
|
6
|
+
*
|
|
7
|
+
* Nothing in this module touches instance state — they are all pure
|
|
8
|
+
* functions of their inputs (plus `config` for the two functions that
|
|
9
|
+
* consult service settings). The stateful reply orchestration
|
|
10
|
+
* (runReplyTargets map, sendTextReply, updateRunLifecycleReply) stays
|
|
11
|
+
* on FeiqueService for now; that's a future β step.
|
|
12
|
+
*/
|
|
13
|
+
export function formatQuotedReply(body, _originalText) {
|
|
14
|
+
return body;
|
|
15
|
+
}
|
|
16
|
+
export function buildReplyTitle(body) {
|
|
17
|
+
const firstLine = body
|
|
18
|
+
.split(/\r?\n/)
|
|
19
|
+
.map((line) => line.trim())
|
|
20
|
+
.find(Boolean);
|
|
21
|
+
return truncateExcerpt(firstLine ?? '飞鹊 (Feique)', 40);
|
|
22
|
+
}
|
|
23
|
+
export function sanitizeUserVisibleReply(body) {
|
|
24
|
+
return body
|
|
25
|
+
.split(/\r?\n/)
|
|
26
|
+
.filter((line) => !/^(运行|当前运行|阻塞运行|run[_ -]?id|session[_ -]?id|conversation[_ -]?key|chat[_ -]?id|tenant[_ -]?key|project[_ -]?root|pid):/i.test(line.trim()))
|
|
27
|
+
.join('\n')
|
|
28
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
29
|
+
.trim();
|
|
30
|
+
}
|
|
31
|
+
export function stripLifecycleMetadata(body) {
|
|
32
|
+
return body
|
|
33
|
+
.split(/\r?\n/)
|
|
34
|
+
.filter((line) => !/^(项目|处理状态|会话|当前会话|已保存会话数):/.test(line.trim()))
|
|
35
|
+
.join('\n')
|
|
36
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
37
|
+
.trim();
|
|
38
|
+
}
|
|
39
|
+
export function supportsInteractiveCardActions(config) {
|
|
40
|
+
return config.feishu.transport === 'webhook';
|
|
41
|
+
}
|
|
42
|
+
export function resolveRunLifecycleReplyMode(config) {
|
|
43
|
+
if (config.service.reply_mode === 'post') {
|
|
44
|
+
return 'card';
|
|
45
|
+
}
|
|
46
|
+
return config.service.reply_mode;
|
|
47
|
+
}
|
|
48
|
+
export function buildRunLifecycleCard(input) {
|
|
49
|
+
const sanitizedBody = sanitizeUserVisibleReply(input.body);
|
|
50
|
+
if (input.includeActions) {
|
|
51
|
+
return buildStatusCard({
|
|
52
|
+
title: input.title,
|
|
53
|
+
summary: input.cardSummary ?? truncateForFeishuCard(stripLifecycleMetadata(sanitizedBody)),
|
|
54
|
+
projectAlias: input.projectAlias,
|
|
55
|
+
runStatus: input.runStatus,
|
|
56
|
+
runPhase: input.runPhase,
|
|
57
|
+
includeActions: true,
|
|
58
|
+
rerunPayload: input.rerunPayload,
|
|
59
|
+
newSessionPayload: input.newSessionPayload,
|
|
60
|
+
statusPayload: input.statusPayload,
|
|
61
|
+
cancelPayload: input.cancelPayload,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
return buildMessageCard({
|
|
65
|
+
title: input.title,
|
|
66
|
+
body: stripLifecycleMetadata(sanitizedBody),
|
|
67
|
+
status: input.runStatus,
|
|
68
|
+
phase: input.runPhase,
|
|
69
|
+
projectAlias: input.projectAlias,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=reply-builders.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reply-builders.js","sourceRoot":"","sources":["../../src/bridge/reply-builders.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { BridgeConfig, ProjectConfig } from '../config/schema.js';
|
|
2
|
+
import type { IncomingMessageContext } from './types.js';
|
|
3
|
+
import type { Logger } from '../logging.js';
|
|
4
|
+
import type { FeishuClient } from '../feishu/client.js';
|
|
5
|
+
import type { AuditLog } from '../state/audit-log.js';
|
|
6
|
+
import type { SessionStore } from '../state/session-store.js';
|
|
7
|
+
import type { MemoryStore } from '../state/memory-store.js';
|
|
8
|
+
import type { RunStateStore } from '../state/run-state-store.js';
|
|
9
|
+
import type { TrustStore } from '../state/trust-store.js';
|
|
10
|
+
import type { MetricsRegistry } from '../observability/metrics.js';
|
|
11
|
+
import type { Backend, BackendName } from '../backend/types.js';
|
|
12
|
+
import type { CodexSessionIndex } from '../codex/session-index.js';
|
|
13
|
+
import type { FailoverInfo } from '../backend/factory.js';
|
|
14
|
+
import { retrieveMemoryContext } from '../memory/retrieve.js';
|
|
15
|
+
import type { RunState } from '../state/run-state-store.js';
|
|
16
|
+
import type { ActiveRunHandle } from './service.js';
|
|
17
|
+
/**
|
|
18
|
+
* Subset of FeiqueService that the executePrompt pipeline needs.
|
|
19
|
+
*
|
|
20
|
+
* This is the largest host interface in feique because executePrompt is
|
|
21
|
+
* the central run-execution pipeline that touches almost everything in
|
|
22
|
+
* the service: stores, reply rendering, run state, audit, metrics, and
|
|
23
|
+
* collaboration features.
|
|
24
|
+
*
|
|
25
|
+
* Splitting it further into per-phase functions is a future refactor;
|
|
26
|
+
* for now we move the whole 470-line pipeline into a single free
|
|
27
|
+
* function so service.ts can shed the bulk.
|
|
28
|
+
*/
|
|
29
|
+
export interface PipelineHost {
|
|
30
|
+
readonly config: BridgeConfig;
|
|
31
|
+
readonly logger: Logger;
|
|
32
|
+
readonly feishuClient: FeishuClient;
|
|
33
|
+
readonly auditLog: AuditLog;
|
|
34
|
+
readonly sessionStore: SessionStore;
|
|
35
|
+
readonly memoryStore: MemoryStore;
|
|
36
|
+
readonly runStateStore: RunStateStore;
|
|
37
|
+
readonly trustStore: TrustStore;
|
|
38
|
+
readonly metrics?: MetricsRegistry;
|
|
39
|
+
readonly codexSessionIndex: CodexSessionIndex;
|
|
40
|
+
readonly activeRuns: Map<string, ActiveRunHandle>;
|
|
41
|
+
readonly runReplyTargets: Map<string, unknown>;
|
|
42
|
+
resolveBackendByName(projectAlias: string, sessionOverride?: BackendName): Backend;
|
|
43
|
+
buildBridgePrompt(projectAlias: string, project: ProjectConfig, incomingMessage: IncomingMessageContext, effectivePrompt: string, memoryContext: Awaited<ReturnType<typeof retrieveMemoryContext>>): Promise<string>;
|
|
44
|
+
appendProjectAuditEvent(projectAlias: string, project: ProjectConfig, event: {
|
|
45
|
+
type: string;
|
|
46
|
+
[key: string]: unknown;
|
|
47
|
+
}): Promise<void>;
|
|
48
|
+
resolveProjectTempDir(projectAlias: string, project: ProjectConfig): string;
|
|
49
|
+
resolveProjectCacheDir(projectAlias: string, project: ProjectConfig): string;
|
|
50
|
+
handleBackendFailover(chatId: string, projectAlias: string, runId: string, info: FailoverInfo): Promise<void>;
|
|
51
|
+
updateRunStartedReply(chatId: string, projectAlias: string, runId: string, backendLabel?: string): Promise<void>;
|
|
52
|
+
updateRunProgressReply(input: {
|
|
53
|
+
chatId: string;
|
|
54
|
+
projectAlias: string;
|
|
55
|
+
prompt: string;
|
|
56
|
+
sessionKey: string;
|
|
57
|
+
replyToMessageId?: string;
|
|
58
|
+
}, runId: string, message: string, backendLabel?: string): Promise<void>;
|
|
59
|
+
sendOrUpdateRunOutcome(input: {
|
|
60
|
+
input: ExecutePromptInput;
|
|
61
|
+
runId: string;
|
|
62
|
+
title: string;
|
|
63
|
+
body: string;
|
|
64
|
+
runStatus: 'success' | 'failure' | 'cancelled';
|
|
65
|
+
runPhase: string;
|
|
66
|
+
cardSummary: string;
|
|
67
|
+
sessionId?: string;
|
|
68
|
+
}): Promise<void>;
|
|
69
|
+
enforceSessionHistoryLimit(conversationKey: string, projectAlias: string): Promise<void>;
|
|
70
|
+
notifyProjectChats(projectAlias: string, text: string): Promise<void>;
|
|
71
|
+
checkAndSendAlerts(completedRun: RunState): Promise<void>;
|
|
72
|
+
}
|
|
73
|
+
export interface ExecutePromptInput {
|
|
74
|
+
runId?: string;
|
|
75
|
+
chatId: string;
|
|
76
|
+
actorId?: string;
|
|
77
|
+
tenantKey?: string;
|
|
78
|
+
projectAlias: string;
|
|
79
|
+
project: ProjectConfig;
|
|
80
|
+
incomingMessage: IncomingMessageContext;
|
|
81
|
+
prompt: string;
|
|
82
|
+
sessionKey: string;
|
|
83
|
+
queueKey: string;
|
|
84
|
+
replyToMessageId?: string;
|
|
85
|
+
}
|
|
86
|
+
export declare function executePrompt(host: PipelineHost, input: ExecutePromptInput): Promise<void>;
|
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { resolveProjectBackendWithFailover } from '../backend/factory.js';
|
|
4
|
+
import { retrieveMemoryContext } from '../memory/retrieve.js';
|
|
5
|
+
import { summarizeThreadTurn } from '../memory/summarize.js';
|
|
6
|
+
import { extractInsights } from '../collaboration/knowledge.js';
|
|
7
|
+
import { recordRunOutcome, DEFAULT_TRUST_POLICY } from '../collaboration/trust.js';
|
|
8
|
+
import { buildProjectTimeline, buildOnboardingContext, isNewActor } from '../collaboration/timeline.js';
|
|
9
|
+
import { truncateForFeishuCard } from '../feishu/text.js';
|
|
10
|
+
import { estimateCost } from '../observability/cost.js';
|
|
11
|
+
import { extractFileMarkers, friendlyErrorMessage, truncateExcerpt } from './service-utils.js';
|
|
12
|
+
export async function executePrompt(host, input) {
|
|
13
|
+
const conversation = (await host.sessionStore.getConversation(input.sessionKey)) ??
|
|
14
|
+
(await host.sessionStore.ensureConversation(input.sessionKey, {
|
|
15
|
+
chat_id: input.chatId,
|
|
16
|
+
actor_id: input.actorId,
|
|
17
|
+
tenant_key: input.tenantKey,
|
|
18
|
+
scope: input.project.session_scope,
|
|
19
|
+
}));
|
|
20
|
+
let currentSession = conversation.projects[input.projectAlias];
|
|
21
|
+
// Auto-adopt latest local session when no active session exists
|
|
22
|
+
if (!currentSession?.thread_id && host.config.service.project_switch_auto_adopt_latest) {
|
|
23
|
+
try {
|
|
24
|
+
const sessionBackendOverrideForAdopt = await host.sessionStore.getProjectBackend(input.sessionKey, input.projectAlias);
|
|
25
|
+
const backendForAdopt = host.resolveBackendByName(input.projectAlias, sessionBackendOverrideForAdopt);
|
|
26
|
+
const latestLocal = await backendForAdopt.findLatestSession(input.project.root);
|
|
27
|
+
if (latestLocal) {
|
|
28
|
+
await host.sessionStore.upsertProjectSession(input.sessionKey, input.projectAlias, {
|
|
29
|
+
thread_id: latestLocal.sessionId,
|
|
30
|
+
});
|
|
31
|
+
const refreshed = await host.sessionStore.getConversation(input.sessionKey);
|
|
32
|
+
currentSession = refreshed?.projects[input.projectAlias];
|
|
33
|
+
host.logger.info({ projectAlias: input.projectAlias, sessionId: latestLocal.sessionId, backend: latestLocal.backend }, 'Auto-adopted latest local session for prompt execution');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch { /* auto-adopt is best-effort */ }
|
|
37
|
+
}
|
|
38
|
+
if (host.config.service.memory_enabled) {
|
|
39
|
+
await host.memoryStore.cleanupExpiredMemories();
|
|
40
|
+
}
|
|
41
|
+
const memoryContext = host.config.service.memory_enabled
|
|
42
|
+
? await retrieveMemoryContext(host.memoryStore, {
|
|
43
|
+
conversationKey: input.sessionKey,
|
|
44
|
+
projectAlias: input.projectAlias,
|
|
45
|
+
threadId: currentSession?.thread_id,
|
|
46
|
+
query: input.prompt,
|
|
47
|
+
searchLimit: host.config.service.memory_search_limit,
|
|
48
|
+
groupChatId: input.incomingMessage.chat_type === 'group' ? input.incomingMessage.chat_id : undefined,
|
|
49
|
+
includeGroupMemories: host.config.service.memory_group_enabled && input.incomingMessage.chat_type === 'group',
|
|
50
|
+
})
|
|
51
|
+
: { pinnedMemories: [], relevantMemories: [], pinnedGroupMemories: [], relevantGroupMemories: [] };
|
|
52
|
+
// Direction 6: Inject onboarding context for new actors
|
|
53
|
+
let onboardingPrefix = '';
|
|
54
|
+
if (input.actorId && host.config.service.memory_enabled) {
|
|
55
|
+
try {
|
|
56
|
+
const allRuns = await host.runStateStore.listRuns();
|
|
57
|
+
if (isNewActor(input.actorId, allRuns, input.projectAlias)) {
|
|
58
|
+
const memories = await host.memoryStore.listRecentMemories({ scope: 'project', project_alias: input.projectAlias }, 10);
|
|
59
|
+
const timeline = buildProjectTimeline(allRuns, memories, [], input.projectAlias, 10);
|
|
60
|
+
onboardingPrefix = buildOnboardingContext(timeline, memories, input.projectAlias);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch { /* onboarding injection is best-effort */ }
|
|
64
|
+
}
|
|
65
|
+
const effectivePrompt = onboardingPrefix
|
|
66
|
+
? `${onboardingPrefix}\n\n${input.prompt}`
|
|
67
|
+
: input.prompt;
|
|
68
|
+
const bridgePrompt = await host.buildBridgePrompt(input.projectAlias, input.project, input.incomingMessage, effectivePrompt, memoryContext);
|
|
69
|
+
const startedAt = Date.now();
|
|
70
|
+
const projectRoot = path.resolve(input.project.root);
|
|
71
|
+
const runId = input.runId ?? randomUUID();
|
|
72
|
+
let lastProgressUpdate = 0;
|
|
73
|
+
const activeRun = {
|
|
74
|
+
runId,
|
|
75
|
+
controller: new AbortController(),
|
|
76
|
+
};
|
|
77
|
+
host.activeRuns.set(input.queueKey, activeRun);
|
|
78
|
+
const sessionBackendOverride = await host.sessionStore.getProjectBackend(input.sessionKey, input.projectAlias);
|
|
79
|
+
const failoverResolution = await resolveProjectBackendWithFailover(host.config, input.projectAlias, sessionBackendOverride, host.codexSessionIndex);
|
|
80
|
+
const backend = failoverResolution.backend;
|
|
81
|
+
if (failoverResolution.failover) {
|
|
82
|
+
await host.handleBackendFailover(input.chatId, input.projectAlias, runId, failoverResolution.failover);
|
|
83
|
+
}
|
|
84
|
+
const backendLabel = backend.name === 'claude' ? 'Claude' : 'Codex';
|
|
85
|
+
await host.updateRunStartedReply(input.chatId, input.projectAlias, runId, backendLabel);
|
|
86
|
+
await host.runStateStore.upsertRun(runId, {
|
|
87
|
+
queue_key: input.queueKey,
|
|
88
|
+
conversation_key: input.sessionKey,
|
|
89
|
+
project_alias: input.projectAlias,
|
|
90
|
+
chat_id: input.chatId,
|
|
91
|
+
actor_id: input.actorId,
|
|
92
|
+
actor_name: input.incomingMessage.actor_name,
|
|
93
|
+
session_id: currentSession?.thread_id,
|
|
94
|
+
project_root: projectRoot,
|
|
95
|
+
prompt_excerpt: truncateExcerpt(input.prompt),
|
|
96
|
+
status: 'running',
|
|
97
|
+
status_detail: undefined,
|
|
98
|
+
});
|
|
99
|
+
await host.auditLog.append({
|
|
100
|
+
type: 'codex.run.started',
|
|
101
|
+
run_id: runId,
|
|
102
|
+
chat_id: input.chatId,
|
|
103
|
+
actor_id: input.actorId,
|
|
104
|
+
project_alias: input.projectAlias,
|
|
105
|
+
conversation_key: input.sessionKey,
|
|
106
|
+
session_id: currentSession?.thread_id,
|
|
107
|
+
prompt: input.prompt,
|
|
108
|
+
});
|
|
109
|
+
await host.appendProjectAuditEvent(input.projectAlias, input.project, {
|
|
110
|
+
type: 'codex.run.started',
|
|
111
|
+
run_id: runId,
|
|
112
|
+
chat_id: input.chatId,
|
|
113
|
+
actor_id: input.actorId,
|
|
114
|
+
session_id: currentSession?.thread_id,
|
|
115
|
+
project_root: projectRoot,
|
|
116
|
+
});
|
|
117
|
+
host.logger.info({
|
|
118
|
+
runId,
|
|
119
|
+
queueKey: input.queueKey,
|
|
120
|
+
sessionKey: input.sessionKey,
|
|
121
|
+
projectAlias: input.projectAlias,
|
|
122
|
+
projectRoot,
|
|
123
|
+
sessionId: currentSession?.thread_id,
|
|
124
|
+
}, 'Codex run started');
|
|
125
|
+
host.metrics?.recordCodexTurnStarted(input.projectAlias, runId);
|
|
126
|
+
try {
|
|
127
|
+
const outputTokenLimit = backend.name === 'claude'
|
|
128
|
+
? (host.config.claude?.output_token_limit ?? host.config.codex.output_token_limit)
|
|
129
|
+
: host.config.codex.output_token_limit;
|
|
130
|
+
const result = await backend.run({
|
|
131
|
+
workdir: input.project.root,
|
|
132
|
+
prompt: bridgePrompt,
|
|
133
|
+
sessionId: currentSession?.thread_id,
|
|
134
|
+
timeoutMs: backend.name === 'claude'
|
|
135
|
+
? (host.config.claude?.run_timeout_ms ?? host.config.codex.run_timeout_ms)
|
|
136
|
+
: host.config.codex.run_timeout_ms,
|
|
137
|
+
signal: activeRun.controller.signal,
|
|
138
|
+
logger: host.logger,
|
|
139
|
+
projectConfig: backend.name === 'codex'
|
|
140
|
+
? {
|
|
141
|
+
profile: input.project.profile ?? host.config.codex.default_profile,
|
|
142
|
+
model: input.project.codex_model,
|
|
143
|
+
sandbox: input.project.codex_sandbox ?? input.project.sandbox ?? host.config.codex.default_sandbox,
|
|
144
|
+
tempDir: host.resolveProjectTempDir(input.projectAlias, input.project),
|
|
145
|
+
cacheDir: host.resolveProjectCacheDir(input.projectAlias, input.project),
|
|
146
|
+
}
|
|
147
|
+
: {
|
|
148
|
+
permissionMode: input.project.claude_permission_mode ?? host.config.claude?.default_permission_mode,
|
|
149
|
+
model: input.project.claude_model ?? host.config.claude?.default_model,
|
|
150
|
+
maxBudgetUsd: input.project.claude_max_budget_usd ?? host.config.claude?.max_budget_usd,
|
|
151
|
+
allowedTools: input.project.claude_allowed_tools ?? host.config.claude?.allowed_tools,
|
|
152
|
+
systemPromptAppend: input.project.claude_system_prompt_append ?? host.config.claude?.system_prompt_append,
|
|
153
|
+
},
|
|
154
|
+
onSpawn: async (pid) => {
|
|
155
|
+
activeRun.pid = pid;
|
|
156
|
+
await host.runStateStore.upsertRun(runId, {
|
|
157
|
+
queue_key: input.queueKey,
|
|
158
|
+
conversation_key: input.sessionKey,
|
|
159
|
+
project_alias: input.projectAlias,
|
|
160
|
+
chat_id: input.chatId,
|
|
161
|
+
actor_id: input.actorId,
|
|
162
|
+
session_id: currentSession?.thread_id,
|
|
163
|
+
project_root: projectRoot,
|
|
164
|
+
prompt_excerpt: truncateExcerpt(input.prompt),
|
|
165
|
+
status: 'running',
|
|
166
|
+
status_detail: undefined,
|
|
167
|
+
pid,
|
|
168
|
+
});
|
|
169
|
+
},
|
|
170
|
+
onEvent: async (event) => {
|
|
171
|
+
if (!host.config.service.emit_progress_updates) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const message = backend.summarizeEvent(event);
|
|
175
|
+
if (!message) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const now = Date.now();
|
|
179
|
+
if (now - lastProgressUpdate < host.config.service.progress_update_interval_ms) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
lastProgressUpdate = now;
|
|
183
|
+
await host.updateRunProgressReply(input, runId, message, backendLabel);
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
const excerpt = result.finalMessage.slice(0, outputTokenLimit);
|
|
187
|
+
if (!excerpt.trim()) {
|
|
188
|
+
host.logger.warn({
|
|
189
|
+
runId,
|
|
190
|
+
queueKey: input.queueKey,
|
|
191
|
+
sessionKey: input.sessionKey,
|
|
192
|
+
projectAlias: input.projectAlias,
|
|
193
|
+
sessionId: result.sessionId,
|
|
194
|
+
durationMs: Date.now() - startedAt,
|
|
195
|
+
}, 'Codex run completed without a displayable final message');
|
|
196
|
+
}
|
|
197
|
+
// Extract and send any [SEND_FILE:path] markers before text reply
|
|
198
|
+
const { cleanText: excerptWithoutFiles, filePaths } = extractFileMarkers(excerpt);
|
|
199
|
+
if (filePaths.length > 0) {
|
|
200
|
+
for (const filePath of filePaths) {
|
|
201
|
+
try {
|
|
202
|
+
await host.feishuClient.sendFile(input.chatId, filePath);
|
|
203
|
+
host.logger.info({ chatId: input.chatId, filePath }, 'Sent file to Feishu');
|
|
204
|
+
}
|
|
205
|
+
catch (err) {
|
|
206
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
207
|
+
host.logger.warn({ chatId: input.chatId, filePath, error: msg }, 'Failed to send file to Feishu');
|
|
208
|
+
// Notify user about the failure inline
|
|
209
|
+
excerptWithoutFiles === excerpt || await host.feishuClient.sendText(input.chatId, `⚠️ 文件发送失败: ${filePath}\n${msg}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
const finalExcerpt = excerptWithoutFiles.trim() || excerpt;
|
|
214
|
+
const cardSummary = truncateForFeishuCard(finalExcerpt || `${backendLabel} 已完成,但没有返回可显示文本。`);
|
|
215
|
+
await host.auditLog.append({
|
|
216
|
+
type: 'codex.run.completed',
|
|
217
|
+
run_id: runId,
|
|
218
|
+
chat_id: input.chatId,
|
|
219
|
+
actor_id: input.actorId,
|
|
220
|
+
project_alias: input.projectAlias,
|
|
221
|
+
conversation_key: input.sessionKey,
|
|
222
|
+
session_id: result.sessionId,
|
|
223
|
+
exit_code: result.exitCode,
|
|
224
|
+
duration_ms: Date.now() - startedAt,
|
|
225
|
+
backend: backend.name,
|
|
226
|
+
});
|
|
227
|
+
await host.appendProjectAuditEvent(input.projectAlias, input.project, {
|
|
228
|
+
type: 'codex.run.completed',
|
|
229
|
+
run_id: runId,
|
|
230
|
+
chat_id: input.chatId,
|
|
231
|
+
actor_id: input.actorId,
|
|
232
|
+
session_id: result.sessionId,
|
|
233
|
+
duration_ms: Date.now() - startedAt,
|
|
234
|
+
backend: backend.name,
|
|
235
|
+
});
|
|
236
|
+
host.logger.info({
|
|
237
|
+
runId,
|
|
238
|
+
queueKey: input.queueKey,
|
|
239
|
+
sessionKey: input.sessionKey,
|
|
240
|
+
projectAlias: input.projectAlias,
|
|
241
|
+
sessionId: result.sessionId,
|
|
242
|
+
exitCode: result.exitCode,
|
|
243
|
+
finalMessageChars: excerpt.length,
|
|
244
|
+
durationMs: Date.now() - startedAt,
|
|
245
|
+
}, 'Codex run completed');
|
|
246
|
+
await host.sessionStore.upsertProjectSession(input.sessionKey, input.projectAlias, {
|
|
247
|
+
thread_id: result.sessionId,
|
|
248
|
+
last_prompt: input.prompt,
|
|
249
|
+
last_response_excerpt: excerpt,
|
|
250
|
+
});
|
|
251
|
+
if (host.config.service.memory_enabled && result.sessionId) {
|
|
252
|
+
const summaryDraft = summarizeThreadTurn({
|
|
253
|
+
previousSummary: memoryContext.threadSummary?.summary,
|
|
254
|
+
prompt: input.prompt,
|
|
255
|
+
responseExcerpt: excerpt,
|
|
256
|
+
maxChars: host.config.service.thread_summary_max_chars,
|
|
257
|
+
});
|
|
258
|
+
const threadSummary = await host.memoryStore.upsertThreadSummary({
|
|
259
|
+
conversation_key: input.sessionKey,
|
|
260
|
+
project_alias: input.projectAlias,
|
|
261
|
+
thread_id: result.sessionId,
|
|
262
|
+
summary: summaryDraft.summary,
|
|
263
|
+
recent_prompt: input.prompt,
|
|
264
|
+
recent_response_excerpt: excerpt,
|
|
265
|
+
files_touched: summaryDraft.filesTouched,
|
|
266
|
+
open_tasks: summaryDraft.openTasks,
|
|
267
|
+
decisions: summaryDraft.decisions,
|
|
268
|
+
});
|
|
269
|
+
await host.auditLog.append({
|
|
270
|
+
type: 'memory.thread_summary.updated',
|
|
271
|
+
run_id: runId,
|
|
272
|
+
project_alias: input.projectAlias,
|
|
273
|
+
conversation_key: input.sessionKey,
|
|
274
|
+
thread_id: result.sessionId,
|
|
275
|
+
files_touched: threadSummary.files_touched,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
await host.enforceSessionHistoryLimit(input.sessionKey, input.projectAlias);
|
|
279
|
+
await host.runStateStore.upsertRun(runId, {
|
|
280
|
+
queue_key: input.queueKey,
|
|
281
|
+
conversation_key: input.sessionKey,
|
|
282
|
+
project_alias: input.projectAlias,
|
|
283
|
+
chat_id: input.chatId,
|
|
284
|
+
actor_id: input.actorId,
|
|
285
|
+
session_id: result.sessionId,
|
|
286
|
+
project_root: projectRoot,
|
|
287
|
+
pid: activeRun.pid,
|
|
288
|
+
prompt_excerpt: truncateExcerpt(input.prompt),
|
|
289
|
+
status: 'success',
|
|
290
|
+
status_detail: undefined,
|
|
291
|
+
input_tokens: result.inputTokens,
|
|
292
|
+
output_tokens: result.outputTokens,
|
|
293
|
+
estimated_cost_usd: estimateCost(result.inputTokens, result.outputTokens, backend.name),
|
|
294
|
+
});
|
|
295
|
+
host.metrics?.recordCodexTurn('success', input.projectAlias, (Date.now() - startedAt) / 1000, runId);
|
|
296
|
+
// Record cost and token metrics
|
|
297
|
+
if (result.inputTokens || result.outputTokens) {
|
|
298
|
+
const costUsd = estimateCost(result.inputTokens, result.outputTokens, backend.name) ?? 0;
|
|
299
|
+
host.metrics?.recordCost(input.projectAlias, backend.name, costUsd);
|
|
300
|
+
host.metrics?.recordTokens(input.projectAlias, backend.name, result.inputTokens ?? 0, result.outputTokens ?? 0);
|
|
301
|
+
}
|
|
302
|
+
// Direction 5: Record trust outcome
|
|
303
|
+
try {
|
|
304
|
+
const trustState = await host.trustStore.getOrCreate(input.projectAlias);
|
|
305
|
+
const updated = recordRunOutcome(trustState, true, DEFAULT_TRUST_POLICY);
|
|
306
|
+
await host.trustStore.update(input.projectAlias, updated);
|
|
307
|
+
host.metrics?.recordTrustLevel(input.projectAlias, updated.current_level);
|
|
308
|
+
}
|
|
309
|
+
catch { /* trust tracking is best-effort */ }
|
|
310
|
+
// Proactive alerts: check if this run triggers any team alerts
|
|
311
|
+
try {
|
|
312
|
+
const completedRunState = await host.runStateStore.getRun(runId);
|
|
313
|
+
if (completedRunState) {
|
|
314
|
+
await host.checkAndSendAlerts(completedRunState);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
catch { /* alerts are best-effort */ }
|
|
318
|
+
// Direction 2: Auto-extract knowledge
|
|
319
|
+
if (host.config.service.memory_enabled && excerpt.length >= 100) {
|
|
320
|
+
try {
|
|
321
|
+
const insight = extractInsights(input.prompt, excerpt, input.projectAlias);
|
|
322
|
+
if (insight) {
|
|
323
|
+
await host.memoryStore.saveProjectMemory({
|
|
324
|
+
project_alias: insight.project_alias,
|
|
325
|
+
title: insight.title,
|
|
326
|
+
content: insight.content,
|
|
327
|
+
tags: insight.tags,
|
|
328
|
+
source: 'auto',
|
|
329
|
+
created_by: input.actorId,
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
catch { /* auto-extraction is best-effort */ }
|
|
334
|
+
}
|
|
335
|
+
await host.sendOrUpdateRunOutcome({
|
|
336
|
+
input,
|
|
337
|
+
runId,
|
|
338
|
+
title: `${backendLabel} 已完成`,
|
|
339
|
+
body: finalExcerpt || `${backendLabel} 已完成,但没有返回可显示文本。`,
|
|
340
|
+
runStatus: 'success',
|
|
341
|
+
runPhase: '已完成',
|
|
342
|
+
cardSummary,
|
|
343
|
+
sessionId: result.sessionId,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
catch (error) {
|
|
347
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
348
|
+
const cancelled = error instanceof Error && error.name === 'AbortError' && activeRun.cancelReason === 'user';
|
|
349
|
+
const status = cancelled ? 'cancelled' : 'failure';
|
|
350
|
+
if (!cancelled && error instanceof Error && error.name === 'AbortError') {
|
|
351
|
+
activeRun.cancelReason = 'timeout';
|
|
352
|
+
}
|
|
353
|
+
if (!cancelled && activeRun.cancelReason === 'timeout') {
|
|
354
|
+
host.metrics?.recordCodexTurn('failure', input.projectAlias, (Date.now() - startedAt) / 1000, runId);
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
host.metrics?.recordCodexTurn(cancelled ? 'cancelled' : 'failure', input.projectAlias, (Date.now() - startedAt) / 1000, runId);
|
|
358
|
+
}
|
|
359
|
+
await host.runStateStore.upsertRun(runId, {
|
|
360
|
+
queue_key: input.queueKey,
|
|
361
|
+
conversation_key: input.sessionKey,
|
|
362
|
+
project_alias: input.projectAlias,
|
|
363
|
+
chat_id: input.chatId,
|
|
364
|
+
actor_id: input.actorId,
|
|
365
|
+
session_id: currentSession?.thread_id,
|
|
366
|
+
project_root: projectRoot,
|
|
367
|
+
pid: activeRun.pid,
|
|
368
|
+
prompt_excerpt: truncateExcerpt(input.prompt),
|
|
369
|
+
status,
|
|
370
|
+
status_detail: undefined,
|
|
371
|
+
error: message,
|
|
372
|
+
});
|
|
373
|
+
await host.auditLog.append({
|
|
374
|
+
type: cancelled ? 'codex.run.cancelled' : 'codex.run.failed',
|
|
375
|
+
run_id: runId,
|
|
376
|
+
chat_id: input.chatId,
|
|
377
|
+
actor_id: input.actorId,
|
|
378
|
+
project_alias: input.projectAlias,
|
|
379
|
+
conversation_key: input.sessionKey,
|
|
380
|
+
error: message,
|
|
381
|
+
});
|
|
382
|
+
await host.appendProjectAuditEvent(input.projectAlias, input.project, {
|
|
383
|
+
type: cancelled ? 'codex.run.cancelled' : 'codex.run.failed',
|
|
384
|
+
run_id: runId,
|
|
385
|
+
chat_id: input.chatId,
|
|
386
|
+
actor_id: input.actorId,
|
|
387
|
+
error: message,
|
|
388
|
+
});
|
|
389
|
+
// Direction 5: Record trust failure (only for actual failures, not cancellations)
|
|
390
|
+
if (!cancelled) {
|
|
391
|
+
try {
|
|
392
|
+
const trustState = await host.trustStore.getOrCreate(input.projectAlias);
|
|
393
|
+
const updated = recordRunOutcome(trustState, false, DEFAULT_TRUST_POLICY);
|
|
394
|
+
await host.trustStore.update(input.projectAlias, updated);
|
|
395
|
+
}
|
|
396
|
+
catch { /* trust tracking is best-effort */ }
|
|
397
|
+
// Notify project chats about the failure
|
|
398
|
+
await host.notifyProjectChats(input.projectAlias, `❌ 运行失败 [${input.projectAlias}]\n${message.slice(0, 200)}`);
|
|
399
|
+
// Proactive alerts on failure
|
|
400
|
+
try {
|
|
401
|
+
const failedRunState = await host.runStateStore.getRun(runId);
|
|
402
|
+
if (failedRunState) {
|
|
403
|
+
await host.checkAndSendAlerts(failedRunState);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
catch { /* alerts are best-effort */ }
|
|
407
|
+
}
|
|
408
|
+
if (cancelled) {
|
|
409
|
+
host.logger.warn({
|
|
410
|
+
runId,
|
|
411
|
+
queueKey: input.queueKey,
|
|
412
|
+
sessionKey: input.sessionKey,
|
|
413
|
+
projectAlias: input.projectAlias,
|
|
414
|
+
durationMs: Date.now() - startedAt,
|
|
415
|
+
}, 'Codex run cancelled');
|
|
416
|
+
}
|
|
417
|
+
else {
|
|
418
|
+
host.logger.error({
|
|
419
|
+
error,
|
|
420
|
+
runId,
|
|
421
|
+
queueKey: input.queueKey,
|
|
422
|
+
sessionKey: input.sessionKey,
|
|
423
|
+
projectAlias: input.projectAlias,
|
|
424
|
+
durationMs: Date.now() - startedAt,
|
|
425
|
+
}, 'Codex run failed');
|
|
426
|
+
}
|
|
427
|
+
await host.sendOrUpdateRunOutcome({
|
|
428
|
+
input,
|
|
429
|
+
runId,
|
|
430
|
+
title: cancelled ? '运行已取消' : '执行失败',
|
|
431
|
+
body: cancelled ? '当前运行已取消。' : ['执行失败。', '', friendlyErrorMessage(message)].join('\n'),
|
|
432
|
+
runStatus: cancelled ? 'cancelled' : 'failure',
|
|
433
|
+
runPhase: cancelled ? '已取消' : '失败',
|
|
434
|
+
cardSummary: truncateForFeishuCard(cancelled ? '当前运行已取消。' : friendlyErrorMessage(message)),
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
finally {
|
|
438
|
+
host.activeRuns.delete(input.queueKey);
|
|
439
|
+
host.runReplyTargets.delete(runId);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
//# sourceMappingURL=run-pipeline.js.map
|