feat(page-ai): add resumable run journal descriptors
This commit is contained in:
@@ -27,10 +27,12 @@ import { readFileSync, existsSync } from 'node:fs';
|
||||
import { homedir } from 'node:os';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { execFileSync, spawn } from 'node:child_process';
|
||||
|
||||
const MNOTE_WEB_URL = process.env.MNOTE_WEB_URL || 'http://127.0.0.1:3000';
|
||||
const DEBUG = process.env.MNOTE_REASONIX_ACP_DEBUG === '1';
|
||||
const REASONIX_BIN = process.env.MNOTE_REASONIX_BIN || 'reasonix';
|
||||
const REASONIX_ACP_BACKEND = (process.env.MNOTE_REASONIX_ACP_BACKEND || 'auto').trim().toLowerCase();
|
||||
|
||||
function debugLog(message) {
|
||||
if (DEBUG) process.stderr.write(`${message}\n`);
|
||||
@@ -74,6 +76,75 @@ function assertSameSortedSet(actual, expected, label) {
|
||||
}
|
||||
}
|
||||
|
||||
function parseReasonixMajorVersion(versionText) {
|
||||
const text = String(versionText || '');
|
||||
const match = text.match(/\b(?:npm-v|v)?(\d+)(?:\.\d+){0,2}(?:[-+][\w.-]+)?\b/);
|
||||
if (!match) return null;
|
||||
const major = Number.parseInt(match[1], 10);
|
||||
return Number.isFinite(major) ? major : null;
|
||||
}
|
||||
|
||||
function readReasonixVersionText() {
|
||||
try {
|
||||
return execFileSync(REASONIX_BIN, ['--version'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
|
||||
} catch (error) {
|
||||
debugLog(`[reasonix-acp-mnote] unable to read ${REASONIX_BIN} --version: ${error instanceof Error ? error.message : String(error)}`);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function shouldUseNativeReasonixAcp(versionText = readReasonixVersionText()) {
|
||||
if (REASONIX_ACP_BACKEND === 'legacy') return false;
|
||||
if (REASONIX_ACP_BACKEND === 'native') return true;
|
||||
const major = parseReasonixMajorVersion(versionText);
|
||||
return major !== null && major >= 1;
|
||||
}
|
||||
|
||||
function nativeReasonixAcpArgs() {
|
||||
const args = ['acp'];
|
||||
const model = (process.env.REASONIX_MODEL || '').trim();
|
||||
if (model) args.push('-model', model);
|
||||
const extraArgsText = (process.env.MNOTE_REASONIX_ACP_ARGS_JSON || '').trim();
|
||||
if (extraArgsText) {
|
||||
let extraArgs;
|
||||
try {
|
||||
extraArgs = JSON.parse(extraArgsText);
|
||||
} catch (error) {
|
||||
throw new Error(`MNOTE_REASONIX_ACP_ARGS_JSON must be a JSON array of strings: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
if (!Array.isArray(extraArgs) || extraArgs.some((value) => typeof value !== 'string')) {
|
||||
throw new Error('MNOTE_REASONIX_ACP_ARGS_JSON must be a JSON array of strings');
|
||||
}
|
||||
args.push(...extraArgs);
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
function proxyNativeReasonixAcp(versionText = readReasonixVersionText()) {
|
||||
const args = nativeReasonixAcpArgs();
|
||||
debugLog(`[reasonix-acp-mnote] delegating to native ${REASONIX_BIN} ${args.join(' ')} (${versionText || 'version unknown'})`);
|
||||
const child = spawn(REASONIX_BIN, args, {
|
||||
stdio: ['inherit', 'inherit', 'inherit'],
|
||||
env: process.env,
|
||||
});
|
||||
for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
|
||||
process.on(signal, () => {
|
||||
if (!child.killed) child.kill(signal);
|
||||
});
|
||||
}
|
||||
child.on('error', (error) => {
|
||||
process.stderr.write(`FATAL: failed to spawn native Reasonix ACP (${REASONIX_BIN}): ${error.message}\n`);
|
||||
process.exit(1);
|
||||
});
|
||||
child.on('exit', (code, signal) => {
|
||||
if (signal) {
|
||||
process.kill(process.pid, signal);
|
||||
return;
|
||||
}
|
||||
process.exit(code === null ? 1 : code);
|
||||
});
|
||||
}
|
||||
|
||||
function isWriteMnoteTool(toolName) {
|
||||
return ![
|
||||
'mnote.skill.read',
|
||||
@@ -89,6 +160,22 @@ function isWriteMnoteTool(toolName) {
|
||||
].includes(toolName);
|
||||
}
|
||||
|
||||
function toolResultStatusFromContent(content) {
|
||||
const text = String(content || '');
|
||||
if (!text.trim()) return 'completed';
|
||||
try {
|
||||
const payload = JSON.parse(text);
|
||||
if (payload && typeof payload === 'object') {
|
||||
if (payload.ok === false) return 'failed';
|
||||
if (payload.error && payload.error !== null) return 'failed';
|
||||
return 'completed';
|
||||
}
|
||||
} catch {
|
||||
// 非 JSON 工具结果按普通文本处理,只识别明确错误前缀。
|
||||
}
|
||||
return /^\s*(error|failed|exception)\b/i.test(text) ? 'failed' : 'completed';
|
||||
}
|
||||
|
||||
function stableIdPart(value, fallback) {
|
||||
const text = String(value || fallback || '')
|
||||
.trim()
|
||||
@@ -181,6 +268,15 @@ function buildMnoteToolPayload(toolName, rawArgs = {}, context = {}) {
|
||||
}
|
||||
|
||||
if (process.env.MNOTE_REASONIX_ACP_SELFTEST === '1') {
|
||||
if (parseReasonixMajorVersion('reasonix 0.53.2') !== 0) {
|
||||
throw new Error('selftest expected legacy Reasonix 0.x version parsing');
|
||||
}
|
||||
if (parseReasonixMajorVersion('reasonix npm-v1.4.0-rc.1') !== 1) {
|
||||
throw new Error('selftest expected native Reasonix 1.x version parsing');
|
||||
}
|
||||
if (parseReasonixMajorVersion('unversioned') !== null) {
|
||||
throw new Error('selftest expected unversioned Reasonix output to be unknown');
|
||||
}
|
||||
const payload = buildMnoteToolPayload(
|
||||
'mnote.context.read_current_page',
|
||||
{
|
||||
@@ -331,6 +427,14 @@ if (process.env.MNOTE_REASONIX_ACP_SELFTEST === '1') {
|
||||
rustKnowledgeTools,
|
||||
'selftest expected Reasonix wrapper tool mapping to match Rust knowledge RAG manifest tools',
|
||||
);
|
||||
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
||||
const knowledgeSkillPath = join(scriptDir, '..', 'skills', 'mnote-knowledge-rag', 'SKILL.md');
|
||||
const knowledgeSkillText = readFileSync(knowledgeSkillPath, 'utf8');
|
||||
for (const toolName of rustKnowledgeTools) {
|
||||
if (!knowledgeSkillText.includes(toolName)) {
|
||||
throw new Error(`selftest expected mnote-knowledge-rag skill text to mention ${toolName}`);
|
||||
}
|
||||
}
|
||||
process.stderr.write('[reasonix-acp-mnote] selftest ok\n');
|
||||
process.exit(0);
|
||||
}
|
||||
@@ -369,6 +473,11 @@ if (!DEEPSEEK_API_KEY) {
|
||||
|
||||
process.env.DEEPSEEK_API_KEY = DEEPSEEK_API_KEY;
|
||||
|
||||
const reasonixVersionText = readReasonixVersionText();
|
||||
if (shouldUseNativeReasonixAcp(reasonixVersionText)) {
|
||||
proxyNativeReasonixAcp(reasonixVersionText);
|
||||
} else {
|
||||
|
||||
// ── Dynamic import of reasonix public API ────────────
|
||||
|
||||
let CacheFirstLoop, DeepSeekClient, ToolRegistry, ImmutablePrefix;
|
||||
@@ -533,22 +642,6 @@ function emitToolResult(sessionId, toolCallId, status, text) {
|
||||
});
|
||||
}
|
||||
|
||||
function toolResultStatusFromContent(content) {
|
||||
const text = String(content || '');
|
||||
if (!text.trim()) return 'completed';
|
||||
try {
|
||||
const payload = JSON.parse(text);
|
||||
if (payload && typeof payload === 'object') {
|
||||
if (payload.ok === false) return 'failed';
|
||||
if (payload.error && payload.error !== null) return 'failed';
|
||||
return 'completed';
|
||||
}
|
||||
} catch {
|
||||
// 非 JSON 工具结果按普通文本处理,只识别明确错误前缀。
|
||||
}
|
||||
return /^\s*(error|failed|exception)\b/i.test(text) ? 'failed' : 'completed';
|
||||
}
|
||||
|
||||
function emitUsage(sessionId, used, size) {
|
||||
emitSessionUpdate(sessionId, {
|
||||
sessionUpdate: 'usage_update',
|
||||
@@ -1129,3 +1222,4 @@ for (const raw of queuedRpcLines.splice(0)) {
|
||||
await handleRpcLine(raw);
|
||||
}
|
||||
process.stderr.write(`[reasonix-acp-mnote] ready (mnote=${MNOTE_WEB_URL})\n`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user