feat: support inbound files across all channels

This commit is contained in:
xmanrui 2026-08-23 22:39:26 +08:00
parent 29bcb67a64
commit 0a26d7df92
45 changed files with 2651 additions and 343 deletions

View file

@ -86,9 +86,10 @@ export async function createProductionController(ctx, config = {}, internals = {
return state;
};
const commandExecutor = createHarnessCommandExecutor(ctx, internals.commandExecutor);
const { controlExecutor, sessionMaintenanceExecutor } = createHarnessSessionExecutors(ctx, {
const { controlExecutor, sessionMaintenanceExecutor, fileIngressExecutor } = createHarnessSessionExecutors(ctx, {
controlExecutor: internals.controlExecutor,
sessionMaintenanceExecutor: internals.sessionMaintenanceExecutor,
fileIngressExecutor: internals.fileIngressExecutor,
});
const harness = new Harness({
baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
@ -98,6 +99,7 @@ export async function createProductionController(ctx, config = {}, internals = {
...(commandExecutor ? { commandExecutor } : {}),
...(controlExecutor ? { controlExecutor } : {}),
...(sessionMaintenanceExecutor ? { sessionMaintenanceExecutor } : {}),
...(fileIngressExecutor ? { fileIngressExecutor } : {}),
});
const coreController = new Controller({
deviceAuth,

View file

@ -133,9 +133,10 @@ export async function createProductionController(ctx, config = {}, internals = {
return stateFor(botConfig);
};
const commandExecutor = createHarnessCommandExecutor(ctx, internals.commandExecutor);
const { controlExecutor, sessionMaintenanceExecutor } = createHarnessSessionExecutors(ctx, {
const { controlExecutor, sessionMaintenanceExecutor, fileIngressExecutor } = createHarnessSessionExecutors(ctx, {
controlExecutor: internals.controlExecutor,
sessionMaintenanceExecutor: internals.sessionMaintenanceExecutor,
fileIngressExecutor: internals.fileIngressExecutor,
});
const harness = new Harness({
baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
@ -147,6 +148,7 @@ export async function createProductionController(ctx, config = {}, internals = {
...(commandExecutor ? { commandExecutor } : {}),
...(controlExecutor ? { controlExecutor } : {}),
...(sessionMaintenanceExecutor ? { sessionMaintenanceExecutor } : {}),
...(fileIngressExecutor ? { fileIngressExecutor } : {}),
});
const proxyEnv = internals.proxyEnv ?? process.env;
const wsAgent = createFeishuWebSocketAgent(proxyEnv, internals.createProxyAgent);

View file

@ -77,9 +77,10 @@ export async function createProductionController(ctx, config = {}, internals = {
return state;
};
const commandExecutor = createHarnessCommandExecutor(ctx, internals.commandExecutor);
const { controlExecutor, sessionMaintenanceExecutor } = createHarnessSessionExecutors(ctx, {
const { controlExecutor, sessionMaintenanceExecutor, fileIngressExecutor } = createHarnessSessionExecutors(ctx, {
controlExecutor: internals.controlExecutor,
sessionMaintenanceExecutor: internals.sessionMaintenanceExecutor,
fileIngressExecutor: internals.fileIngressExecutor,
});
const harness = new Harness({
baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
@ -89,6 +90,7 @@ export async function createProductionController(ctx, config = {}, internals = {
...(commandExecutor ? { commandExecutor } : {}),
...(controlExecutor ? { controlExecutor } : {}),
...(sessionMaintenanceExecutor ? { sessionMaintenanceExecutor } : {}),
...(fileIngressExecutor ? { fileIngressExecutor } : {}),
});
const coreController = new Controller({
qrAuth,

View file

@ -78,9 +78,10 @@ export async function createTokenProductionController(ctx, config, internals, de
return state;
};
const commandExecutor = createHarnessCommandExecutor(ctx, internals.commandExecutor);
const { controlExecutor, sessionMaintenanceExecutor } = createHarnessSessionExecutors(ctx, {
const { controlExecutor, sessionMaintenanceExecutor, fileIngressExecutor } = createHarnessSessionExecutors(ctx, {
controlExecutor: internals.controlExecutor,
sessionMaintenanceExecutor: internals.sessionMaintenanceExecutor,
fileIngressExecutor: internals.fileIngressExecutor,
});
const harness = new ResolvedHarness({
baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
@ -90,6 +91,7 @@ export async function createTokenProductionController(ctx, config, internals, de
...(commandExecutor ? { commandExecutor } : {}),
...(controlExecutor ? { controlExecutor } : {}),
...(sessionMaintenanceExecutor ? { sessionMaintenanceExecutor } : {}),
...(fileIngressExecutor ? { fileIngressExecutor } : {}),
});
const coreController = new ResolvedController({
credentials: ctx.credentials,

View file

@ -56,9 +56,10 @@ export async function createProductionController(ctx, config = {}, internals = {
return state;
};
const commandExecutor = createHarnessCommandExecutor(ctx, internals.commandExecutor);
const { controlExecutor, sessionMaintenanceExecutor } = createHarnessSessionExecutors(ctx, {
const { controlExecutor, sessionMaintenanceExecutor, fileIngressExecutor } = createHarnessSessionExecutors(ctx, {
controlExecutor: internals.controlExecutor,
sessionMaintenanceExecutor: internals.sessionMaintenanceExecutor,
fileIngressExecutor: internals.fileIngressExecutor,
});
const harness = new ResolvedHarness({
baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
@ -68,6 +69,7 @@ export async function createProductionController(ctx, config = {}, internals = {
...(commandExecutor ? { commandExecutor } : {}),
...(controlExecutor ? { controlExecutor } : {}),
...(sessionMaintenanceExecutor ? { sessionMaintenanceExecutor } : {}),
...(fileIngressExecutor ? { fileIngressExecutor } : {}),
});
const coreController = new ResolvedController({
credentials: ctx.credentials,

View file

@ -80,9 +80,10 @@ export async function createProductionController(ctx, config = {}, internals = {
return state;
};
const commandExecutor = createHarnessCommandExecutor(ctx, internals.commandExecutor);
const { controlExecutor, sessionMaintenanceExecutor } = createHarnessSessionExecutors(ctx, {
const { controlExecutor, sessionMaintenanceExecutor, fileIngressExecutor } = createHarnessSessionExecutors(ctx, {
controlExecutor: internals.controlExecutor,
sessionMaintenanceExecutor: internals.sessionMaintenanceExecutor,
fileIngressExecutor: internals.fileIngressExecutor,
});
const harness = new Harness({
baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
@ -92,6 +93,7 @@ export async function createProductionController(ctx, config = {}, internals = {
...(commandExecutor ? { commandExecutor } : {}),
...(controlExecutor ? { controlExecutor } : {}),
...(sessionMaintenanceExecutor ? { sessionMaintenanceExecutor } : {}),
...(fileIngressExecutor ? { fileIngressExecutor } : {}),
});
const coreController = new Controller({
qrAuth,

View file

@ -80,9 +80,10 @@ export async function createProductionController(ctx, config = {}, internals = {
return state;
};
const commandExecutor = createHarnessCommandExecutor(ctx, internals.commandExecutor);
const { controlExecutor, sessionMaintenanceExecutor } = createHarnessSessionExecutors(ctx, {
const { controlExecutor, sessionMaintenanceExecutor, fileIngressExecutor } = createHarnessSessionExecutors(ctx, {
controlExecutor: internals.controlExecutor,
sessionMaintenanceExecutor: internals.sessionMaintenanceExecutor,
fileIngressExecutor: internals.fileIngressExecutor,
});
const harness = new Harness({
baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
@ -92,6 +93,7 @@ export async function createProductionController(ctx, config = {}, internals = {
...(commandExecutor ? { commandExecutor } : {}),
...(controlExecutor ? { controlExecutor } : {}),
...(sessionMaintenanceExecutor ? { sessionMaintenanceExecutor } : {}),
...(fileIngressExecutor ? { fileIngressExecutor } : {}),
});
const coreController = new Controller({
api,

View file

@ -83,9 +83,10 @@ export async function createProductionController(ctx, config = {}, internals = {
return state;
};
const commandExecutor = createHarnessCommandExecutor(ctx, internals.commandExecutor);
const { controlExecutor, sessionMaintenanceExecutor } = createHarnessSessionExecutors(ctx, {
const { controlExecutor, sessionMaintenanceExecutor, fileIngressExecutor } = createHarnessSessionExecutors(ctx, {
controlExecutor: internals.controlExecutor,
sessionMaintenanceExecutor: internals.sessionMaintenanceExecutor,
fileIngressExecutor: internals.fileIngressExecutor,
});
const harness = new Harness({
baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
@ -95,6 +96,7 @@ export async function createProductionController(ctx, config = {}, internals = {
...(commandExecutor ? { commandExecutor } : {}),
...(controlExecutor ? { controlExecutor } : {}),
...(sessionMaintenanceExecutor ? { sessionMaintenanceExecutor } : {}),
...(fileIngressExecutor ? { fileIngressExecutor } : {}),
});
const coreController = new Controller({
configStore: observedConfigStore,

View file

@ -1,5 +1,10 @@
import { randomUUID } from 'node:crypto';
import {
InboundFileError,
stageInboundFiles,
} from '../../src/channels/shared/inbound-file.mjs';
function agentsFromContext(ctx) {
if (typeof ctx?.get !== 'function') return undefined;
let agents;
@ -108,13 +113,30 @@ function createSessionMaintenanceExecutor(agents) {
};
}
function createFileIngressExecutor(agents) {
return ({ sessionId, workspace, files, signal }) => {
const agent = agents.get(sessionId);
const attachedWorkspace = agent?.session?.header?.cwd;
const exactWorkspace = typeof attachedWorkspace === 'string' && attachedWorkspace
? attachedWorkspace
: workspace;
if (typeof exactWorkspace !== 'string' || !exactWorkspace) {
throw new InboundFileError(
'inbound-file-workspace-unavailable',
'The Harness Session workspace is unavailable for inbound files.',
);
}
return stageInboundFiles({ files }, { workspace: exactWorkspace, signal });
};
}
/**
* Build optional same-process Session executors without adding a hard Cordis
* service injection. Fixtures and deployments without AgentRegistry preserve
* the existing HTTP behavior.
* Build optional same-process Session executors. File ingress also works for
* cold Sessions by using the authoritative cwd returned by session.list;
* when an Agent is attached, its live Session header remains authoritative.
*/
export function createHarnessSessionExecutors(ctx, provided = {}) {
const { controlExecutor, sessionMaintenanceExecutor } = provided;
const { controlExecutor, sessionMaintenanceExecutor, fileIngressExecutor } = provided;
if (controlExecutor !== undefined && typeof controlExecutor !== 'function') {
throw new TypeError('controlExecutor must be a function');
}
@ -122,13 +144,18 @@ export function createHarnessSessionExecutors(ctx, provided = {}) {
&& typeof sessionMaintenanceExecutor !== 'function') {
throw new TypeError('sessionMaintenanceExecutor must be a function');
}
if (fileIngressExecutor !== undefined && typeof fileIngressExecutor !== 'function') {
throw new TypeError('fileIngressExecutor must be a function');
}
const agents = controlExecutor && sessionMaintenanceExecutor
const agents = controlExecutor && sessionMaintenanceExecutor && fileIngressExecutor
? undefined
: agentsFromContext(ctx);
return {
controlExecutor: controlExecutor ?? (agents ? createControlExecutor(agents) : undefined),
sessionMaintenanceExecutor: sessionMaintenanceExecutor
?? (agents ? createSessionMaintenanceExecutor(agents) : undefined),
fileIngressExecutor: fileIngressExecutor
?? createFileIngressExecutor(agents ?? { get: () => undefined }),
};
}