|
@@ -1,7 +1,7 @@
|
|
|
const fs = require('fs');
|
|
const fs = require('fs');
|
|
|
const path = require('path');
|
|
const path = require('path');
|
|
|
const crypto = require('crypto');
|
|
const crypto = require('crypto');
|
|
|
-const { PACKAGE_ROOT, WORKSPACE_ROOT, latestPath } = require('../core/output-paths');
|
|
|
|
|
|
|
+const { PACKAGE_ROOT, WORKSPACE_ROOT, latestPath, outputsRoot } = require('../core/output-paths');
|
|
|
const { AgentWorkbenchDb } = require('../core/agent-workbench-db');
|
|
const { AgentWorkbenchDb } = require('../core/agent-workbench-db');
|
|
|
const { AgentKnowledgeStore } = require('../core/agent-knowledge');
|
|
const { AgentKnowledgeStore } = require('../core/agent-knowledge');
|
|
|
const { QiweiAgentRuntime, extractExplicitCustomerIntelligence } = require('../core/agent-runtime');
|
|
const { QiweiAgentRuntime, extractExplicitCustomerIntelligence } = require('../core/agent-runtime');
|
|
@@ -42,6 +42,43 @@ function readEnvFile(filePath) {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+function readRuntimeState() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const filePath = path.join(outputsRoot(), 'runtime', 'qiwei-runtime.json');
|
|
|
|
|
+ return JSON.parse(fs.readFileSync(filePath, 'utf8').replace(/^\uFEFF/, ''));
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ return {};
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function runtimeProcessAlive(pid) {
|
|
|
|
|
+ const numericPid = Number(pid);
|
|
|
|
|
+ if (!Number.isInteger(numericPid) || numericPid <= 0) return false;
|
|
|
|
|
+ try {
|
|
|
|
|
+ process.kill(numericPid, 0);
|
|
|
|
|
+ return true;
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function listenerStateWithRuntime(product, localState) {
|
|
|
|
|
+ const runtime = readRuntimeState();
|
|
|
|
|
+ if (!runtimeProcessAlive(runtime.pid) || !['starting', 'running'].includes(runtime.status)) return localState;
|
|
|
|
|
+ const component = product.mode === 'enterprise'
|
|
|
|
|
+ ? runtime.components?.enterpriseRelay
|
|
|
|
|
+ : runtime.components?.personalPolling;
|
|
|
|
|
+ if (!component) return localState;
|
|
|
|
|
+ return {
|
|
|
|
|
+ ...localState,
|
|
|
|
|
+ running: Boolean(localState.running || ['starting', 'running'].includes(component.status)),
|
|
|
|
|
+ lastError: component.lastError || localState.lastError || '',
|
|
|
|
|
+ transport: runtime.transport,
|
|
|
|
|
+ runtimePid: runtime.pid,
|
|
|
|
|
+ runtimeStatus: component.status,
|
|
|
|
|
+ };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
function readClaudeSettingsEnv() {
|
|
function readClaudeSettingsEnv() {
|
|
|
const result = {};
|
|
const result = {};
|
|
|
const home = process.env.USERPROFILE || process.env.HOME || '';
|
|
const home = process.env.USERPROFILE || process.env.HOME || '';
|
|
@@ -222,6 +259,7 @@ class QiweiAgentPoller {
|
|
|
this.loopPromise = null;
|
|
this.loopPromise = null;
|
|
|
this.startedAt = 0;
|
|
this.startedAt = 0;
|
|
|
this.lastError = '';
|
|
this.lastError = '';
|
|
|
|
|
+ this.wakeLoop = null;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
status() {
|
|
status() {
|
|
@@ -251,10 +289,25 @@ class QiweiAgentPoller {
|
|
|
stop() {
|
|
stop() {
|
|
|
if (!this.running) return this.status();
|
|
if (!this.running) return this.status();
|
|
|
this.running = false;
|
|
this.running = false;
|
|
|
|
|
+ if (this.wakeLoop) this.wakeLoop();
|
|
|
this.db.audit({ actor: 'runtime', action: 'poller_stopped', detail: this.status() });
|
|
this.db.audit({ actor: 'runtime', action: 'poller_stopped', detail: this.status() });
|
|
|
return this.status();
|
|
return this.status();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ waitInterval() {
|
|
|
|
|
+ return new Promise(resolve => {
|
|
|
|
|
+ const timer = setTimeout(() => {
|
|
|
|
|
+ this.wakeLoop = null;
|
|
|
|
|
+ resolve();
|
|
|
|
|
+ }, this.config.intervalMs);
|
|
|
|
|
+ this.wakeLoop = () => {
|
|
|
|
|
+ clearTimeout(timer);
|
|
|
|
|
+ this.wakeLoop = null;
|
|
|
|
|
+ resolve();
|
|
|
|
|
+ };
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
async establishBaseline() {
|
|
async establishBaseline() {
|
|
|
let cursor = 0;
|
|
let cursor = 0;
|
|
|
let reachedEnd = false;
|
|
let reachedEnd = false;
|
|
@@ -293,7 +346,7 @@ class QiweiAgentPoller {
|
|
|
this.lastError = error.message;
|
|
this.lastError = error.message;
|
|
|
this.db.audit({ actor: 'runtime', action: 'poller_error', detail: { message: error.message } });
|
|
this.db.audit({ actor: 'runtime', action: 'poller_error', detail: { message: error.message } });
|
|
|
}
|
|
}
|
|
|
- if (this.running) await delay(this.config.intervalMs);
|
|
|
|
|
|
|
+ if (this.running) await this.waitInterval();
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -824,12 +877,13 @@ async function getAgentStatus() {
|
|
|
const account = await detectAccountStatus();
|
|
const account = await detectAccountStatus();
|
|
|
const state = workbench.service.state(workbench.poller.status());
|
|
const state = workbench.service.state(workbench.poller.status());
|
|
|
const product = getProductMode();
|
|
const product = getProductMode();
|
|
|
|
|
+ const listener = listenerStateWithRuntime(product, state.poller);
|
|
|
return {
|
|
return {
|
|
|
status: 'ok',
|
|
status: 'ok',
|
|
|
data: {
|
|
data: {
|
|
|
globalMode: state.global.paused ? 'paused' : state.global.defaultMode,
|
|
globalMode: state.global.paused ? 'paused' : state.global.defaultMode,
|
|
|
global: state.global,
|
|
global: state.global,
|
|
|
- listener: state.poller,
|
|
|
|
|
|
|
+ listener,
|
|
|
account,
|
|
account,
|
|
|
product,
|
|
product,
|
|
|
agent: state.agent,
|
|
agent: state.agent,
|
|
@@ -1345,12 +1399,49 @@ async function startListenerForWorkbench(target, account) {
|
|
|
};
|
|
};
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+async function ingestRuntimeMessage(message = {}, options = {}) {
|
|
|
|
|
+ await workbench.poller.process({
|
|
|
|
|
+ ...message,
|
|
|
|
|
+ _runtimeSource: String(options.source || 'runtime'),
|
|
|
|
|
+ });
|
|
|
|
|
+ return {
|
|
|
|
|
+ status: 'ok',
|
|
|
|
|
+ data: {
|
|
|
|
|
+ source: String(options.source || 'runtime'),
|
|
|
|
|
+ messageId: String(message.msgUniqueIdentifier || message.msgServerId || ''),
|
|
|
|
|
+ },
|
|
|
|
|
+ };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
async function startListener() {
|
|
async function startListener() {
|
|
|
|
|
+ const product = getProductMode();
|
|
|
|
|
+ if (product.mode === 'enterprise') {
|
|
|
|
|
+ const runtime = listenerStateWithRuntime(product, workbench.poller.status());
|
|
|
|
|
+ if (!runtime.running) throw new Error('Enterprise Relay runtime is not running. Start the Qiwei runtime first.');
|
|
|
|
|
+ workbench.service.setGlobal({ paused: false, defaultMode: 'review' });
|
|
|
|
|
+ return {
|
|
|
|
|
+ status: 'ok',
|
|
|
|
|
+ assistantMessage: 'Enterprise Relay keeps collecting messages; AI review and reply processing is enabled.',
|
|
|
|
|
+ data: runtime,
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
const account = await detectAccountStatus(true);
|
|
const account = await detectAccountStatus(true);
|
|
|
return startListenerForWorkbench(workbench, account);
|
|
return startListenerForWorkbench(workbench, account);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function stopListener() {
|
|
function stopListener() {
|
|
|
|
|
+ const product = getProductMode();
|
|
|
|
|
+ if (product.mode === 'enterprise') {
|
|
|
|
|
+ workbench.service.setGlobal({ paused: true, defaultMode: 'review' });
|
|
|
|
|
+ for (const conversation of workbench.db.listConversations()) {
|
|
|
|
|
+ workbench.service.setConversationMode(conversation.id, 'human');
|
|
|
|
|
+ }
|
|
|
|
|
+ return {
|
|
|
|
|
+ status: 'ok',
|
|
|
|
|
+ assistantMessage: 'Enterprise Relay continues collecting messages; AI replies are paused and conversations are in human mode.',
|
|
|
|
|
+ data: { running: false, relayRunning: true, transport: 'server_relay' },
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
const status = workbench.poller.stop();
|
|
const status = workbench.poller.stop();
|
|
|
workbench.service.setGlobal({ paused: false, defaultMode: 'review' });
|
|
workbench.service.setGlobal({ paused: false, defaultMode: 'review' });
|
|
|
for (const conversation of workbench.db.listConversations()) {
|
|
for (const conversation of workbench.db.listConversations()) {
|
|
@@ -1391,6 +1482,7 @@ module.exports = {
|
|
|
syncCustomerTaskToOfficialTodo,
|
|
syncCustomerTaskToOfficialTodo,
|
|
|
updateCustomerAlert,
|
|
updateCustomerAlert,
|
|
|
getAudit,
|
|
getAudit,
|
|
|
|
|
+ ingestRuntimeMessage,
|
|
|
startListener,
|
|
startListener,
|
|
|
stopListener,
|
|
stopListener,
|
|
|
getAgentRuntimeConfig,
|
|
getAgentRuntimeConfig,
|