ci: auto build & obfuscate [2026-05-16 19:16:28] (Jenkins #21)

This commit is contained in:
Jenkins CI
2026-05-16 19:16:28 +08:00
parent 4bc6e0a047
commit d9fa3072a2
46 changed files with 4154 additions and 1584 deletions

View File

@@ -1,100 +1,31 @@
/**
* @file 旧版 <Amily2Edit> 文本格式的解析器 + executeCommands 入口。
*
* Phase 0 重构后职责收窄:
* - 仅负责把 LLM 返回的文本块解析成 Operation[]legacy formatter 角色)
* - 推演下推到 actions/applyOperations.js本文件不再持有 insertRow/updateRow/deleteRow 实现
*
* 对外 API
* - parseToOperations(text) : 纯解析,文本 → Op[]Phase B legacy formatter 直接复用)
* - executeCommands(text, state) : 解析 + 推演,返回历史 shape { finalState, hasChanges, changes }
*
* 等 Phase B 引入 formatters/ 目录后,本文件改名为 formatters/legacy.js。
*
* @typedef {import('./dto/Operation.js').Operation} Operation
* @typedef {import('./dto/Table.js').TableState} TableState
*/
import { log } from './logger.js';
import { applyOperations } from './actions/applyOperations.js';
function insertRow(state, tableIndex, data) {
if (!state[tableIndex]) {
log(`AI指令错误尝试在不存在的表格索引 ${tableIndex} 中插入行。`, 'error');
return { state, changes: [] };
}
// 【安全检查】确保 data 是对象
if (typeof data !== 'object' || data === null) {
log(`AI指令错误insertRow 的 data 参数必须是对象,实际收到: ${typeof data} (${data})`, 'error');
return { state, changes: [] };
}
const table = state[tableIndex];
const colCount = table.headers.length;
const newRow = Array(colCount).fill('');
const changes = [];
const newRowIndex = table.rows.length;
for (const colIndex in data) {
const cIndex = parseInt(colIndex, 10);
if (cIndex < colCount) {
newRow[cIndex] = data[colIndex];
changes.push({ type: 'update', tableIndex, rowIndex: newRowIndex, colIndex: cIndex });
}
}
table.rows.push(newRow);
// 同步更新 rowStatuses
if (!table.rowStatuses) {
table.rowStatuses = Array(table.rows.length - 1).fill('normal');
}
table.rowStatuses.push('normal');
return { state, changes };
}
function updateRow(state, tableIndex, rowIndex, data) {
if (!state[tableIndex]) {
log(`AI指令错误尝试更新不存在的表格 ${tableIndex}`, 'error');
return { state, changes: [] };
}
// 【安全检查】确保 data 是对象
if (typeof data !== 'object' || data === null) {
log(`AI指令错误updateRow 的 data 参数必须是对象,实际收到: ${typeof data} (${data})`, 'error');
return { state, changes: [] };
}
const table = state[tableIndex];
if (rowIndex >= table.rows.length) {
log(`AI指令修正updateRow 的行索引 ${rowIndex} 超出范围,自动转换为 insertRow。`, 'warn');
return insertRow(state, tableIndex, data);
}
const row = table.rows[rowIndex];
const changes = [];
for (const colIndex in data) {
const cIndex = parseInt(colIndex, 10);
if (cIndex < row.length) {
row[cIndex] = data[colIndex];
changes.push({ type: 'update', tableIndex, rowIndex, colIndex: cIndex });
}
}
return { state, changes };
}
function deleteRow(state, tableIndex, rowIndex) {
const table = state[tableIndex];
if (!table || !table.rows[rowIndex]) {
log(`AI指令错误尝试删除不存在的表格 ${tableIndex} 或行 ${rowIndex}`, 'error');
return { state, changes: [] };
}
if (!table.rowStatuses) {
table.rowStatuses = Array(table.rows.length).fill('normal');
}
if (table.rowStatuses[rowIndex] !== 'pending-deletion') {
table.rowStatuses[rowIndex] = 'pending-deletion';
const changes = [{ type: 'delete', tableIndex, rowIndex }];
return { state, changes };
}
return { state, changes: [] };
}
const allowedFunctions = {
insertRow,
updateRow,
deleteRow,
};
const ALLOWED_FN_NAMES = new Set(['insertRow', 'updateRow', 'deleteRow']);
/**
* 把单行函数调用文本解析为 { name, args } 中间表示。
* 内部用不导出。args 是位置参数数组,待 _argsToOperation 转成 Operation 对象。
* @param {string} callString
* @returns {{ name: string, args: any[] } | null}
*/
function parseFunctionCall(callString) {
const match = callString.trim().match(/(\w+)\((.*)\)/);
if (!match) {
@@ -105,7 +36,7 @@ function parseFunctionCall(callString) {
const functionName = match[1];
const argsString = match[2];
if (!allowedFunctions[functionName]) {
if (!ALLOWED_FN_NAMES.has(functionName)) {
log(`检测到非法函数调用: "${functionName}"。已阻止执行。`, 'error');
return null;
}
@@ -116,11 +47,11 @@ function parseFunctionCall(callString) {
let currentArg = '';
let inQuote = false;
let quoteChar = '';
let braceDepth = 0;
let braceDepth = 0;
for (let i = 0; i < argsString.length; i++) {
const char = argsString[i];
if ((char === '"' || char === "'") && (i === 0 || argsString[i-1] !== '\\')) {
if (!inQuote) {
inQuote = true;
@@ -164,7 +95,7 @@ function parseValue(val) {
if (val === 'null') return null;
if (val === 'undefined') return undefined;
if (!isNaN(Number(val)) && val !== '') return Number(val);
if (val.startsWith('"') && val.endsWith('"')) {
try { return JSON.parse(val); } catch (e) { return val.slice(1, -1); }
}
@@ -203,14 +134,14 @@ function parseValue(val) {
function tryParseObject(str) {
if (!str.startsWith('{') || !str.endsWith('}')) return null;
let content = str.slice(1, -1);
const result = {};
let hasMatch = false;
const strings = [];
let placeholderIndex = 0;
// 提取字符串并替换为占位符,避免正则在字符串内部匹配
const stringRegex = /"([^"\\]|\\.)*"|'([^'\\]|\\.)*'/g;
content = content.replace(stringRegex, (match) => {
@@ -219,36 +150,36 @@ function tryParseObject(str) {
placeholderIndex++;
return placeholder;
});
// 匹配键:(开头或逗号/分号/冒号) + (数字 或 字母数字下划线 或 占位符) + 冒号
const keyRegex = /(?:^|[,;:]+\s*)(?:(\d+)|([a-zA-Z0-9_]+)|(__STR_\d+__))\s*:/g;
let match;
let lastIndex = 0;
let lastKey = null;
while ((match = keyRegex.exec(content)) !== null) {
hasMatch = true;
if (lastKey !== null) {
let valStr = content.slice(lastIndex, match.index).trim();
valStr = valStr.replace(/[,;:]+$/, '').trim();
let actualKey = restoreStrings(lastKey, strings);
result[actualKey] = restoreStrings(valStr, strings);
}
lastKey = match[1] || match[2] || match[3];
lastIndex = match.index + match[0].length;
}
if (lastKey !== null) {
let valStr = content.slice(lastIndex).trim();
valStr = valStr.replace(/[,;:]+$/, '').trim();
let actualKey = restoreStrings(lastKey, strings);
result[actualKey] = restoreStrings(valStr, strings);
}
return hasMatch ? result : null;
}
@@ -269,51 +200,77 @@ function cleanValueStr(str) {
return str;
}
/**
* 把 parseFunctionCall 返回的位置参数数组转成 Operation 对象。
* @param {string} name
* @param {any[]} args
* @returns {Operation | null}
*/
function _argsToOperation(name, args) {
if (name === 'insertRow') {
return /** @type {Operation} */ ({ op: 'insertRow', tableIndex: args[0], data: args[1] });
}
if (name === 'updateRow') {
return /** @type {Operation} */ ({ op: 'updateRow', tableIndex: args[0], rowIndex: args[1], data: args[2] });
}
if (name === 'deleteRow') {
return /** @type {Operation} */ ({ op: 'deleteRow', tableIndex: args[0], rowIndex: args[1] });
}
return null;
}
export function executeCommands(aiResponseText, initialState) {
/**
* 把 LLM 返回的文本块解析为 Operation[]。
* 不在文本中找到 <Amily2Edit> 块时返回空数组(不视为错误)。
*
* @param {string} aiResponseText
* @returns {Operation[]}
*/
export function parseToOperations(aiResponseText) {
const commandBlockRegex = /<Amily2Edit>([\s\S]*?)<\/Amily2Edit>/;
const match = aiResponseText.match(commandBlockRegex);
const match = (aiResponseText || '').match(commandBlockRegex);
if (!match) return [];
if (!match) {
return { finalState: initialState, hasChanges: false, changes: [] };
}
log('检测到AI指令块开始推演...', 'info');
const commandBlock = match[1].replace(/<!--|-->/g, '').trim();
if (!commandBlock) {
return { finalState: initialState, hasChanges: false, changes: [] };
}
if (!commandBlock) return [];
const commands = commandBlock.split('\n').filter(line => line.trim() !== '');
if (commands.length === 0) {
if (commands.length === 0) return [];
/** @type {Operation[]} */
const ops = [];
for (const commandString of commands) {
const trimmed = commandString.trim();
if (!trimmed.startsWith('insertRow(') &&
!trimmed.startsWith('updateRow(') &&
!trimmed.startsWith('deleteRow(')) {
continue;
}
const parsed = parseFunctionCall(trimmed);
if (!parsed) continue;
const op = _argsToOperation(parsed.name, parsed.args);
if (op) ops.push(op);
}
return ops;
}
/**
* 解析 LLM 文本指令并推演到 state 上。
* 历史 API调用方期望返回 { finalState, hasChanges, changes }。
*
* @param {string} aiResponseText
* @param {TableState} initialState
* @returns {{ finalState: TableState, hasChanges: boolean, changes: import('./dto/Change.js').Change[] }}
*/
export function executeCommands(aiResponseText, initialState) {
const ops = parseToOperations(aiResponseText);
if (ops.length === 0) {
return { finalState: initialState, hasChanges: false, changes: [] };
}
let currentState = JSON.parse(JSON.stringify(initialState));
let allChanges = [];
log(`检测到 ${ops.length} 条 AI 指令,开始推演...`, 'info');
commands.forEach(commandString => {
const trimmedCommand = commandString.trim();
if (trimmedCommand.startsWith('insertRow(') ||
trimmedCommand.startsWith('deleteRow(') ||
trimmedCommand.startsWith('updateRow('))
{
const parsed = parseFunctionCall(trimmedCommand);
if (parsed) {
try {
const result = allowedFunctions[parsed.name](currentState, ...parsed.args);
currentState = result.state;
if (result.changes && result.changes.length > 0) {
allChanges = allChanges.concat(result.changes);
}
log(`成功推演指令: ${commandString}`, 'success');
} catch (e) {
log(`推演指令 "${commandString}" 时发生运行时错误: ${e.message}`, 'error');
}
}
}
});
const hasChanges = allChanges.length > 0;
return { finalState: currentState, hasChanges, changes: allChanges };
const { state, changes } = applyOperations(initialState, ops);
return { finalState: state, hasChanges: changes.length > 0, changes };
}