Перейти к содержимому
FastOffice

Связи книги

Формула считает по чужой книге: список связей, их состояние и адрес источника приходят от ядра, а открывает книгу хозяин окна.

формула считает по чужой книге: список связей, их состояние и адрес источника приходят от ядра, а открывает книгу хозяин окна
открыть в новой вкладке ↗Работающий пример. Ниже — его код целиком, файл за файлом.

В рамке выше работает тот самый проект, чей код лежит ниже, — собранный, но не переписанный: те же файлы, только пропущенные через сборку. Считает в нём не ядро, а дублёр транспорта, поэтому числа в ячейках ничего не значат: смотреть нужно на код.

Страница, которую открывает браузер.

index.html
<!doctype html>
<!--
Связи книги — НАСТОЯЩИЙ МАЛЕНЬКИЙ ПРОЕКТ.
Не «страница витрины», а тот проект, который встраивающий копирует к себе
целиком. Галерея примеров ЗАПУСКАЕТ эти файлы и ПОКАЗЫВАЕТ их же.
-->
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Связи книги — @f1</title>
</head>
<body>
<div data-example="workbook-links" class="flex h-screen w-screen flex-col bg-base-100 text-base-content">
<header class="border-b border-base-300 px-4 py-2">
<div class="text-sm font-semibold">Связи книги: формула считает по чужой книге</div>
<div class="text-xs opacity-70">
Восемь намерений одной операции <code>workbook_links_manager</code>: перечислить, решить о
доверии при открытии, обновить значения, сменить источник, узнать адрес источника,
разорвать, отменить, повторить. Ни одна внешняя ссылка при этом не разбирается в оболочке —
список, состояние каждой связи и то, чему равно её значение, приходят из-за шва.
</div>
</header>
<section class="m-4 flex flex-col gap-3 rounded-card border border-base-300 p-4" data-links-panel>
<!-- Подписи — Excel для macOS, область «Связи книги» (те же строки
ресурсов, что у нашего окна: `strings.resjson` области). -->
<div class="flex flex-wrap items-end gap-2">
<button data-links-update class="btn btn-sm btn-primary">Обновить все</button>
<button data-links-trust class="btn btn-sm">Всегда обновлять</button>
<button data-links-open class="btn btn-sm">Открыть книгу</button>
<label class="form-control grow">
<span class="label-text text-xs">Изменить источник</span>
<input
data-links-source
value="/Пользователи/общий/Курсы 2026.xlsx"
class="input input-sm input-bordered w-full"
/>
</label>
<button data-links-change class="btn btn-sm">Изменить источник</button>
<button data-links-break class="btn btn-sm">Разорвать все</button>
</div>
<div data-links-answer class="text-xs opacity-60">ещё не спрашивали</div>
<div data-links-list class="flex flex-col gap-1 text-xs"></div>
<p class="text-xs opacity-60">
«Открыть книгу» отдаёт АДРЕС источника: открывает его хозяин окна, а не SDK — файловой
системы за швом нет вовсе. А «Обновить все» до решения о доверии отвечает
<code>external-source-not-trusted</code> — успешным конвертом и с причиной: сначала
«Всегда обновлять», потом обновление. Порядок задаёт ядро, а не порядок кнопок.
</p>
</section>
<div id="app" class="flex min-h-0 flex-1 flex-col"></div>
</div>
<script type="module" src="./src/main.ts"></script>
</body>
</html>

Точка входа: здесь лист создаётся и сюда же приходят события.

