feat(ai-workspace): support SEO codex provider bridge
This commit is contained in:
@@ -1257,6 +1257,8 @@ function usesJsonOutput(args) {
|
||||
return Array.isArray(args) && args.includes('--json')
|
||||
}
|
||||
|
||||
const CODEX_ASSISTANT_TEXT_MAX = 250000
|
||||
|
||||
function truncateText(value, max = 1400) {
|
||||
const text = String(value || '').trim()
|
||||
return text.length > max ? `${text.slice(0, max).trim()}...` : text
|
||||
@@ -1504,7 +1506,7 @@ function createActiveMirror(command, payload = {}, meta = {}) {
|
||||
if (kind === 'codex_files') state.currentFiles = markdownText(event.text || text, 3000)
|
||||
if (kind === 'codex_tool') state.currentTool = markdownText(event.text || text, 3000)
|
||||
if (kind === 'codex_todo') state.currentTodo = markdownText(event.text || text, 3000)
|
||||
if (kind === 'codex_message') state.assistantText = markdownText(event.text || text, 12000)
|
||||
if (kind === 'codex_message') state.assistantText = markdownText(event.text || text, CODEX_ASSISTANT_TEXT_MAX)
|
||||
if (kind === 'done' || kind === 'codex_turn_completed') state.status = 'completed'
|
||||
if (kind === 'timeout') state.status = 'timeout'
|
||||
if (kind === 'error' && !/reconnecting/i.test(text)) state.status = 'failed'
|
||||
@@ -1520,7 +1522,7 @@ function createActiveMirror(command, payload = {}, meta = {}) {
|
||||
async complete(status, assistantText = '') {
|
||||
state.status = status
|
||||
state.finishedAt = new Date().toISOString()
|
||||
if (assistantText) state.assistantText = markdownText(assistantText, 12000)
|
||||
if (assistantText) state.assistantText = markdownText(assistantText, CODEX_ASSISTANT_TEXT_MAX)
|
||||
await write({
|
||||
at: state.finishedAt,
|
||||
kind: `mirror_${status}`,
|
||||
@@ -1656,7 +1658,7 @@ function codexJsonEventToBridgeEvent(event, reasoningSummaryState = null) {
|
||||
const text = truncateText(reasoningTextFromItem(item))
|
||||
return text ? { kind: 'codex_reasoning', message: text } : null
|
||||
}
|
||||
if (itemType === 'agent_message') return { kind: 'codex_message', message: 'Assistant response received.', text: truncateText(item.text || '') }
|
||||
if (itemType === 'agent_message') return { kind: 'codex_message', message: 'Assistant response received.', text: markdownText(item.text || '', CODEX_ASSISTANT_TEXT_MAX) }
|
||||
if (itemType === 'command_execution') {
|
||||
const status = item.status ? ` - ${item.status}` : ''
|
||||
return {
|
||||
|
||||
+5
-5
@@ -9,7 +9,7 @@ import { fileURLToPath } from "node:url";
|
||||
const DEFAULT_GATEWAY = "https://ai-hub.nodedc.ru";
|
||||
const PACKAGE_NAME = "@nodedc/ai-workspace-bridge";
|
||||
const SERVICE_NAME = "NDC AI Workspace Bridge";
|
||||
const WINDOWS_TASK_NAME = "NDC AI Workspace Bridge";
|
||||
const WINDOWS_TASK_NAME = "NDCAIWorkspaceBridge";
|
||||
const HEALTH_PATH = "/api/ai-workspace/bridge/v1/health";
|
||||
const BRIDGE_PROTOCOL_VERSION = "ai-workspace-bridge.v1";
|
||||
const BRIDGE_CAPABILITIES = [
|
||||
@@ -638,7 +638,7 @@ async function installAutostart(startScriptPath, installRoot) {
|
||||
|
||||
async function installWindowsTask(startScriptPath) {
|
||||
const taskRun = `"${process.execPath}" "${startScriptPath}"`;
|
||||
runCommandQuiet("schtasks", ["/Delete", "/TN", WINDOWS_TASK_NAME, "/F"], {});
|
||||
runCommandQuiet("schtasks", ["/Delete", "/TN", WINDOWS_TASK_NAME, "/F"], { shell: false });
|
||||
await runCommand("schtasks", [
|
||||
"/Create",
|
||||
"/TN",
|
||||
@@ -650,7 +650,7 @@ async function installWindowsTask(startScriptPath) {
|
||||
"/RL",
|
||||
"HIGHEST",
|
||||
"/F",
|
||||
], {});
|
||||
], { shell: false });
|
||||
}
|
||||
|
||||
async function installLaunchAgent(startScriptPath) {
|
||||
@@ -788,7 +788,7 @@ function runCommandQuiet(command, args, options = {}) {
|
||||
const result = spawnSync(command, args, {
|
||||
env: commandEnv(options),
|
||||
encoding: "utf8",
|
||||
shell: process.platform === "win32",
|
||||
shell: options.shell ?? process.platform === "win32",
|
||||
windowsHide: true,
|
||||
});
|
||||
return {
|
||||
@@ -807,7 +807,7 @@ function runCommand(command, args, options = {}) {
|
||||
const child = spawn(command, args, {
|
||||
env: commandEnv(options),
|
||||
stdio: options.stdio || "inherit",
|
||||
shell: process.platform === "win32",
|
||||
shell: options.shell ?? process.platform === "win32",
|
||||
windowsHide: true,
|
||||
});
|
||||
child.on("error", reject);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nodedc/ai-workspace-bridge",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"description": "Install NODE.DC AI Workspace Bridge worker.",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Pool } from "pg";
|
||||
import { handleAssistantCallerRequest } from "../../ontology-core/src/assistant-action-caller.mjs";
|
||||
import { loadCatalog } from "../../ontology-core/src/catalog.mjs";
|
||||
|
||||
const SUPPORTED_SURFACES = new Set(["engine", "ops", "global"]);
|
||||
const SUPPORTED_SURFACES = new Set(["engine", "ops", "seo-mode", "global"]);
|
||||
const SUPPORTED_EXECUTOR_TYPES = new Set(["codex-remote", "ndc-agent-core"]);
|
||||
const SUPPORTED_CONNECTION_MODES = new Set(["hub", "direct"]);
|
||||
const SUPPORTED_EXECUTOR_STATUSES = new Set(["unknown", "online", "offline", "checking", "error"]);
|
||||
@@ -100,6 +100,24 @@ const APP_ROUTING_CATALOG = [
|
||||
requiredScopes: ["ops:project:read"],
|
||||
deniedText: ACCESS_DENIED_TEXT,
|
||||
},
|
||||
{
|
||||
appId: "seo-mode",
|
||||
appTitle: "NDC SEO Mode",
|
||||
surface: "seo-mode",
|
||||
skillId: "seo-mode-context",
|
||||
whenToUse: [
|
||||
"SEO model-provider tasks",
|
||||
"keyword cleaning proposals",
|
||||
"SERP interpretation proposals",
|
||||
"strategy synthesis proposals",
|
||||
"strategy quality review",
|
||||
],
|
||||
actionNamespaces: ["seo.*"],
|
||||
actionIdPrefixes: ["seo."],
|
||||
mcpServerNames: [],
|
||||
requiredScopes: ["seo:model-provider:run"],
|
||||
deniedText: ACCESS_DENIED_TEXT,
|
||||
},
|
||||
];
|
||||
const ASSISTANT_ACTION_CACHE_TTL_MS = Number(process.env.AI_WORKSPACE_ASSISTANT_ACTION_CACHE_TTL_MS || 30_000);
|
||||
const ASSISTANT_ACTION_RELAY_POLL_TIMEOUT_MS = Number(process.env.AI_WORKSPACE_ASSISTANT_RELAY_POLL_TIMEOUT_MS || 25_000);
|
||||
@@ -422,7 +440,8 @@ app.post("/api/ai-workspace/assistant/v1/executors/:executorId/agent/setup-comma
|
||||
port: installerPort,
|
||||
appMcpServers,
|
||||
});
|
||||
const command = buildBridgeSetupCommand(setupCode.code);
|
||||
const bridgePackageSpec = bridgePackageSpecForSetupCode(setupCode.code);
|
||||
const command = buildBridgeSetupCommand(setupCode.code, bridgePackageSpec);
|
||||
res.status(201).json({
|
||||
ok: true,
|
||||
owner: publicOwner(owner),
|
||||
@@ -430,7 +449,7 @@ app.post("/api/ai-workspace/assistant/v1/executors/:executorId/agent/setup-comma
|
||||
install: {
|
||||
command,
|
||||
packageName: config.bridgePackageName,
|
||||
packageSpec: config.bridgePackageSpec,
|
||||
packageSpec: bridgePackageSpec,
|
||||
gatewayUrl: config.setupGatewayUrl,
|
||||
expiresAt: setupCode.expiresAt,
|
||||
},
|
||||
@@ -980,12 +999,12 @@ function bridgeSetupPayload(executor, options = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
function buildBridgeSetupCommand(code) {
|
||||
function buildBridgeSetupCommand(code, packageSpec = config.bridgePackageSpec) {
|
||||
const parts = [
|
||||
"npx",
|
||||
"--yes",
|
||||
"--package",
|
||||
config.bridgePackageSpec,
|
||||
packageSpec,
|
||||
AI_WORKSPACE_BRIDGE_PACKAGE_BIN,
|
||||
"setup",
|
||||
code,
|
||||
@@ -995,6 +1014,13 @@ function buildBridgeSetupCommand(code) {
|
||||
return parts.join(" ");
|
||||
}
|
||||
|
||||
function bridgePackageSpecForSetupCode(code) {
|
||||
const packageSpec = String(config.bridgePackageSpec || "");
|
||||
if (!/^https?:\/\//i.test(packageSpec)) return packageSpec;
|
||||
const separator = packageSpec.includes("?") ? "&" : "?";
|
||||
return `${packageSpec}${separator}setup=${encodeURIComponent(setupCodeSuffix(code))}`;
|
||||
}
|
||||
|
||||
async function getBridgePackageTarball() {
|
||||
if (cachedBridgePackageTarball) return cachedBridgePackageTarball;
|
||||
|
||||
@@ -1217,7 +1243,7 @@ function mergeOwnerActiveContext(current, incoming) {
|
||||
};
|
||||
}
|
||||
|
||||
if ((surface === "engine" || surface === "ops") && !isPlainObject(incomingContexts[surface])) {
|
||||
if ((surface === "engine" || surface === "ops" || surface === "seo-mode") && !isPlainObject(incomingContexts[surface])) {
|
||||
const { contexts: _contexts, ...surfaceContext } = incomingContext;
|
||||
nextContexts[surface] = {
|
||||
...(isPlainObject(nextContexts[surface]) ? nextContexts[surface] : {}),
|
||||
@@ -2770,15 +2796,17 @@ function mergeSurfaceContexts(...contexts) {
|
||||
}
|
||||
const surface = normalizeKey(context.surface);
|
||||
const mode = normalizeKey(context.modeId);
|
||||
const inferredSurface = surface === "engine" || surface === "ops"
|
||||
const inferredSurface = surface === "engine" || surface === "ops" || surface === "seo-mode"
|
||||
? surface
|
||||
: mode === "ndc-agent-core"
|
||||
? "engine"
|
||||
: mode === "ops"
|
||||
? "ops"
|
||||
: mode === "seo-model-task"
|
||||
? "seo-mode"
|
||||
: "";
|
||||
if (inferredSurface) {
|
||||
const { contexts: _contexts, engineContext: _engineContext, opsContext: _opsContext, ...plain } = context;
|
||||
const { contexts: _contexts, engineContext: _engineContext, opsContext: _opsContext, seoModeContext: _seoModeContext, ...plain } = context;
|
||||
out[inferredSurface] = mergeBridgeContextObject(out[inferredSurface], plain);
|
||||
}
|
||||
}
|
||||
@@ -3373,7 +3401,7 @@ async function migrate() {
|
||||
constraint ai_workspace_threads_owner_check
|
||||
check (owner_user_id is not null or owner_email is not null),
|
||||
constraint ai_workspace_threads_surface_check
|
||||
check (origin_surface in ('engine', 'ops', 'global')),
|
||||
check (origin_surface in ('engine', 'ops', 'seo-mode', 'global')),
|
||||
constraint ai_workspace_threads_lifecycle_check
|
||||
check (lifecycle_state in ('active', 'archived'))
|
||||
)
|
||||
@@ -3420,13 +3448,27 @@ async function migrate() {
|
||||
constraint ai_workspace_runs_owner_check
|
||||
check (owner_user_id is not null or owner_email is not null),
|
||||
constraint ai_workspace_runs_surface_check
|
||||
check (origin_surface in ('engine', 'ops', 'global')),
|
||||
check (origin_surface in ('engine', 'ops', 'seo-mode', 'global')),
|
||||
constraint ai_workspace_runs_status_check
|
||||
check (status in ('running', 'completed', 'failed', 'timeout')),
|
||||
unique (owner_key, thread_id, request_id)
|
||||
)
|
||||
`);
|
||||
|
||||
await pool.query(`
|
||||
alter table ai_workspace_threads
|
||||
drop constraint if exists ai_workspace_threads_surface_check,
|
||||
add constraint ai_workspace_threads_surface_check
|
||||
check (origin_surface in ('engine', 'ops', 'seo-mode', 'global'))
|
||||
`);
|
||||
|
||||
await pool.query(`
|
||||
alter table ai_workspace_runs
|
||||
drop constraint if exists ai_workspace_runs_surface_check,
|
||||
add constraint ai_workspace_runs_surface_check
|
||||
check (origin_surface in ('engine', 'ops', 'seo-mode', 'global'))
|
||||
`);
|
||||
|
||||
await pool.query(`
|
||||
create table if not exists ai_workspace_run_events (
|
||||
id uuid primary key,
|
||||
@@ -4203,6 +4245,11 @@ function parseEntitlementAdapters() {
|
||||
token: process.env.AI_WORKSPACE_ENGINE_ENTITLEMENT_TOKEN || process.env.NDC_AI_WORKSPACE_ENGINE_ENTITLEMENT_TOKEN,
|
||||
required: process.env.AI_WORKSPACE_ENGINE_ENTITLEMENT_REQUIRED || process.env.NDC_AI_WORKSPACE_ENGINE_ENTITLEMENT_REQUIRED,
|
||||
});
|
||||
collectEntitlementAdapter(adapters, "seo-mode", {
|
||||
url: process.env.AI_WORKSPACE_SEO_MODE_ENTITLEMENT_URL || process.env.NDC_AI_WORKSPACE_SEO_MODE_ENTITLEMENT_URL,
|
||||
token: process.env.AI_WORKSPACE_SEO_MODE_ENTITLEMENT_TOKEN || process.env.NDC_AI_WORKSPACE_SEO_MODE_ENTITLEMENT_TOKEN,
|
||||
required: process.env.AI_WORKSPACE_SEO_MODE_ENTITLEMENT_REQUIRED || process.env.NDC_AI_WORKSPACE_SEO_MODE_ENTITLEMENT_REQUIRED,
|
||||
});
|
||||
collectEntitlementAdapter(adapters, "launcher", {
|
||||
url:
|
||||
process.env.AI_WORKSPACE_LAUNCHER_ENTITLEMENT_URL ||
|
||||
@@ -4228,6 +4275,7 @@ function parseEntitlementAdapters() {
|
||||
const order = new Map([
|
||||
["ops", 10],
|
||||
["engine", 20],
|
||||
["seo-mode", 30],
|
||||
["launcher", 90],
|
||||
]);
|
||||
return Array.from(byAppId.values())
|
||||
|
||||
@@ -1962,6 +1962,8 @@ function usesJsonOutput(args) {
|
||||
return Array.isArray(args) && args.includes('--json')
|
||||
}
|
||||
|
||||
const CODEX_ASSISTANT_TEXT_MAX = 250000
|
||||
|
||||
function truncateText(value, max = 1400) {
|
||||
const text = String(value || '').trim()
|
||||
return text.length > max ? `${text.slice(0, max).trim()}...` : text
|
||||
@@ -2209,7 +2211,7 @@ function createActiveMirror(command, payload = {}, meta = {}) {
|
||||
if (kind === 'codex_files') state.currentFiles = markdownText(event.text || text, 3000)
|
||||
if (kind === 'codex_tool') state.currentTool = markdownText(event.text || text, 3000)
|
||||
if (kind === 'codex_todo') state.currentTodo = markdownText(event.text || text, 3000)
|
||||
if (kind === 'codex_message') state.assistantText = markdownText(event.text || text, 12000)
|
||||
if (kind === 'codex_message') state.assistantText = markdownText(event.text || text, CODEX_ASSISTANT_TEXT_MAX)
|
||||
if (kind === 'done' || kind === 'codex_turn_completed') state.status = 'completed'
|
||||
if (kind === 'timeout') state.status = 'timeout'
|
||||
if (kind === 'error' && !/reconnecting/i.test(text)) state.status = 'failed'
|
||||
@@ -2225,7 +2227,7 @@ function createActiveMirror(command, payload = {}, meta = {}) {
|
||||
async complete(status, assistantText = '') {
|
||||
state.status = status
|
||||
state.finishedAt = new Date().toISOString()
|
||||
if (assistantText) state.assistantText = markdownText(assistantText, 12000)
|
||||
if (assistantText) state.assistantText = markdownText(assistantText, CODEX_ASSISTANT_TEXT_MAX)
|
||||
await write({
|
||||
at: state.finishedAt,
|
||||
kind: `mirror_${status}`,
|
||||
@@ -2361,7 +2363,7 @@ function codexJsonEventToBridgeEvent(event, reasoningSummaryState = null) {
|
||||
const text = truncateText(reasoningTextFromItem(item))
|
||||
return text ? { kind: 'codex_reasoning', message: text } : null
|
||||
}
|
||||
if (itemType === 'agent_message') return { kind: 'codex_message', message: 'Assistant response received.', text: truncateText(item.text || '') }
|
||||
if (itemType === 'agent_message') return { kind: 'codex_message', message: 'Assistant response received.', text: markdownText(item.text || '', CODEX_ASSISTANT_TEXT_MAX) }
|
||||
if (itemType === 'command_execution') {
|
||||
const status = item.status ? ` - ${item.status}` : ''
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user