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

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