src/main.ts
import { createSheetsCore } from "@f1/preset-sheets-core";
import type { WorkbookLink, WorkbookLinksResult } from "@f1/facade";
import { EXECUTOR, transport } from "./book.js";
import { hostAsks } from "./host.js";
import "./styles.css";
/**
* СВЯЗИ С ЧУЖИМИ КНИГАМИ ЧЕРЕЗ SDK — ВОСЕМЬ НАМЕРЕНИЙ ОДНОЙ ОПЕРАЦИИ.
*
* Ответ на чужую страницу «Cross Workbook Formula Calculation». Считает формулу
* по чужой книге ЯДРО; оболочка не разбирает ни одной внешней ссылки и не знает
* файловой системы. Отсюда три свойства, которых обычно не видно:
*
* · СОСТОЯНИЕ СВЯЗИ — КОД ЯДРА, а не вывод из пути к файлу: «источник не
* найден» говорит оно, а страница только переводит код в слово Excel;
* · «ОТКРЫТЬ КНИГУ» ОТДАЁТ АДРЕС, а не открывает: файлы открывает хозяин окна;
* · `status: "blocked"` — НЕ ОТКАЗ КОНВЕРТА. Ядро ответило успешно, инвентарь
* годен, а запрошенного не сделало и сказало почему. Принять это за успех
* значило бы показать несделанное сделанным (инвариант №3).
*
* НОМЕР СОСТОЯНИЯ уходит ВНУТРИ намерения и берётся ИЗ ОТВЕТА — фасад ведёт его
* сам, и здесь его нет ни в одной строке: иначе список правился бы вслепую.
*/
const { locale } = hostAsks();
const source = document.querySelector<HTMLInputElement>("[data-links-source]")!;
const answer = document.querySelector<HTMLElement>("[data-links-answer]")!;
const list = document.querySelector<HTMLElement>("[data-links-list]")!;
const core = createSheetsCore({ mount: document.getElementById("app")!, transport, locale });
const workbook = core.f1.getActiveWorkbook();
let current: WorkbookLink | null = null;
const STATE: Readonly<Record<string, string>> = {
cached: "Значения из кэша",
trusted: "Источник доверенный",
current: "Значения свежие",
missing: "Источник не найден",
moved: "Источник перемещён",
corrupt: "Источник не читается",
blocked: "Источник закрыт",
};
function show(said: WorkbookLinksResult, what: string): void {
if (!said.ok) {
answer.className = "text-xs text-error";
answer.setAttribute("data-links-refusal", said.code ?? "refused");
answer.textContent = `${what}: отказ — ${said.reason}${said.code ? ` (${said.code})` : ""}`;
return;
}
answer.removeAttribute("data-links-refusal");
answer.className = said.status === "blocked" ? "text-xs text-warning" : "text-xs opacity-70";
answer.setAttribute("data-links-status", said.status);
answer.textContent =
said.status === "blocked"
? `${what}: не сделано — ${said.reason ?? "причина не названа"} (ответ пришёл успешным, но запрошенного нет)`
: `${what} — сделано ${EXECUTOR}; ответ «${said.status}», связей ${said.links.length}, ` +
`номер состояния ${said.revision}${said.openSourceUri ? `, адрес источника ${said.openSourceUri}` : ""}.`;
current = said.links[0] ?? null;
list.replaceChildren(
...said.links.map((link) => {
const row = document.createElement("div");
row.setAttribute("data-links-item", link.id);
row.className = "rounded border border-base-300 px-2 py-1";
row.textContent =
`${link.displayName} · ${STATE[link.state] ?? link.state} · ` +
`формул ${link.formulaCellCount}, имён ${link.definedNameCount}, ячеек в кэше ${link.cachedCellCount}` +
`${link.sourceRevision ? ` · состояние источника ${link.sourceRevision}` : ""}`;
return row;
}),
);
if (!said.links.length) {
const empty = document.createElement("div");
empty.setAttribute("data-links-empty", "");
empty.className = "opacity-60";
/* Слово Excel для пустой области — его же, а не наше. */
empty.textContent = "Ссылки на внешние книги отсутствуют.";
list.replaceChildren(empty);
}
}
document.querySelector("[data-links-update]")!.addEventListener("click", () => {
void workbook.updateWorkbookLinksAsync().then((said) => show(said, "Обновить все"));
});
document.querySelector("[data-links-trust]")!.addEventListener("click", () => {
/* «Параметры обновления» Excel — три переключателя, а решение ядра БУЛЕВО:
доверять или нет. Третьего состояния («спрашивать каждый раз») в операции
нет, и рисовать его здесь было бы обещанием. */
void workbook.setWorkbookLinksTrustAsync(true).then((said) => show(said, "Всегда обновлять"));
});
document.querySelector("[data-links-open]")!.addEventListener("click", () => {
if (!current) return;
void workbook.openWorkbookLinkSourceAsync(current.id).then((said) => show(said, "Открыть книгу"));
});
document.querySelector("[data-links-change]")!.addEventListener("click", () => {
if (!current) return;
void workbook
.changeWorkbookLinkSourceAsync(current.id, source.value)
.then((said) => show(said, "Изменить источник"));
});
document.querySelector("[data-links-break]")!.addEventListener("click", () => {
void workbook.breakWorkbookLinksAsync().then((said) => show(said, "Разорвать все"));
});
/* ПЕРВЫЙ ВОПРОС — ИМЕННО ВОПРОС: `list` ничего не меняет и не называет номера
состояния вовсе (просить ядро проверить то, чего мы ещё не знаем, нельзя). */
void core.f1.loadAsync().then(() => workbook.getWorkbookLinksAsync().then((said) => show(said, "Список связей")));
import.meta.hot?.dispose(() => core.destroy());

Книга примера: данные, с которыми лист открывается.

