mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-09-05 01:30:54 +03:00
Merge branch 'dev-web'
This commit is contained in:
@@ -106,6 +106,13 @@ await navigateLink('РегистрНакопления.ЗаказыКлиент
|
|||||||
await navigateLink('Справочник.Контрагенты');
|
await navigateLink('Справочник.Контрагенты');
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### `openFile(path)` → form state
|
||||||
|
Open an external data processor or report (EPF/ERF) via File → Open. Handles the security confirmation dialog automatically.
|
||||||
|
```js
|
||||||
|
const form = await openFile('C:\\WS\\build\\МояОбработка.epf');
|
||||||
|
const form = await openFile('build/МояОбработка.epf'); // relative paths work too
|
||||||
|
```
|
||||||
|
|
||||||
#### `switchTab(name)` → form state
|
#### `switchTab(name)` → form state
|
||||||
Switch to an already-open tab/window (fuzzy match).
|
Switch to an already-open tab/window (fuzzy match).
|
||||||
|
|
||||||
|
|||||||
@@ -400,6 +400,105 @@ function normalizeE1cibUrl(url) {
|
|||||||
return `e1cib/list/${url}`;
|
return `e1cib/list/${url}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open an external data processor or report (EPF/ERF) via File → Open menu.
|
||||||
|
* Handles the security confirmation dialog on first open.
|
||||||
|
* @param {string} filePath - path to EPF/ERF file (absolute or relative to cwd)
|
||||||
|
* @returns {Promise<object>} form state of the opened processor/report
|
||||||
|
*/
|
||||||
|
export async function openFile(filePath) {
|
||||||
|
ensureConnected();
|
||||||
|
await dismissPendingErrors();
|
||||||
|
const absPath = pathResolve(filePath);
|
||||||
|
|
||||||
|
const MAX_ATTEMPTS = 2; // 1st may trigger security dialog, 2nd is the real open
|
||||||
|
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
||||||
|
const formBefore = await page.evaluate(detectFormScript());
|
||||||
|
|
||||||
|
// 1. Ctrl+O opens 1C's "Выбор файлов" dialog
|
||||||
|
await page.keyboard.press('Control+o');
|
||||||
|
|
||||||
|
// 2. Wait for the file selection dialog
|
||||||
|
const dialogOk = await waitForCondition(`(() => {
|
||||||
|
const ok = document.querySelector('#fileSelectDialogOk');
|
||||||
|
return ok && ok.offsetWidth > 0 ? true : false;
|
||||||
|
})()`, 3000);
|
||||||
|
if (!dialogOk) throw new Error("File selection dialog did not open (Ctrl+O)");
|
||||||
|
|
||||||
|
// 3. Click "выберите с диска" to trigger the native OS file picker
|
||||||
|
let fileChooser;
|
||||||
|
try {
|
||||||
|
[fileChooser] = await Promise.all([
|
||||||
|
page.waitForEvent('filechooser', { timeout: 5000 }),
|
||||||
|
page.click('a.underline.pointer'),
|
||||||
|
]);
|
||||||
|
} catch (e) {
|
||||||
|
// Try closing the dialog before throwing
|
||||||
|
await page.keyboard.press('Escape');
|
||||||
|
throw new Error(`File chooser did not appear: ${e.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Set the file path and click OK
|
||||||
|
await fileChooser.setFiles(absPath);
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
await page.click('#fileSelectDialogOk');
|
||||||
|
await waitForStable(formBefore);
|
||||||
|
|
||||||
|
// 5. Check for security dialog
|
||||||
|
const err = await checkForErrors();
|
||||||
|
if (err?.confirmation) {
|
||||||
|
// Security confirmation — click the positive button (Продолжить/Да/OK)
|
||||||
|
const positiveBtn = err.confirmation.buttons.find(b =>
|
||||||
|
/продолжить|да|ok|yes|открыть/i.test(b)
|
||||||
|
) || err.confirmation.buttons[0];
|
||||||
|
if (positiveBtn) {
|
||||||
|
const btns = await page.$$(`#form${err.confirmation.formNum}_container a.press.pressButton`);
|
||||||
|
for (const b of btns) {
|
||||||
|
const txt = (await b.textContent())?.trim();
|
||||||
|
if (txt === positiveBtn) { await b.click(); break; }
|
||||||
|
}
|
||||||
|
await waitForStable(formBefore);
|
||||||
|
}
|
||||||
|
// After confirmation, check if EPF form appeared or a follow-up dialog showed.
|
||||||
|
// Check form change FIRST — avoids confusing a small EPF form with a modal dialog.
|
||||||
|
const formAfter = await page.evaluate(detectFormScript());
|
||||||
|
if (formAfter != null && formAfter !== formBefore) {
|
||||||
|
// New form appeared — but is it the EPF or an informational dialog?
|
||||||
|
// Informational "re-open" dialogs are tiny (< 20 elements).
|
||||||
|
const elCount = await page.evaluate(`document.querySelectorAll('[id^="form${formAfter}_"]').length`);
|
||||||
|
if (elCount < 20) {
|
||||||
|
// Likely an info dialog — check and dismiss
|
||||||
|
const err2 = await checkForErrors();
|
||||||
|
if (err2?.modal) {
|
||||||
|
await dismissPendingErrors();
|
||||||
|
await waitForStable(formBefore);
|
||||||
|
continue; // retry open cycle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// It's the real EPF form
|
||||||
|
const state = await getFormState();
|
||||||
|
state.opened = { file: absPath, attempt: attempt + 1 };
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
// Form didn't appear — retry
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No security dialog — check if form appeared
|
||||||
|
if (err?.modal) {
|
||||||
|
throw new Error(`Error opening file: ${err.modal.message}`);
|
||||||
|
}
|
||||||
|
const formAfter = await page.evaluate(detectFormScript());
|
||||||
|
if (formAfter != null && formAfter !== formBefore) {
|
||||||
|
const state = await getFormState();
|
||||||
|
state.opened = { file: absPath, attempt: attempt + 1 };
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Form did not open after ${MAX_ATTEMPTS} attempts for: ${absPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
/** Navigate to a 1C navigation link via Shift+F11 dialog. Returns new form state. */
|
/** Navigate to a 1C navigation link via Shift+F11 dialog. Returns new form state. */
|
||||||
export async function navigateLink(url) {
|
export async function navigateLink(url) {
|
||||||
ensureConnected();
|
ensureConnected();
|
||||||
|
|||||||
@@ -889,8 +889,11 @@ export function checkErrorsScript() {
|
|||||||
if (elCount > 100) continue; // Skip large content forms
|
if (elCount > 100) continue; // Skip large content forms
|
||||||
if (buttons.length > 1) {
|
if (buttons.length > 1) {
|
||||||
// Confirmation dialog (multiple buttons: Да/Нет, OK/Отмена, etc.)
|
// Confirmation dialog (multiple buttons: Да/Нет, OK/Отмена, etc.)
|
||||||
|
// Must have a Message element — real 1C confirmations always have form{N}_Message.
|
||||||
|
// Without it, this is just a regular form with multiple buttons (e.g. EPF form).
|
||||||
const msgEl = document.getElementById(p + 'Message');
|
const msgEl = document.getElementById(p + 'Message');
|
||||||
const message = msgEl?.innerText?.trim() || '';
|
if (!msgEl || msgEl.offsetWidth === 0) continue;
|
||||||
|
const message = msgEl.innerText?.trim() || '';
|
||||||
const btnNames = buttons.map(el => {
|
const btnNames = buttons.map(el => {
|
||||||
const b = { name: el.innerText?.trim() || '' };
|
const b = { name: el.innerText?.trim() || '' };
|
||||||
if (el.classList.contains('pressDefault')) b.default = true;
|
if (el.classList.contains('pressDefault')) b.default = true;
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ async function executeScript(code) {
|
|||||||
// and stop execution immediately with diagnostic info
|
// and stop execution immediately with diagnostic info
|
||||||
const ACTION_FNS = [
|
const ACTION_FNS = [
|
||||||
'clickElement', 'fillFields', 'selectValue', 'fillTableRow',
|
'clickElement', 'fillFields', 'selectValue', 'fillTableRow',
|
||||||
'deleteTableRow', 'openCommand', 'navigateSection', 'navigateLink',
|
'deleteTableRow', 'openCommand', 'navigateSection', 'navigateLink', 'openFile',
|
||||||
'closeForm', 'filterList', 'unfilterList'
|
'closeForm', 'filterList', 'unfilterList'
|
||||||
];
|
];
|
||||||
for (const name of ACTION_FNS) {
|
for (const name of ACTION_FNS) {
|
||||||
|
|||||||
@@ -88,6 +88,14 @@ Claude напишет сценарий, который сформирует от
|
|||||||
|
|
||||||
Claude загрузит расширение через `/db-load-xml`, затем через `/web-test` откроет форму и проверит ожидаемое поведение.
|
Claude загрузит расширение через `/db-load-xml`, затем через `/web-test` откроет форму и проверит ожидаемое поведение.
|
||||||
|
|
||||||
|
### Открытие внешней обработки
|
||||||
|
|
||||||
|
```
|
||||||
|
> Открой обработку build/РедакторДвижений.epf в веб-клиенте и покажи что на форме
|
||||||
|
```
|
||||||
|
|
||||||
|
Claude откроет EPF через Ctrl+O, автоматически обработает диалог безопасности (если есть) и прочитает форму.
|
||||||
|
|
||||||
### Пошаговая отладка
|
### Пошаговая отладка
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -190,6 +198,7 @@ await closeForm({ save: false });
|
|||||||
| `navigateSection(name)` | Перейти в раздел (fuzzy match) | `{ sections, commands }` |
|
| `navigateSection(name)` | Перейти в раздел (fuzzy match) | `{ sections, commands }` |
|
||||||
| `openCommand(name)` | Открыть команду из панели функций | form state |
|
| `openCommand(name)` | Открыть команду из панели функций | form state |
|
||||||
| `navigateLink(path)` | Открыть по пути метаданных (`Документ.ЗаказКлиента`) | form state |
|
| `navigateLink(path)` | Открыть по пути метаданных (`Документ.ЗаказКлиента`) | form state |
|
||||||
|
| `openFile(path)` | Открыть внешнюю обработку/отчёт (EPF/ERF) через «Файл → Открыть» | form state |
|
||||||
| `switchTab(name)` | Переключить открытую вкладку | form state |
|
| `switchTab(name)` | Переключить открытую вкладку | form state |
|
||||||
|
|
||||||
### Чтение
|
### Чтение
|
||||||
|
|||||||
Reference in New Issue
Block a user