src/book.ts
import { createTransport, transportExecutorName, transportSettingFromSearch, type MockWorkbookLink } from "@f1/contract";
/**
* КНИГА-ЗАГЛУШКА СО СВЯЗЯМИ — ВМЕСТО НАСТОЯЩЕГО ЯДРА.
*
* СВЯЗИ ОБЪЯВЛЯЕТ ФИКСТУРА, А НЕ РАЗБОР ФОРМУЛ. Чтобы вывести связь из
* `='/путь/[Книга.xlsx]Лист1'!A1`, надо разобрать внешнюю ссылку — то есть
* завести второй семантический путь (инвариант №1). Не объявлены — книга честно
* отвечает «связей нет», ровно как новая книга Excel.
*
* ТРАНСПОРТ ВЫБИРАЕТСЯ НАСТРОЙКОЙ: умолчание — заглушка,
* `?core=http://127.0.0.1:5507` уводит на живое ядро. Код примера не меняется.
*/
const LINKS: readonly MockWorkbookLink[] = [
{
id: "link-1",
displayName: "Курсы.xlsx",
sourceKind: "local",
state: "cached",
cachedValueTruth: "imported_cache",
formulaCellCount: 3,
definedNameCount: 1,
cachedCellCount: 7,
structuredTables: [],
canUpdate: true,
canChangeSource: true,
canOpenSource: true,
canBreakLink: true,
},
{
id: "link-2",
displayName: "Отгрузки 2025.xlsx",
sourceKind: "local",
state: "missing",
cachedValueTruth: "imported_cache",
formulaCellCount: 2,
definedNameCount: 0,
cachedCellCount: 4,
structuredTables: [],
canUpdate: false,
canChangeSource: true,
canOpenSource: false,
canBreakLink: true,
},
];
const TABLE: readonly (readonly string[])[] = [
["Позиция", "Курс из чужой книги", "Сумма"],
["Партия A", "92,40", "18 480"],
["Партия B", "91,10", "9 110"],
];
const setting = transportSettingFromSearch(globalThis.location?.search ?? "", {
sheets: ["Лист1"],
usedRange: { rowEnd: 2, colEnd: 2 },
cellAt: (row, col) => TABLE[row]?.[col],
links: LINKS,
});
export const transport = createTransport(setting);
/**
* КТО ДЕРЖИТ СПИСОК СВЯЗЕЙ — и почему это написано рядом с транспортом. Ни одна
* внешняя ссылка в оболочке не разбирается: список, состояние каждой связи и то,
* чему равно её значение, приходят из-за шва. Подпись обязана называть
* исполнителя правдиво и на заглушке, и на живом ядре.
*/
export const EXECUTOR = transportExecutorName(setting);

Сторона хозяина страницы: что приложение отвечает листу и что от него получает.

src/host.ts
import { applyLocaleAttributes, resolveLocale, setLocale, type Locale } from "@f1/i18n";
/**
* ЧТО ХОЗЯИН СТРАНИЦЫ ДАЁТ ЛИСТУ — тема и язык.
*
* У встраивающего это уже есть своё: тему он знает из настроек, язык — из
* профиля пользователя. Здесь и то и другое читается из адреса
* (`?theme=dark&locale=ar`) ровно затем, чтобы на любое состояние можно было
* дать ссылку и чтобы его проверял обход: настройка, доступная только щелчком,
* недоступна ни ссылке, ни автоматической проверке.
*
* ПРИЗНАКИ СТАВЯТСЯ НА КОРЕНЬ ДОКУМЕНТА — потому что это делает ХОЗЯИН, а не
* лист. Лист читает тему оттуда, где стоит (`:host-context`), а язык получает
* СВОЙСТВОМ сборки: два листа на одной странице вправе говорить на разном.
*
* НАПРАВЛЕНИЕ ПИСЬМА — ИНТЕРФЕЙСА, А НЕ ДОКУМЕНТА. `dir="rtl"` зеркалит хром
* страницы; «Лист справа налево» из Excel — свойство КНИГИ, и сетка от
* арабского языка интерфейса не переворачивается. Это не пропуск, а решение
* (`locale.ts`).
*/
export function hostAsks(): { readonly theme: "light" | "dark"; readonly locale: Locale } {
const params = new URLSearchParams(location.search);
const theme = params.get("theme") === "dark" ? "dark" : "light";
const locale = resolveLocale(params.get("locale") ?? undefined, "ru");
document.documentElement.setAttribute("data-theme", theme);
/* Язык — один на процесс: словарь модульный, и ставит его хозяин страницы. */
setLocale(locale);
applyLocaleAttributes(document.documentElement, locale);
return { theme, locale };
}

Оформление страницы вокруг листа.

src/styles.css
/* ЛИСТ СТИЛЕЙ НАБОРА КОМПОНЕНТОВ — ЕДИНСТВЕННАЯ СТРОКА, КОТОРУЮ НЕЛЬЗЯ ЗАБЫТЬ.
Собственное оформление лист приносит в теневые корни сам, но утилиты
раскладки, которыми он держит свои органы, компилируются ЭТИМ листом. Без
него страница не просто выглядит иначе — вкладка ЗАВИСАЕТ (бесконечный цикл
замера у сетки без размеров). У потребителя вне репозитория это
`@fastoffices/formula-kit` из его node_modules. */
@import "@fastoffices/formula-kit"; /* см. примечание выше */
html,
body {
height: 100%;
margin: 0;
}

Из каких пакетов собран пример.

package.json
{
"name": "f1-example-workbook-links",
"private": true,
"type": "module",
"description": "Связи книги: восемь намерений одной операции, а внешние ссылки разбирает ядро.",
"scripts": {
"dev": "vite",
"build": "vite build"
},
"dependencies": {
"@f1/preset-sheets-core": "*",
"@f1/facade": "*",
"@f1/contract": "*",
"@f1/i18n": "*"
},
"devDependencies": {
"vite": "^7.0.0"
}
}

Ассистент документации

Ответ собран из документации и может быть неточным — сверяйтесь с источниками.