@webskill/chatbot 0.18.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { i as __toESM, r as __require, t as __commonJSMin } from "./rolldown-runtime-DZ8SGSJo.js";
2
- import { i as withRuntimeConfigDefaults, n as defaultRuntimeConfig, o as mergeQuickPrompts, r as mergeRuntimeConfigDefaults, s as normalizeLocalizedText } from "./types-C4dzGOPp.js";
2
+ import { i as withRuntimeConfigDefaults, n as defaultRuntimeConfig, o as mergeQuickPrompts, r as mergeRuntimeConfigDefaults, s as normalizeLocalizedText } from "./types-D6kVxBO9.js";
3
3
  import { a as svg, i as html$1, n as refractor, o as find, r as stringify$1, s as stringify } from "./core-D3Tq37jA.js";
4
4
  import { t as require_core } from "./core-DBetA176.js";
5
5
  import { t as markup } from "./markup-D2NSmoAx.js";
@@ -19,11 +19,11 @@ import { fromMarkdown } from "mdast-util-from-markdown";
19
19
  import { gfmFromMarkdown, gfmToMarkdown } from "mdast-util-gfm";
20
20
  import { gfm } from "micromark-extension-gfm";
21
21
  import { ActionBarPrimitive, AttachmentPrimitive, AuiProvider, ComposerPrimitive, ExternalThread, INTERNAL, MessagePrimitive, ThreadListItemPrimitive, ThreadListPrimitive, ThreadPrimitive, useAui, useAuiState, useMessagePartText } from "@assistant-ui/react";
22
- import { BEHAVIOR_RECORDS_KEY, DEFAULT_LOOP_LIMITS, DEFAULT_USER_PROFILE_LIMITS, EventBus, FsArtifactStore, FsMemoryStore, FsRunSnapshotStore, FsRunTraceStore, FsSessionStore, FsToolStepStore, FullDisclosureRouter, SUPPORTED_DOCUMENT_MIME, SerializingMemoryStore, SkillDiscovery, USER_PROFILE_KEY, WebSkillError, WebSkillRuntime, applyUserProfileImport, assertRemoteUrlAllowed, assertSafePathSegment, atomicWriteText, diffUserProfile, exportUserProfile, formatAttachmentText, isUnsupportedRunSnapshot, messageOf, readBehaviorRecords, readUserProfile, refineUserProfile, resolveInsideRoot, sanitizeUntrustedLine, summarizeRunUsage, summarizeToolCalls, textParts } from "@webskill/sdk";
23
- import { BrowserWorkerScriptExecutor, WebOfficePolicy, capturePhoto, checkCameraAvailability, checkDictationAvailability, compressImageToBudget, createEncryptedMemoryStore, createLlmClient, createWebOfficeToolSource, deleteMemoryEncryptionKey, openCamera, openMemoryEncryptionKey, probeChromeBuiltinAvailability, startDictation } from "@webskill/sdk/browser";
22
+ import { BEHAVIOR_RECORDS_KEY, DEFAULT_LOOP_LIMITS, DEFAULT_USER_PROFILE_LIMITS, EventBus, FsArtifactStore, FsMemoryStore, FsRunSnapshotStore, FsRunTraceStore, FsSessionStore, FsToolStepStore, FullDisclosureRouter, SPREADSHEET_LIMITS, SUPPORTED_DOCUMENT_MIME, SerializingMemoryStore, SkillDiscovery, USER_PROFILE_KEY, WebSkillError, WebSkillRuntime, applyUserProfileImport, assertRemoteUrlAllowed, assertSafePathSegment, atomicWriteText, diffUserProfile, exportUserProfile, extractedDocumentFormat, formatAttachmentText, isUnsupportedRunSnapshot, messageOf, readBehaviorRecords, readUserProfile, refineUserProfile, resolveInsideRoot, sanitizeUntrustedLine, summarizeRunUsage, summarizeToolCalls, textParts } from "@webskill/sdk";
23
+ import { BrowserWorkerScriptExecutor, WebOfficePolicy, capturePhoto, checkCameraAvailability, checkDictationAvailability, compressImageToBudget, createEncryptedMemoryStore, createLlmClient, createWebOfficeToolSource, deleteMemoryEncryptionKey, openCamera, openMemoryEncryptionKey, probeChromeBuiltinAvailability, startDictation, toDocumentImage } from "@webskill/sdk/browser";
24
24
  import { JsonRenderSpecSurface, NativeSpecSurface, OpenUiSpecSurface, ReactBridgeState, SpecInteractionChannel, SurfaceFormTextsProvider, UiSurfaceSnapshotList, probeJsonRenderAvailability } from "@webskill/sdk/ui-react";
25
25
  import { A2UI_SPEC_FORM_PATH, collectFormScopes, createUiCatalogToolSource, fromA2uiSpecAction, fromA2uiSurfaceAction, interactionToFormModel, interactionToUiSpec, loadOpenUiPeers, loadWebSkillLitCatalog, renderMiniChart, shapeInteractionValue, toA2uiSpecMessages, toA2uiSurfaceAction, toVercelToolInvocation, uiCatalog } from "@webskill/sdk/ui";
26
- import { DownloadedFilePolicy, PAGE_ACTION_KINDS, TodoStore, createDelegationToolSource, createDocumentToolSource, createDownloadedFileToolSource, createPageActionToolSource, createPagePerceptionToolSource, createSkillGenerationToolSource, createSpreadsheetToolSource, createTabWorksetToolSource, createTodoToolSource, withDelegationOrigin } from "@webskill/sdk/agent";
26
+ import { DownloadedFilePolicy, PAGE_ACTION_KINDS, TodoStore, createDelegationToolSource, createDocumentToolSource, createDownloadedFileToolSource, createDwgToolSource, createPageActionToolSource, createPageImageToolSource, createPagePerceptionToolSource, createRemoteImageToolSource, createSkillGenerationToolSource, createSpreadsheetToolSource, createTabWorksetToolSource, createTemplateToolSource, createTodoToolSource, withDelegationOrigin } from "@webskill/sdk/agent";
27
27
  import { DataRenderers, Tools } from "@assistant-ui/core/react";
28
28
  import { ModelContext, Suggestions } from "@assistant-ui/core/store";
29
29
  import { Derived, attachTransformScopes, useClientLookup, useClientResource } from "@assistant-ui/store";
@@ -19599,6 +19599,284 @@ function defaultUrlTransform(value) {
19599
19599
  return "";
19600
19600
  }
19601
19601
 
19602
+ //#endregion
19603
+ //#region ../ui-kit/src/markdown/image.tsx
19604
+ /** `<img>` 里的 data URL 不会执行脚本,SVG 同理——放行的前提就是这个上下文 */
19605
+ const DATA_IMAGE = /^data:image\/[a-z0-9.+-]+[;,]/i;
19606
+ /**
19607
+ * 仓内图片引用。模型输出不了 base64,所以正文里贴附件图只能靠短引用,
19608
+ * 由宿主注入的解析器换成可显示的 URL。与投放通道的 `data-webskill-image` 同一套引用形态。
19609
+ */
19610
+ const IMAGE_REF = /^(upload|artifact):/i;
19611
+ /**
19612
+ * react-markdown 的默认 `urlTransform` 只放行 `https?|ircs?|mailto|xmpp`,
19613
+ * 于是 `![](data:image/png;base64,…)` 与 `![](upload:xxx)` 的 src 都被剥成空串。
19614
+ *
19615
+ * 这里**只对 `<img src>` 补放行**:`<a href="data:image/svg+xml,…">` 点开是导航,
19616
+ * 那个上下文里 SVG 的脚本会执行,所以 href 仍走默认策略。
19617
+ */
19618
+ const markdownUrlTransform = (url, key, node) => {
19619
+ if (key === "src" && node.tagName === "img" && (DATA_IMAGE.test(url) || IMAGE_REF.test(url))) return url;
19620
+ return defaultUrlTransform(url);
19621
+ };
19622
+ const ResolverContext = createContext(void 0);
19623
+ /**
19624
+ * 注入引用解析器。
19625
+ *
19626
+ * `resolve` **必须是稳定引用**(`useCallback` 且不依赖每次渲染新建的值),
19627
+ * 否则每次渲染都会重新解析一遍图片。
19628
+ */
19629
+ function MarkdownImageResolverProvider({ resolve, children }) {
19630
+ return /* @__PURE__ */ jsx(ResolverContext.Provider, {
19631
+ value: resolve,
19632
+ children
19633
+ });
19634
+ }
19635
+ /**
19636
+ * 正文里的图片。外链、data URL、同源相对路径直接显示;`upload:` / `artifact:` 引用
19637
+ * 走宿主注入的解析器。
19638
+ *
19639
+ * `referrerPolicy="no-referrer"`:外链图会向第三方发请求,不必把当前会话的 URL 一起送出去。
19640
+ * 失败时退回 `alt` 文本而不是留破图——alt 由内容方给,天然随内容语言,不需要另配文案。
19641
+ */
19642
+ function MarkdownImage({ node: _node, className, src, alt, ...rest }) {
19643
+ const resolve = useContext(ResolverContext);
19644
+ const [failed, setFailed] = useState(false);
19645
+ const [resolved, setResolved] = useState();
19646
+ const isRef = typeof src === "string" && IMAGE_REF.test(src);
19647
+ useEffect(() => {
19648
+ if (!isRef || typeof src !== "string") return;
19649
+ if (resolve === void 0) {
19650
+ setFailed(true);
19651
+ return;
19652
+ }
19653
+ let cancelled = false;
19654
+ let created;
19655
+ setFailed(false);
19656
+ setResolved(void 0);
19657
+ resolve(src).then((url) => {
19658
+ if (url === void 0) {
19659
+ if (!cancelled) setFailed(true);
19660
+ return;
19661
+ }
19662
+ created = url;
19663
+ if (cancelled) {
19664
+ if (url.startsWith("blob:")) URL.revokeObjectURL(url);
19665
+ return;
19666
+ }
19667
+ setResolved(url);
19668
+ }).catch(() => {
19669
+ if (!cancelled) setFailed(true);
19670
+ });
19671
+ return () => {
19672
+ cancelled = true;
19673
+ if (created?.startsWith("blob:")) URL.revokeObjectURL(created);
19674
+ };
19675
+ }, [
19676
+ isRef,
19677
+ resolve,
19678
+ src
19679
+ ]);
19680
+ const effective = isRef ? resolved : src;
19681
+ const text = typeof alt === "string" ? alt : "";
19682
+ if (failed || effective === void 0 || effective === "") {
19683
+ if (isRef && !failed) return null;
19684
+ return /* @__PURE__ */ jsxs("span", {
19685
+ className: "my-2 inline-flex items-center gap-1.5 rounded-md bg-subtle px-2 py-1 text-xs text-muted",
19686
+ children: [/* @__PURE__ */ jsx(ImageOff, {
19687
+ className: "size-3.5 shrink-0",
19688
+ "aria-hidden": true
19689
+ }), text === "" ? null : /* @__PURE__ */ jsx("span", { children: text })]
19690
+ });
19691
+ }
19692
+ return /* @__PURE__ */ jsx("img", {
19693
+ src: effective,
19694
+ alt: text,
19695
+ loading: "lazy",
19696
+ decoding: "async",
19697
+ referrerPolicy: "no-referrer",
19698
+ onError: () => setFailed(true),
19699
+ className: cn("my-2 max-h-96 max-w-full rounded-card border border-border object-contain", className),
19700
+ ...rest
19701
+ });
19702
+ }
19703
+
19704
+ //#endregion
19705
+ //#region ../ui-kit/src/theme/useDarkAncestor.ts
19706
+ /**
19707
+ * 声明了主题的祖先:`data-theme` 由 ThemeScope 挂(亮暗都挂),`.dark` 是宿主页面惯用的暗色类名。
19708
+ * 两者一起选,`closest` 才会停在**最近**的那一层,而不是越过亮色实例去命中外层的暗色宿主。
19709
+ */
19710
+ const THEME_ANCESTOR = "[data-theme], .dark";
19711
+ function readDark(node) {
19712
+ const scope = node.closest(THEME_ANCESTOR);
19713
+ if (!scope) return false;
19714
+ const declared = scope.getAttribute("data-theme");
19715
+ return declared === null ? true : declared === "dark";
19716
+ }
19717
+ /**
19718
+ * 判定挂载点**最近的**主题祖先是否处于暗色(0.6.0 FR-20.4)。
19719
+ *
19720
+ * 不用 `document.querySelector('.dark')`:那是全局查询,宿主页面是暗色时,
19721
+ * 嵌在暗色宿主里的亮色 SDK 实例也会被判成暗色(反之亦然)。
19722
+ *
19723
+ * 返回 ref 回调而不是接受 ref 对象:ref 对象的赋值不触发渲染,拿不到「已挂载」这个时机。
19724
+ *
19725
+ * 首次判定在 ref 回调里**同步**完成,不等 effect:消费方(如按主题分叉的 markdown
19726
+ * 高亮器)可能在首帧就固化选择,晚一帧就会永久停在亮色(设计 15-01 §1.2 假设 H-c)。
19727
+ */
19728
+ function useDarkAncestor() {
19729
+ const scoped = useThemeValue();
19730
+ const [node, setNode] = useState(null);
19731
+ const [probed, setProbed] = useState(false);
19732
+ const attach = useCallback((next) => {
19733
+ setNode(next);
19734
+ if (next) setProbed(readDark(next));
19735
+ }, []);
19736
+ useEffect(() => {
19737
+ if (!node || scoped !== void 0) return;
19738
+ const update = () => setProbed(readDark(node));
19739
+ update();
19740
+ const observer = new MutationObserver(update);
19741
+ observer.observe(document.documentElement, {
19742
+ attributes: true,
19743
+ attributeFilter: ["class", "data-theme"],
19744
+ subtree: true
19745
+ });
19746
+ return () => observer.disconnect();
19747
+ }, [node, scoped]);
19748
+ return [scoped !== void 0 ? scoped === "dark" : probed, attach];
19749
+ }
19750
+
19751
+ //#endregion
19752
+ //#region ../ui-kit/src/markdown/mermaid.tsx
19753
+ /** 代码围栏的语言标签;两个渲染入口共用这一份判定 */
19754
+ const MERMAID_LANGUAGE = "mermaid";
19755
+ /**
19756
+ * 一条消息里常有多张图,共用同一个模块 promise,避免每个挂载点各发一次动态 import。
19757
+ * mermaid 是可选 peer:没装时这个 promise 一直是 rejected,各挂载点一律退回代码块。
19758
+ */
19759
+ let mermaidModule;
19760
+ const loadMermaid = () => mermaidModule ??= import("mermaid");
19761
+ /** mermaid 主题变量 → 本仓语义 token。配色一律从这里取,不写死色值 */
19762
+ const MERMAID_COLOR_TOKENS = {
19763
+ background: "--color-canvas",
19764
+ primaryColor: "--color-subtle",
19765
+ primaryTextColor: "--color-ink",
19766
+ primaryBorderColor: "--color-border",
19767
+ secondaryColor: "--color-surface",
19768
+ tertiaryColor: "--color-card",
19769
+ lineColor: "--color-muted",
19770
+ textColor: "--color-ink"
19771
+ };
19772
+ /**
19773
+ * token 的值可能是 `oklch()` / `color-mix()`,而 mermaid 的调色靠 khroma,只认 rgb 一族。
19774
+ * 画进 1×1 画布再读像素,是唯一不依赖具体色彩空间的归一化方式。
19775
+ */
19776
+ function readThemeColors(node) {
19777
+ if (typeof document === "undefined") return void 0;
19778
+ const styles = getComputedStyle(node);
19779
+ const raw = Object.entries(MERMAID_COLOR_TOKENS).map(([variable, token]) => [variable, styles.getPropertyValue(token).trim()]).filter(([, value]) => value !== "");
19780
+ if (raw.length === 0) return void 0;
19781
+ const canvas = document.createElement("canvas");
19782
+ canvas.width = 1;
19783
+ canvas.height = 1;
19784
+ const ctx = canvas.getContext("2d");
19785
+ if (!ctx) return void 0;
19786
+ const out = {};
19787
+ for (const [variable, value] of raw) {
19788
+ ctx.clearRect(0, 0, 1, 1);
19789
+ ctx.fillStyle = value;
19790
+ ctx.fillRect(0, 0, 1, 1);
19791
+ const [r, g, b, a] = ctx.getImageData(0, 0, 1, 1).data;
19792
+ out[variable] = `rgba(${r ?? 0}, ${g ?? 0}, ${b ?? 0}, ${(a ?? 255) / 255})`;
19793
+ }
19794
+ return out;
19795
+ }
19796
+ /**
19797
+ * mermaid 的配置是**全局**的(不像 echarts 是实例级),所以每次渲染前都整份重设:
19798
+ * 这样亮暗两个 ThemeScope 同页时,各自渲染各自那一次的配置仍然是对的。
19799
+ */
19800
+ function buildConfig(dark, node) {
19801
+ const themeVariables = node ? readThemeColors(node) : void 0;
19802
+ return {
19803
+ startOnLoad: false,
19804
+ securityLevel: "strict",
19805
+ htmlLabels: false,
19806
+ suppressErrorRendering: true,
19807
+ theme: dark ? "dark" : "default",
19808
+ ...themeVariables ? { themeVariables } : {}
19809
+ };
19810
+ }
19811
+ /**
19812
+ * `click X href "…"` 即便在 strict 档也只是被 sanitize-url 过一遍,链接本身照样渲染。
19813
+ * 模型产出的图不该带跳转,所以插进 DOM 之前先把 href 摘干净。
19814
+ */
19815
+ function stripDiagramLinks(svg) {
19816
+ if (typeof DOMParser === "undefined" || !svg.includes("<a")) return svg;
19817
+ const doc = new DOMParser().parseFromString(svg, "text/html");
19818
+ const anchors = doc.querySelectorAll("a");
19819
+ if (anchors.length === 0) return svg;
19820
+ for (const anchor of anchors) {
19821
+ anchor.removeAttribute("href");
19822
+ anchor.removeAttribute("xlink:href");
19823
+ anchor.removeAttribute("target");
19824
+ }
19825
+ return doc.body.innerHTML;
19826
+ }
19827
+ /**
19828
+ * 代码围栏里的 mermaid 图。两个 markdown 渲染入口共用本组件,
19829
+ * 差别只在 `fallback` 由各自的代码块呈现提供。
19830
+ *
19831
+ * 三种情况一律退回 `fallback`:宿主没装 mermaid、语法不合法、渲染抛错。
19832
+ * 不加「渲染失败」之类的提示——降级物就是图的源码,用户看得见那是什么
19833
+ * (与 `MarkdownImage` 退回 alt 同理)。
19834
+ */
19835
+ function MermaidDiagram({ code, fallback, className }) {
19836
+ const [dark, attachTheme] = useDarkAncestor();
19837
+ const [svg, setSvg] = useState(void 0);
19838
+ const nodeRef = useRef(null);
19839
+ const seqRef = useRef(0);
19840
+ const idBase = `webskill-mermaid-${useId().replace(/[^a-zA-Z0-9_-]/g, "")}`;
19841
+ const attach = useCallback((node) => {
19842
+ nodeRef.current = node;
19843
+ attachTheme(node);
19844
+ }, [attachTheme]);
19845
+ useEffect(() => {
19846
+ let cancelled = false;
19847
+ (async () => {
19848
+ try {
19849
+ const mermaid = (await loadMermaid()).default;
19850
+ if (cancelled) return;
19851
+ mermaid.initialize(buildConfig(dark, nodeRef.current));
19852
+ if (!await mermaid.parse(code, { suppressErrors: true })) return;
19853
+ if (cancelled) return;
19854
+ const { svg: rendered } = await mermaid.render(`${idBase}-${++seqRef.current}`, code);
19855
+ if (!cancelled) setSvg(stripDiagramLinks(rendered));
19856
+ } catch {}
19857
+ })();
19858
+ return () => {
19859
+ cancelled = true;
19860
+ };
19861
+ }, [
19862
+ code,
19863
+ dark,
19864
+ idBase
19865
+ ]);
19866
+ if (svg === void 0) return /* @__PURE__ */ jsx("div", {
19867
+ ref: attach,
19868
+ className: "contents",
19869
+ children: fallback
19870
+ });
19871
+ return /* @__PURE__ */ jsx("div", {
19872
+ ref: attach,
19873
+ "data-webskill-mermaid": "",
19874
+ "data-testid": "mermaid-diagram",
19875
+ className: cn("my-3 flex justify-center overflow-x-auto rounded-card border border-border bg-card p-3", className),
19876
+ dangerouslySetInnerHTML: { __html: svg }
19877
+ });
19878
+ }
19879
+
19602
19880
  //#endregion
19603
19881
  //#region ../../node_modules/.pnpm/@babel+runtime@7.29.7/node_modules/@babel/runtime/helpers/esm/objectWithoutPropertiesLoose.js
19604
19882
  function _objectWithoutPropertiesLoose(r, e) {
@@ -23792,53 +24070,6 @@ var typescript_default = typescript;
23792
24070
  //#region ../../node_modules/.pnpm/react-syntax-highlighter@16.1.1_react@19.2.8/node_modules/react-syntax-highlighter/dist/esm/languages/prism/yaml.js
23793
24071
  var yaml_default = yaml;
23794
24072
 
23795
- //#endregion
23796
- //#region ../ui-kit/src/theme/useDarkAncestor.ts
23797
- /**
23798
- * 声明了主题的祖先:`data-theme` 由 ThemeScope 挂(亮暗都挂),`.dark` 是宿主页面惯用的暗色类名。
23799
- * 两者一起选,`closest` 才会停在**最近**的那一层,而不是越过亮色实例去命中外层的暗色宿主。
23800
- */
23801
- const THEME_ANCESTOR = "[data-theme], .dark";
23802
- function readDark(node) {
23803
- const scope = node.closest(THEME_ANCESTOR);
23804
- if (!scope) return false;
23805
- const declared = scope.getAttribute("data-theme");
23806
- return declared === null ? true : declared === "dark";
23807
- }
23808
- /**
23809
- * 判定挂载点**最近的**主题祖先是否处于暗色(0.6.0 FR-20.4)。
23810
- *
23811
- * 不用 `document.querySelector('.dark')`:那是全局查询,宿主页面是暗色时,
23812
- * 嵌在暗色宿主里的亮色 SDK 实例也会被判成暗色(反之亦然)。
23813
- *
23814
- * 返回 ref 回调而不是接受 ref 对象:ref 对象的赋值不触发渲染,拿不到「已挂载」这个时机。
23815
- *
23816
- * 首次判定在 ref 回调里**同步**完成,不等 effect:消费方(如按主题分叉的 markdown
23817
- * 高亮器)可能在首帧就固化选择,晚一帧就会永久停在亮色(设计 15-01 §1.2 假设 H-c)。
23818
- */
23819
- function useDarkAncestor() {
23820
- const scoped = useThemeValue();
23821
- const [node, setNode] = useState(null);
23822
- const [probed, setProbed] = useState(false);
23823
- const attach = useCallback((next) => {
23824
- setNode(next);
23825
- if (next) setProbed(readDark(next));
23826
- }, []);
23827
- useEffect(() => {
23828
- if (!node || scoped !== void 0) return;
23829
- const update = () => setProbed(readDark(node));
23830
- update();
23831
- const observer = new MutationObserver(update);
23832
- observer.observe(document.documentElement, {
23833
- attributes: true,
23834
- attributeFilter: ["class", "data-theme"],
23835
- subtree: true
23836
- });
23837
- return () => observer.disconnect();
23838
- }, [node, scoped]);
23839
- return [scoped !== void 0 ? scoped === "dark" : probed, attach];
23840
- }
23841
-
23842
24073
  //#endregion
23843
24074
  //#region ../ui-kit/src/markdown/prism.ts
23844
24075
  const languages = {
@@ -24475,7 +24706,16 @@ const CODE_TOKEN_VARS = Object.fromEntries(Object.keys(PRISM_THEME.light).filter
24475
24706
  //#region ../ui-kit/src/markdown/Markdown.tsx
24476
24707
  function CodeBlock({ node: _node, className, children, ...rest }) {
24477
24708
  const [dark, themeRef] = useDarkAncestor();
24478
- const language = resolveLanguage(/language-(\w+)/.exec(className ?? "")?.[1]);
24709
+ const match = /language-(\w+)/.exec(className ?? "");
24710
+ const language = resolveLanguage(match?.[1]);
24711
+ if (match?.[1] === "mermaid") return /* @__PURE__ */ jsx(MermaidDiagram, {
24712
+ code: String(children).replace(/\n$/, ""),
24713
+ fallback: /* @__PURE__ */ jsx("code", {
24714
+ className: "rounded-md bg-subtle px-1.5 py-0.5 font-mono text-ink",
24715
+ ...rest,
24716
+ children
24717
+ })
24718
+ });
24479
24719
  if (language) return /* @__PURE__ */ jsxs("div", {
24480
24720
  ref: themeRef,
24481
24721
  className: "my-3 overflow-hidden rounded-card border border-border bg-card",
@@ -24502,35 +24742,42 @@ function CodeBlock({ node: _node, className, children, ...rest }) {
24502
24742
  children
24503
24743
  });
24504
24744
  }
24745
+ /**
24746
+ * 模块级常量,**不可内联进 JSX**:react-markdown 按 `components` 的引用决定是否重建子树,
24747
+ * 每次渲染换新对象会让图片组件卸载重挂,于是流式输出期间同一张图被反复解析。
24748
+ */
24749
+ const MARKDOWN_COMPONENTS = {
24750
+ code: CodeBlock,
24751
+ img: MarkdownImage,
24752
+ p: ({ node: _node, ...props }) => /* @__PURE__ */ jsx("p", {
24753
+ className: "my-2",
24754
+ ...props
24755
+ }),
24756
+ ul: ({ node: _node, ...props }) => /* @__PURE__ */ jsx("ul", {
24757
+ className: "my-2 list-disc space-y-1 pl-5",
24758
+ ...props
24759
+ }),
24760
+ ol: ({ node: _node, ...props }) => /* @__PURE__ */ jsx("ol", {
24761
+ className: "my-2 list-decimal space-y-1 pl-5",
24762
+ ...props
24763
+ }),
24764
+ a: ({ node: _node, ...props }) => /* @__PURE__ */ jsx("a", {
24765
+ className: "font-medium text-ink underline underline-offset-4 hover:text-muted",
24766
+ target: "_blank",
24767
+ rel: "noreferrer",
24768
+ ...props
24769
+ }),
24770
+ pre: ({ node: _node, ...props }) => /* @__PURE__ */ jsx("pre", {
24771
+ className: "my-0",
24772
+ ...props
24773
+ })
24774
+ };
24505
24775
  function Markdown({ children, className = "" }) {
24506
24776
  return /* @__PURE__ */ jsx("div", {
24507
24777
  className: `text-sm leading-6 text-ink ${className}`,
24508
24778
  children: /* @__PURE__ */ jsx(Markdown$1, {
24509
- components: {
24510
- code: CodeBlock,
24511
- p: ({ node: _node, ...props }) => /* @__PURE__ */ jsx("p", {
24512
- className: "my-2",
24513
- ...props
24514
- }),
24515
- ul: ({ node: _node, ...props }) => /* @__PURE__ */ jsx("ul", {
24516
- className: "my-2 list-disc space-y-1 pl-5",
24517
- ...props
24518
- }),
24519
- ol: ({ node: _node, ...props }) => /* @__PURE__ */ jsx("ol", {
24520
- className: "my-2 list-decimal space-y-1 pl-5",
24521
- ...props
24522
- }),
24523
- a: ({ node: _node, ...props }) => /* @__PURE__ */ jsx("a", {
24524
- className: "font-medium text-ink underline underline-offset-4 hover:text-muted",
24525
- target: "_blank",
24526
- rel: "noreferrer",
24527
- ...props
24528
- }),
24529
- pre: ({ node: _node, ...props }) => /* @__PURE__ */ jsx("pre", {
24530
- className: "my-0",
24531
- ...props
24532
- })
24533
- },
24779
+ urlTransform: markdownUrlTransform,
24780
+ components: MARKDOWN_COMPONENTS,
24534
24781
  children
24535
24782
  })
24536
24783
  });
@@ -24857,16 +25104,41 @@ const preComponent = ({ className, ...props }) => /* @__PURE__ */ jsx("pre", {
24857
25104
  className: cn("overflow-x-auto rounded-b-lg border border-border bg-card", className),
24858
25105
  ...props
24859
25106
  });
25107
+ const highlighters = {
25108
+ light: makeHighlighter(false),
25109
+ dark: makeHighlighter(true)
25110
+ };
25111
+ /** 图接在 CodeHeader 下面,外框要与 `preComponent` 同形,否则成功与降级两态的版式会跳 */
25112
+ function makeMermaidHighlighter(dark) {
25113
+ const Highlighter = highlighters[dark ? "dark" : "light"];
25114
+ return function MermaidHighlighter(props) {
25115
+ return /* @__PURE__ */ jsx(MermaidDiagram, {
25116
+ code: props.code,
25117
+ className: "my-0 rounded-b-lg",
25118
+ fallback: /* @__PURE__ */ jsx(Highlighter, { ...props })
25119
+ });
25120
+ };
25121
+ }
25122
+ /**
25123
+ * 必须是模块级常量:`CodeOverride` 的 memo 比较里直接拿
25124
+ * `prev.componentsByLanguage === next.componentsByLanguage`,换新对象就是每次渲染重建子树。
25125
+ */
25126
+ const mermaidByLanguage = {
25127
+ light: { [MERMAID_LANGUAGE]: { SyntaxHighlighter: makeMermaidHighlighter(false) } },
25128
+ dark: { [MERMAID_LANGUAGE]: { SyntaxHighlighter: makeMermaidHighlighter(true) } }
25129
+ };
24860
25130
  const markdownComponents = {
24861
25131
  light: memoizeMarkdownComponents({
24862
- SyntaxHighlighter: makeHighlighter(false),
25132
+ SyntaxHighlighter: highlighters.light,
24863
25133
  CodeHeader,
24864
- pre: preComponent
25134
+ pre: preComponent,
25135
+ img: MarkdownImage
24865
25136
  }),
24866
25137
  dark: memoizeMarkdownComponents({
24867
- SyntaxHighlighter: makeHighlighter(true),
25138
+ SyntaxHighlighter: highlighters.dark,
24868
25139
  CodeHeader,
24869
- pre: preComponent
25140
+ pre: preComponent,
25141
+ img: MarkdownImage
24870
25142
  })
24871
25143
  };
24872
25144
  /**
@@ -24887,8 +25159,10 @@ const MarkdownText = memo(function MarkdownText({ className }) {
24887
25159
  className: "contents",
24888
25160
  children: /* @__PURE__ */ jsx(MarkdownTextPrimitive, {
24889
25161
  remarkPlugins: [remarkGfm],
25162
+ urlTransform: markdownUrlTransform,
24890
25163
  className: cn("aui-md", className),
24891
25164
  components,
25165
+ componentsByLanguage: mermaidByLanguage[dark ? "dark" : "light"],
24892
25166
  smooth: false,
24893
25167
  defer: true
24894
25168
  })
@@ -24948,28 +25222,60 @@ const TEXT_EXTENSIONS = [
24948
25222
  /** 非文本非图片但仍可整体外发的类型(provider 的 file 分片) @experimental */
24949
25223
  const FILE_MIME_TYPES = ["application/pdf"];
24950
25224
  /**
24951
- * 需要**客户端先抽取成文本**才能外发的类型(FR-23.9 / 0.13.0 FR-12.5)。
24952
- * 不走直通的 `file`:docx / xlsx 发给 provider 会被当成二进制垃圾。
25225
+ * 需要**客户端先抽取成文本**才能外发的类型(FR-23.9 / 0.13.0 FR-12.5 / 0.22.0 分册 18)。
25226
+ * 不走直通的 `file`:docx / xlsx / pptx 发给 provider 会被当成二进制垃圾。
24953
25227
  */
24954
25228
  const DOCX_MIME = "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
24955
25229
  const XLSX_MIME = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
25230
+ const PPTX_MIME = "application/vnd.openxmlformats-officedocument.presentationml.presentation";
25231
+ /**
25232
+ * DWG 图纸的 MIME。没有官方注册值,各家各发一套,浏览器上传时更常见的是空串。
25233
+ * @experimental
25234
+ */
25235
+ const DWG_MIME_TYPES = [
25236
+ "image/vnd.dwg",
25237
+ "application/acad",
25238
+ "application/x-acad",
25239
+ "application/dwg",
25240
+ "application/x-dwg",
25241
+ "drawing/dwg"
25242
+ ];
25243
+ /**
25244
+ * 只存字节、不进上下文的扩展名。仍然是**白名单制**:
25245
+ * 新增 `binary` 类别不等于任意二进制放行(AC-19.18)。
25246
+ * @experimental
25247
+ */
25248
+ const BINARY_EXTENSIONS = ["dwg"];
24956
25249
  const TEXT_EXTENSION_SET = new Set(TEXT_EXTENSIONS);
24957
25250
  const IMAGE_MIME_SET = new Set(IMAGE_MIME_TYPES);
24958
25251
  const FILE_MIME_SET = new Set(FILE_MIME_TYPES);
24959
- const EXTRACTED_MIME_SET = /* @__PURE__ */ new Set([DOCX_MIME, XLSX_MIME]);
24960
- const EXTRACTED_EXTENSION_SET = /* @__PURE__ */ new Set(["docx", "xlsx"]);
25252
+ const EXTRACTED_MIME_SET = /* @__PURE__ */ new Set([
25253
+ DOCX_MIME,
25254
+ XLSX_MIME,
25255
+ PPTX_MIME
25256
+ ]);
25257
+ const EXTRACTED_EXTENSION_SET = /* @__PURE__ */ new Set([
25258
+ "docx",
25259
+ "xlsx",
25260
+ "pptx"
25261
+ ]);
25262
+ const BINARY_MIME_SET = new Set(DWG_MIME_TYPES);
25263
+ const BINARY_EXTENSION_SET = new Set(BINARY_EXTENSIONS);
25264
+ /** 下载与上传都常把认不出的类型标成它,此时只能看扩展名 */
25265
+ const OPAQUE_MIME = "application/octet-stream";
24961
25266
  function extensionOf(fileName) {
24962
25267
  return fileName.split(".").pop()?.toLowerCase() ?? "";
24963
25268
  }
24964
- /** 分类集中一处,不散落在各 UI 分支里 @experimental */
24965
- function classifyAttachment(contentType, fileName) {
25269
+ /** 分类集中一处,不散落在各 UI 分支里 @experimental */ function classifyAttachment(contentType, fileName) {
24966
25270
  const type = contentType.toLowerCase();
24967
25271
  if (IMAGE_MIME_SET.has(type)) return "image";
25272
+ if (BINARY_MIME_SET.has(type)) return "binary";
24968
25273
  if (EXTRACTED_MIME_SET.has(type)) return "document-text";
24969
25274
  if (FILE_MIME_SET.has(type)) return "file";
24970
25275
  if (type.startsWith("text/") || type === "application/json") return "text";
24971
25276
  if (type === "" && TEXT_EXTENSION_SET.has(extensionOf(fileName))) return "text";
24972
25277
  if (type === "" && EXTRACTED_EXTENSION_SET.has(extensionOf(fileName))) return "document-text";
25278
+ if ((type === "" || type === OPAQUE_MIME) && BINARY_EXTENSION_SET.has(extensionOf(fileName))) return "binary";
24973
25279
  }
24974
25280
 
24975
25281
  //#endregion
@@ -25516,6 +25822,89 @@ var ChatbotVisionDelegate = class {
25516
25822
  }
25517
25823
  };
25518
25824
 
25825
+ //#endregion
25826
+ //#region src/core/imageSource.ts
25827
+ /**
25828
+ * 授权卡的自述作用域(FR-11.10 / AC-G6)。
25829
+ *
25830
+ * 卡上写的范围与实际记忆的范围**由这一个函数同时导出**:
25831
+ * `key` 直接就是 `ImageSourceAuthorizer` 存进集合里的那个值,
25832
+ * `labelKey` 是复选框旁那句话。两者不可能分叉,因为只有一处。
25833
+ *
25834
+ * 出的是**字典键**而不是成品文案:这里是同步路径,读不到语言,
25835
+ * 写死一种语言就把复选框固定成英文了。
25836
+ * @experimental
25837
+ */
25838
+ function describeImageSourceScope(kind) {
25839
+ return {
25840
+ key: kind,
25841
+ labelKey: kind === "page-image" ? "interaction.imageSource.rememberPageImage" : "interaction.imageSource.rememberRemoteImage"
25842
+ };
25843
+ }
25844
+ const MESSAGES = {
25845
+ "page-image": "Allow the assistant to copy a picture from the page you are on into the document it is preparing? The picture is saved with this conversation’s files.",
25846
+ "remote-image": "Allow the assistant to download an image from the web into the document it is preparing? The site hosting it will see that the request came from you."
25847
+ };
25848
+ /**
25849
+ * 图片来源的会话内授权(分册 11 §3.3)。
25850
+ *
25851
+ * 记忆随会话生灭:`forgetSessionDecisions()` 在新建 / 切换会话时调,新会话重新问。
25852
+ * 没有任何持久化端口——本版刻意不做跨会话记忆,那是比用户预期更宽的授权。
25853
+ * @experimental
25854
+ */
25855
+ var ImageSourceAuthorizer = class {
25856
+ #ui;
25857
+ #approved = /* @__PURE__ */ new Set();
25858
+ /** 同一类来源的并发请求搭同一次询问,不各弹各的(同视觉委派 FR-26.8) */
25859
+ #asking = /* @__PURE__ */ new Map();
25860
+ #seq = 0;
25861
+ constructor(options) {
25862
+ this.#ui = options.ui;
25863
+ }
25864
+ forgetSessionDecisions() {
25865
+ this.#approved.clear();
25866
+ }
25867
+ /**
25868
+ * 记忆粒度是**来源类型**,不是域名(FR-13.7)。因此同一类的并发请求搭同一次询问,
25869
+ * 卡上也只会出现其中一个 URL——这正是为什么卡文必须把
25870
+ * 「本次对话内所有外链图片」说出来,而不是只展示那一个域名。
25871
+ */
25872
+ async authorize(kind, details = {}) {
25873
+ if (this.#approved.has(kind)) return true;
25874
+ const pending = this.#asking.get(kind);
25875
+ if (pending !== void 0) return await pending;
25876
+ const ask = this.#ask(kind, details);
25877
+ this.#asking.set(kind, ask);
25878
+ try {
25879
+ return await ask;
25880
+ } finally {
25881
+ this.#asking.delete(kind);
25882
+ }
25883
+ }
25884
+ async #ask(kind, details) {
25885
+ this.#seq += 1;
25886
+ const scope = describeImageSourceScope(kind);
25887
+ const { note, url } = details;
25888
+ const response = await this.#ui.request({
25889
+ type: "authorize",
25890
+ id: `image-source-${kind}-${String(this.#seq)}`,
25891
+ capability: "readImageSource",
25892
+ message: note === void 0 ? MESSAGES[kind] : `${MESSAGES[kind]} The assistant says: ${note}`,
25893
+ details: {
25894
+ kind: scope.key,
25895
+ ...note === void 0 ? {} : { note },
25896
+ ...url === void 0 ? {} : { url },
25897
+ rememberable: true,
25898
+ rememberScope: "session",
25899
+ rememberLabelKey: scope.labelKey
25900
+ }
25901
+ });
25902
+ if (response.cancelled === true || response.value === false) return false;
25903
+ if (response.remembered === true) this.#approved.add(scope.key);
25904
+ return true;
25905
+ }
25906
+ };
25907
+
25519
25908
  //#endregion
25520
25909
  //#region src/core/markdownBoundary.ts
25521
25910
  /**
@@ -26088,6 +26477,8 @@ const chatbotDictionary = defineDictionary({
26088
26477
  "interaction.pageAction.attach": "Allow the assistant to attach a file to “{target}”?",
26089
26478
  "interaction.pageAction.scroll.down": "Allow the assistant to scroll down the area around “{target}”?",
26090
26479
  "interaction.pageAction.scroll.up": "Allow the assistant to scroll up the area around “{target}”?",
26480
+ "interaction.pageAction.hover": "Allow the assistant to hover over “{target}”?",
26481
+ "interaction.pageAction.drag": "Allow the assistant to drag “{target}” onto another element?",
26091
26482
  "interaction.pageAction.back": "Allow the assistant to go back to the previous page on “{target}”?",
26092
26483
  "interaction.pageAction.role.click": "this control",
26093
26484
  "interaction.pageAction.role.fill": "this field",
@@ -26096,6 +26487,8 @@ const chatbotDictionary = defineDictionary({
26096
26487
  "interaction.pageAction.role.set": "this switch",
26097
26488
  "interaction.pageAction.role.attach": "this upload field",
26098
26489
  "interaction.pageAction.role.scroll": "this area",
26490
+ "interaction.pageAction.role.hover": "this element",
26491
+ "interaction.pageAction.role.drag": "this item",
26099
26492
  "interaction.pageAction.role.back": "this page",
26100
26493
  "interaction.pageAction.remember": "Don’t ask again for this kind of action",
26101
26494
  "interaction.pageAction.frame": "The target is in the embedded frame \"{frame}\".",
@@ -26119,6 +26512,15 @@ const chatbotDictionary = defineDictionary({
26119
26512
  "interaction.visionDelegation.rememberFallback": "Don’t ask again for this endpoint",
26120
26513
  "interaction.visionDelegation.rememberSession": "Don’t ask again in this conversation",
26121
26514
  "interaction.visionDelegation.body": "The model you selected cannot read images. Send the {images} image(s) produced by “{source}” to “{label}” at {host} to be described? If you decline, nothing is sent and the turn continues without them.",
26515
+ "interaction.imageSource.title": "Put a picture into the document?",
26516
+ "interaction.imageSource.pageImageBody": "Allow the assistant to copy a picture from the page into the document it is building? The picture is saved with this conversation until you delete it.",
26517
+ "interaction.imageSource.remoteImageBody": "Allow the assistant to download an image from {url} and put it into the document it is building? Fetching it tells that server your network address and when you asked for it.",
26518
+ "interaction.imageSource.remoteImageAnyBody": "Allow the assistant to download images from the web and put them into the document it is building? Fetching one tells that server your network address and when you asked for it.",
26519
+ "interaction.imageSource.insecure": "This address is plain http://, so the image can be read or swapped by anyone on the network in between.",
26520
+ "interaction.imageSource.note": "The assistant says it is for: {note}",
26521
+ "interaction.imageSource.rememberFallback": "Don’t ask again in this conversation",
26522
+ "interaction.imageSource.rememberPageImage": "Don’t ask again in this conversation before taking pictures from the page",
26523
+ "interaction.imageSource.rememberRemoteImage": "Don’t ask again in this conversation before downloading images from the web",
26122
26524
  "interaction.uploadFile.read": "Allow the skill “{skill}” to read the file you uploaded?",
26123
26525
  "interaction.uploadFile.file": "{name} · {type} · {size}",
26124
26526
  "interaction.sandbox.readReference": "Allow the skill “{skill}” to read the reference file “{path}”?",
@@ -26184,6 +26586,7 @@ const chatbotDictionary = defineDictionary({
26184
26586
  "composer.attachment.kind.image": "Image",
26185
26587
  "composer.attachment.kind.file": "File",
26186
26588
  "composer.attachment.kind.document-text": "Doc text",
26589
+ "composer.attachment.kind.binary": "Drawing",
26187
26590
  "composer.attachment.compressed": "Compressed {from} → {to}",
26188
26591
  "composer.model": "Model",
26189
26592
  "composer.model.unconfigured": "Not usable: this entry has no API key or base URL. Configure it in the console.",
@@ -26198,6 +26601,7 @@ const chatbotDictionary = defineDictionary({
26198
26601
  "attachment.storeFailed.title": "Not enough space on this device",
26199
26602
  "attachment.storeFailed": "“{name}” could not be saved to this device’s storage, so it was not attached. Free up space and try again.",
26200
26603
  "attachment.pdfAsText": "“{name}” is {size}, over the {limit} a single request can carry, so its extracted text was sent instead of the original file. Layout, figures and scanned pages are not included.",
26604
+ "attachment.pdfAsPagedRead": "“{name}” is {size}, over the {limit} a single request can carry, so the file was not attached. The assistant will read it page by page instead, which takes several passes over a long document.",
26201
26605
  "attachment.pdfRefused": "“{name}” is {size}, over the {limit} a single request can carry, and no text could be extracted from it — it is most likely a scan. It was not sent. Try a smaller file, a page range, or a text version.",
26202
26606
  "attachment.pdfNoExtractor": "“{name}” is {size}, over the {limit} a single request can carry, and this environment cannot extract PDF text. It was not sent. Switch to a model that accepts large documents, or attach a Word or plain-text version.",
26203
26607
  "attachment.textTooLong": "The text of “{name}” is {size}, over the {limit} one request may put into the model’s context. It was not sent, and it was not truncated either, because half a document is worse than none. Raise the extracted text limit in Settings → Agent runtime, or attach a shorter excerpt.",
@@ -26502,6 +26906,8 @@ const chatbotDictionary = defineDictionary({
26502
26906
  "interaction.pageAction.attach": "允许助手向「{target}」附加文件吗?",
26503
26907
  "interaction.pageAction.scroll.down": "允许助手在「{target}」所在区域向下滚动吗?",
26504
26908
  "interaction.pageAction.scroll.up": "允许助手在「{target}」所在区域向上滚动吗?",
26909
+ "interaction.pageAction.hover": "允许助手将鼠标悬停在「{target}」上吗?",
26910
+ "interaction.pageAction.drag": "允许助手将「{target}」拖到另一个位置吗?",
26505
26911
  "interaction.pageAction.back": "允许助手在「{target}」上返回上一页吗?",
26506
26912
  "interaction.pageAction.role.click": "这个控件",
26507
26913
  "interaction.pageAction.role.fill": "这个输入框",
@@ -26510,6 +26916,8 @@ const chatbotDictionary = defineDictionary({
26510
26916
  "interaction.pageAction.role.set": "这个开关",
26511
26917
  "interaction.pageAction.role.attach": "这个上传控件",
26512
26918
  "interaction.pageAction.role.scroll": "这一块",
26919
+ "interaction.pageAction.role.hover": "这个元素",
26920
+ "interaction.pageAction.role.drag": "这一项",
26513
26921
  "interaction.pageAction.role.back": "这个页面",
26514
26922
  "interaction.pageAction.remember": "以后不再询问此类操作",
26515
26923
  "interaction.pageAction.frame": "目标位于嵌入帧「{frame}」中。",
@@ -26533,6 +26941,15 @@ const chatbotDictionary = defineDictionary({
26533
26941
  "interaction.visionDelegation.rememberFallback": "以后不再为该端点询问",
26534
26942
  "interaction.visionDelegation.rememberSession": "本次对话不再询问,默认同意",
26535
26943
  "interaction.visionDelegation.body": "你选中的模型看不了图片。是否把「{source}」产出的 {images} 张图发往「{label}」({host})请它描述?不同意则什么都不会发出,这一轮就当没看到图。",
26944
+ "interaction.imageSource.title": "把图片放进文档?",
26945
+ "interaction.imageSource.pageImageBody": "允许助手把当前页面上的一张图放进它正在生成的文档吗?该图会随本次会话保存,直到你删除这次会话。",
26946
+ "interaction.imageSource.remoteImageBody": "允许助手从 {url} 下载一张图片放进它正在生成的文档吗?下载会把你的网络地址与访问时间告知该服务器。",
26947
+ "interaction.imageSource.remoteImageAnyBody": "允许助手从互联网下载图片放进它正在生成的文档吗?下载会把你的网络地址与访问时间告知对方服务器。",
26948
+ "interaction.imageSource.insecure": "这个地址是明文 http://,图片内容可被链路上的任何人看到或替换。",
26949
+ "interaction.imageSource.note": "助手声称的用途:{note}",
26950
+ "interaction.imageSource.rememberFallback": "本次对话不再询问",
26951
+ "interaction.imageSource.rememberPageImage": "本次对话不再询问取用页面上的图",
26952
+ "interaction.imageSource.rememberRemoteImage": "本次对话不再询问从互联网下载图片",
26536
26953
  "interaction.uploadFile.read": "允许技能「{skill}」读取你上传的这份文件吗?",
26537
26954
  "interaction.uploadFile.file": "{name} · {type} · {size}",
26538
26955
  "interaction.sandbox.readReference": "允许技能「{skill}」读取参考资料「{path}」吗?",
@@ -26598,6 +27015,7 @@ const chatbotDictionary = defineDictionary({
26598
27015
  "composer.attachment.kind.image": "图片",
26599
27016
  "composer.attachment.kind.file": "文件",
26600
27017
  "composer.attachment.kind.document-text": "文档正文",
27018
+ "composer.attachment.kind.binary": "图纸",
26601
27019
  "composer.attachment.compressed": "已压缩 {from} → {to}",
26602
27020
  "composer.model": "模型",
26603
27021
  "composer.model.unconfigured": "不可用:该条目既没有 API 密钥也没有基础地址,请在控制台补全。",
@@ -26612,6 +27030,7 @@ const chatbotDictionary = defineDictionary({
26612
27030
  "attachment.storeFailed.title": "本机存储不够了",
26613
27031
  "attachment.storeFailed": "「{name}」没能存入本机存储,因此未作为附件添加。请腾出空间后重试。",
26614
27032
  "attachment.pdfAsText": "「{name}」有 {size},超过单次请求能带的 {limit},已改为发送从中抽取的文本,而不是原件。版面、图表与扫描页不在其中。",
27033
+ "attachment.pdfAsPagedRead": "「{name}」有 {size},超过单次请求能带的 {limit},因此未作为附件发送。助手改为逐页读取,长文档会读好几轮。",
26615
27034
  "attachment.pdfRefused": "「{name}」有 {size},超过单次请求能带的 {limit},而且从中抽不出文本——它多半是扫描件。该文件未发送。请改用更小的文件、指定页码范围,或换文本版本。",
26616
27035
  "attachment.pdfNoExtractor": "「{name}」有 {size},超过单次请求能带的 {limit},而本环境无法抽取 PDF 文本。该文件未发送。请换一个能收大文档的模型,或改传 Word / 纯文本版本。",
26617
27036
  "attachment.textTooLong": "「{name}」的文本有 {size},超过单次请求能放进模型上下文的 {limit}。该文件未发送,也没有截断——半份文档比没有更糟。请在「设置 → 智能体运行」里调高抽取文本上限,或改附一段节选。",
@@ -26785,6 +27204,24 @@ function translateText(locale, key, params) {
26785
27204
  return interpolate$1(chatbotDictionary[locale]?.[key] ?? chatbotDictionary.en[key] ?? key, params);
26786
27205
  }
26787
27206
 
27207
+ //#endregion
27208
+ //#region src/core/attachmentRef.ts
27209
+ /**
27210
+ * `upload:` 引用落到哪条附件上。
27211
+ *
27212
+ * 引擎侧(投放文档里的图片块)与组件侧(正文 markdown 里的 `![](upload:…)`)读的是
27213
+ * 两份不同的数据——一份从会话存储读回,一份是 React 手上的消息数组——但**判定必须是同一条**,
27214
+ * 否则会出现「文档里贴得出、对话里贴不出」这种没法向用户解释的差异。
27215
+ *
27216
+ * id 先行:它唯一。文件名兜底是必需的而不是宽容——附件 id 是 `att-…` 随机串,
27217
+ * 模型上下文里最显眼的始终是文件名,只认 id 的结果就是整篇文档一张图都没有。
27218
+ * 重名时取最先那条:这一路的候选全部来自用户自己的消息,没有越权面,
27219
+ * 取错一张也远好过整批丢图。
27220
+ */
27221
+ function findAttachmentByRef(attachments, ref) {
27222
+ return attachments.find((a) => a.id === ref) ?? attachments.find((a) => a.name === ref);
27223
+ }
27224
+
26788
27225
  //#endregion
26789
27226
  //#region src/core/runPhases.ts
26790
27227
  /** 展示用相位:discover 并进 route,prepare/observe 并进 execute(07 号文档 §4) */
@@ -26987,6 +27424,22 @@ function fromBase64(data) {
26987
27424
  function formatSize(bytes) {
26988
27425
  return bytes >= 1024 * 1024 ? `${(bytes / 1024 / 1024).toFixed(1)} MB` : `${Math.max(1, Math.round(bytes / 1024))} KB`;
26989
27426
  }
27427
+ /** 技能写在图片块上的引用,由 publish 脚本渲染成这个属性;字节在这里填回去 */
27428
+ const IMAGE_REF_ATTR = "data-webskill-image";
27429
+ /**
27430
+ * 单张图进文档的字节上限。超了先压(`compressImageToBudget`),压不下去按 FR-11.8 归因丢弃,
27431
+ * 不许原样塞进载荷去撞分册 10 的硬兜底——那会让一张图带走整份文档。
27432
+ */
27433
+ const MAX_DOCUMENT_IMAGE_BYTES = 8e6;
27434
+ /**
27435
+ * 在线 WebOffice 文档作模板时的取件号前缀(0.22.0 分册 42 · FR-42.12)。
27436
+ *
27437
+ * 前缀是显式的,不靠猜:`inspect_template` 的取件号绝大多数是附件 id,
27438
+ * 拿一个普通 id 去在线文档那边碰运气,猜中一次的收益远小于猜错一次的代价——
27439
+ * 那是把另一份文档当成用户指的那一份填了出去。
27440
+ * @experimental
27441
+ */
27442
+ const WEB_OFFICE_TEMPLATE_PREFIX = "weboffice:";
26990
27443
  /**
26991
27444
  * 图片附件的指认说明(0.21.0 分册 22 · FR-22.4b)。
26992
27445
  *
@@ -26998,7 +27451,8 @@ function formatSize(bytes) {
26998
27451
  * 与图分开成片:端点拒收图时 `#dropRejectedImages` 换掉的只有图那一片,说明留在原地。
26999
27452
  */
27000
27453
  function imageAttachmentLabel(attachment) {
27001
- return `--- Attachment: ${attachment.name} (${attachment.contentType}) ---\nThe user attached this image to this message and it is already visible to you. Do not look for it on the page, in the downloads list or anywhere else, and do not call a tool to obtain it.`;
27454
+ return `--- Attachment: ${attachment.name} (${attachment.contentType}, id: ${attachment.id}) ---\nThe user attached this image to this message and it is already visible to you. Do not look for it on the page, in the downloads list or anywhere else, and do not call a tool to obtain it.
27455
+ To show this picture rather than describe it, reference it as \`upload:${attachment.id}\`: write \`![caption](upload:${attachment.id})\` in your reply to display it inline, or pass the same string as the image \`ref\` when a skill or an export tool builds a document. Never invent base64 data for it.`;
27002
27456
  }
27003
27457
  /** 内置演示 LLM 的应答序列(用尽后从头循环) */
27004
27458
  const DEMO_REPLIES = [
@@ -27153,6 +27607,11 @@ var ChatEngine = class {
27153
27607
  #listeners = /* @__PURE__ */ new Set();
27154
27608
  /** runId → 已激活技能累积(skill-activated 事件用;终态清理) */
27155
27609
  #runSkills = /* @__PURE__ */ new Map();
27610
+ /**
27611
+ * runId → sessionId。trace 要等 run 结束才落盘,而导出工具在 run **进行中**
27612
+ * 就要按会话找附件与历史产物,只读 trace 时它们一律扑空(分册 21 §2.6)。
27613
+ */
27614
+ #runSessions = /* @__PURE__ */ new Map();
27156
27615
  /** 文档投放端口只构造一次:渲染侧把它放进 memo 依赖,每次取新对象会触发无谓重渲染 */
27157
27616
  #documentSurfacePort;
27158
27617
  /** 当前渲染框架档(复合 uiBridge 分发;setRenderer 热切换) */
@@ -27181,6 +27640,15 @@ var ChatEngine = class {
27181
27640
  * 拒绝的记忆挂在它身上,因此 `createSession()` 要能找到它把那一位清掉。
27182
27641
  */
27183
27642
  #visionDelegate;
27643
+ /**
27644
+ * 图片来源授权(0.22.0 分册 11 §3.3)。记忆随会话生灭,因此与视觉委派一样,
27645
+ * 切换 / 新建会话时要把那一份集合清掉。
27646
+ */
27647
+ #imageSources;
27648
+ /** run → 已落下的页面图张数;产物名 `page-image-N.png` 的 N 从这里来 */
27649
+ #pageImageSeq = /* @__PURE__ */ new Map();
27650
+ /** run → 已落下的外链图张数;与页面图分开编号,产物名一眼看得出来源 */
27651
+ #remoteImageSeq = /* @__PURE__ */ new Map();
27184
27652
  /** 当前 run('route' 事件捕获,终态清理);清单与页面感知的事件归属共用 */
27185
27653
  #activeRunId;
27186
27654
  /** dispose() 之后拒绝再发起 run:卸载后的组件不该还能悄悄跑一轮 */
@@ -27386,17 +27854,177 @@ var ChatEngine = class {
27386
27854
  const dir = `${this.#chatRoot}/artifacts/${runId}`;
27387
27855
  const html = await this.#adapter.storage.readText(this.#artifactPath(dir, artifact));
27388
27856
  const css = style === void 0 ? void 0 : await this.#adapter.storage.readText(this.#artifactPath(dir, style));
27857
+ const resolved = await this.#resolveDocumentImages(html, runId);
27389
27858
  await host.open({
27390
27859
  skillName: await this.#skillNamesOfRun(runId),
27391
27860
  dataSource: sanitizeUntrustedLine(dataSource),
27392
27861
  document: {
27393
- html,
27862
+ html: resolved.html,
27394
27863
  ...css === void 0 ? {} : { css }
27395
- }
27864
+ },
27865
+ ...resolved.images.length > 0 ? { images: resolved.images } : {},
27866
+ ...resolved.omissions.length > 0 ? { omissions: resolved.omissions } : {}
27396
27867
  });
27397
27868
  } };
27398
27869
  }
27399
27870
  /**
27871
+ * 把产物 HTML 里的图片占位换成可直接显示的 `src`(0.22.0 FR-11.6)。
27872
+ *
27873
+ * 落在引擎而不是宿主或投放面,理由与 `documentSurface` 端口本身相同:要读产物就得知道
27874
+ * 产物目录布局,那是引擎独有的知识。`startViewerShell` 因此一行不改。
27875
+ *
27876
+ * 解析发生在宿主的确认框之前,用户取消时白做一次——接受这个代价,因为**体积与像素的
27877
+ * 度量必须看到已填字节的文档**,否则量到的是一份全是占位的空壳。
27878
+ */
27879
+ async #resolveDocumentImages(html, runId) {
27880
+ if (!html.includes(IMAGE_REF_ATTR)) return {
27881
+ html,
27882
+ images: [],
27883
+ omissions: []
27884
+ };
27885
+ const doc = new DOMParser().parseFromString(html, "text/html");
27886
+ const images = [];
27887
+ const omissions = [];
27888
+ for (const element of [...doc.querySelectorAll(`img[${IMAGE_REF_ATTR}]`)]) {
27889
+ const ref = element.getAttribute(IMAGE_REF_ATTR) ?? "";
27890
+ const alt = element.getAttribute("alt") ?? "";
27891
+ element.removeAttribute(IMAGE_REF_ATTR);
27892
+ try {
27893
+ const src = await this.#documentImageSrc(ref, runId, images);
27894
+ element.setAttribute("src", src);
27895
+ } catch (e) {
27896
+ const reason = messageOf(e);
27897
+ omissions.push({
27898
+ ref,
27899
+ alt,
27900
+ stage: "resolve",
27901
+ reason
27902
+ });
27903
+ const note = doc.createElement("p");
27904
+ note.className = "wsdoc__image-missing";
27905
+ note.textContent = `[image unavailable] ${alt} — ${reason}`;
27906
+ element.replaceWith(note);
27907
+ }
27908
+ }
27909
+ return {
27910
+ html: doc.body.innerHTML,
27911
+ images,
27912
+ omissions
27913
+ };
27914
+ }
27915
+ /** 解析一条引用并把实测事实记进 `images`;失败一律抛错,由调用方转成归因 */
27916
+ async #documentImageSrc(ref, runId, images) {
27917
+ const separator = ref.indexOf(":");
27918
+ const kind = separator === -1 ? "" : ref.slice(0, separator);
27919
+ const rest = ref.slice(separator + 1);
27920
+ if (kind === "remote") {
27921
+ assertRemoteUrlAllowed(rest, { allowHttp: true });
27922
+ return rest;
27923
+ }
27924
+ const image = await toDocumentImage(await this.#documentImageBytes(kind, rest, runId), { maxBytes: MAX_DOCUMENT_IMAGE_BYTES });
27925
+ const src = `data:${image.mimeType};base64,${image.data}`;
27926
+ images.push({
27927
+ ref,
27928
+ width: image.width,
27929
+ height: image.height,
27930
+ units: src.length
27931
+ });
27932
+ return src;
27933
+ }
27934
+ async #documentImageBytes(kind, rest, runId) {
27935
+ if (kind === "artifact") {
27936
+ assertSafePathSegment(rest, "artifact");
27937
+ const read = (owner) => this.#adapter.storage.readBinary(resolveInsideRoot(`${this.#chatRoot}/artifacts/${owner}`, rest));
27938
+ try {
27939
+ return await read(runId);
27940
+ } catch {}
27941
+ for (const prior of await this.#priorRunIds(runId)) try {
27942
+ return await read(prior);
27943
+ } catch {}
27944
+ throw new WebSkillError("FS_NOT_FOUND", `artifact "${rest}" was not produced anywhere in this conversation`);
27945
+ }
27946
+ if (kind === "upload") {
27947
+ const attachment = await this.#uploadOfRun(runId, rest);
27948
+ if (attachment === void 0) throw new WebSkillError("UPLOAD_FILE_NOT_FOUND", `no attachment with id or file name "${rest}" was uploaded anywhere in this conversation`);
27949
+ return this.#adapter.storage.readBinary(resolveInsideRoot(this.attachmentsRoot, attachment.path));
27950
+ }
27951
+ throw new WebSkillError("VALIDATION_FAILED", `image reference "${kind}:${rest}" has no known prefix; expected artifact:, upload: or remote:`);
27952
+ }
27953
+ /**
27954
+ * 这一次 run 属于哪个会话。
27955
+ *
27956
+ * 进行中读内存,已结束读 trace——与 `#skillNamesOfRun` 同一范式。
27957
+ * 只读 trace 会让**工具调用期**的每一次查找扑空:trace 要等 run 收尾才 `put`,
27958
+ * 于是 `export_spreadsheet` 当场拿不到会话,而文档投放(用户事后点按钮)却正常,
27959
+ * 表现成「同一张图 Word 放得进、Excel 放不进」。
27960
+ */
27961
+ async #sessionOfRun(runId) {
27962
+ return this.#runSessions.get(runId) ?? (await this.#traceStore.get(runId).catch(() => void 0))?.sessionId;
27963
+ }
27964
+ /**
27965
+ * 本会话里 `runId` 之前的那些 run,由近及远。
27966
+ *
27967
+ * 「先出 Word,再出一份 PPT」是两轮 run,图片产物却落在第一轮的目录里;
27968
+ * 只认当前 run 会让第二份文档必然缺图,而且缺得毫无道理——用户看来那张图刚刚还在。
27969
+ *
27970
+ * 放宽止于**本会话**:目录仍由引擎按 runId 拼,产物名仍过 `assertSafePathSegment`,
27971
+ * 模型既给不出别的会话,也给不出路径(FR-11.7 的守卫面一寸没让)。
27972
+ */
27973
+ async #priorRunIds(runId) {
27974
+ const sessionId = await this.#sessionOfRun(runId);
27975
+ if (sessionId === void 0) return [];
27976
+ const messages = (await this.#sessions.get(sessionId).catch(() => void 0))?.messages ?? [];
27977
+ const ids = [];
27978
+ for (const message of messages) {
27979
+ const id = message.runId;
27980
+ if (message.role !== "assistant" || id === void 0 || id === runId || ids.includes(id)) continue;
27981
+ ids.push(id);
27982
+ }
27983
+ return ids.reverse();
27984
+ }
27985
+ /**
27986
+ * 表格里那张图的字节(0.22.0 分册 20 FR-20.8)。
27987
+ *
27988
+ * 与投放文档共用 `#documentImageBytes`:AC-G1 要的不是几条通道行为一致,而是只有一条通道。
27989
+ * 「同一张图 Word 放得进、Excel 放不进」正是分叉出第二条通道的代价。
27990
+ *
27991
+ * 外链在这里是硬拒:xlsx 里没有「链着一张图」这回事,工作簿必须自带字节。
27992
+ */
27993
+ async #spreadsheetImage(ref, runId) {
27994
+ if (runId === void 0) throw new WebSkillError("TOOL_EXECUTION_FAILED", "Putting a picture in a workbook requires an active run.");
27995
+ const separator = ref.indexOf(":");
27996
+ const kind = separator === -1 ? "" : ref.slice(0, separator);
27997
+ const rest = ref.slice(separator + 1);
27998
+ if (kind === "remote") throw new WebSkillError("VALIDATION_FAILED", `image reference "${ref}" cannot go into a workbook: a spreadsheet carries the bytes, not a link. Download it with fetch_remote_image first, then reference the artifact it returns.`);
27999
+ const image = await toDocumentImage(await this.#documentImageBytes(kind, rest, runId), { maxBytes: SPREADSHEET_LIMITS.imageBytes });
28000
+ return {
28001
+ bytes: Uint8Array.from(atob(image.data), (char) => char.charCodeAt(0)),
28002
+ contentType: image.mimeType
28003
+ };
28004
+ }
28005
+ /**
28006
+ * 会话里的上传件,从发起本 run 的那条用户消息起往前找。
28007
+ *
28008
+ * 与 `#priorRunIds` 同一个理由:「上传照片 → 出 Word → 再出 PPT」是两轮,
28009
+ * 第二轮那条用户消息上没有附件,只认本轮就必然缺图。附件不随轮次过期。
28010
+ *
28011
+ * 仍然**在消息里查**而不是拿引用当路径拼:候选全部是用户自己在这个会话里上传的东西,
28012
+ * 路径一寸都不来自模型,跨会话与路径穿越都无从谈起(FR-11.7)。
28013
+ * 落到哪条附件上由 `findAttachmentByRef` 判,与正文 markdown 那一侧共用一份规则。
28014
+ */
28015
+ async #uploadOfRun(runId, ref) {
28016
+ const sessionId = await this.#sessionOfRun(runId);
28017
+ if (sessionId === void 0) return void 0;
28018
+ const messages = (await this.#sessions.get(sessionId).catch(() => void 0))?.messages ?? [];
28019
+ const started = messages.findIndex((message) => message.role === "assistant" && message.runId === runId);
28020
+ for (let i = (started === -1 ? messages.length : started) - 1; i >= 0; i -= 1) {
28021
+ const message = messages[i];
28022
+ if (message?.role !== "user") continue;
28023
+ const hit = findAttachmentByRef(message.attachments ?? [], ref);
28024
+ if (hit !== void 0) return hit;
28025
+ }
28026
+ }
28027
+ /**
27400
28028
  * 产物名 → 绝对路径。`assertSafePathSegment` + `resolveInsideRoot` 两道都不是可选写法:
27401
28029
  * 少了它们,「组件只收产物名」就退化成「组件收完整路径」,凭空开出一个读取面。
27402
28030
  */
@@ -27405,6 +28033,84 @@ var ChatEngine = class {
27405
28033
  return resolveInsideRoot(dir, artifact);
27406
28034
  }
27407
28035
  /**
28036
+ * 把宿主页面上的一张图落成本 run 的产物(0.22.0 FR-12.1 / FR-12.7)。
28037
+ *
28038
+ * 三件事按顺序发生且都在引擎侧:授权 → 取字节 → 落产物。宿主只做中间那一步,
28039
+ * 因为「run 目录在哪、会话什么时候换」只有引擎知道。
28040
+ */
28041
+ async #capturePageImage(host, artifacts, input, runId) {
28042
+ if (runId === void 0) throw new WebSkillError("TOOL_EXECUTION_FAILED", "Capturing a picture requires an active run.");
28043
+ const note = input.note === void 0 ? void 0 : sanitizeUntrustedLine(input.note);
28044
+ if (!await (this.#imageSources ??= new ImageSourceAuthorizer({ ui: this.#uiBridge })).authorize("page-image", note === void 0 ? {} : { note })) return {
28045
+ reason: "the user declined to let the assistant copy pictures from the page",
28046
+ triedLevels: []
28047
+ };
28048
+ const captured = await host.capture(input.ref, MAX_DOCUMENT_IMAGE_BYTES);
28049
+ if ("reason" in captured) return {
28050
+ reason: captured.reason,
28051
+ triedLevels: captured.triedLevels
28052
+ };
28053
+ const seq = (this.#pageImageSeq.get(runId) ?? 0) + 1;
28054
+ this.#pageImageSeq.set(runId, seq);
28055
+ const path = `page-image-${String(seq)}.${captured.mimeType === "image/jpeg" ? "jpg" : "png"}`;
28056
+ await artifacts.createBinaryArtifact({
28057
+ runId,
28058
+ path,
28059
+ content: Uint8Array.from(atob(captured.data), (char) => char.charCodeAt(0)),
28060
+ mimeType: captured.mimeType,
28061
+ metadata: { resultCard: false }
28062
+ });
28063
+ return {
28064
+ artifact: `artifact:${path}`,
28065
+ width: captured.width,
28066
+ height: captured.height,
28067
+ bytes: captured.bytes
28068
+ };
28069
+ }
28070
+ /**
28071
+ * 把一张外链图落成本 run 的产物(0.22.0 FR-13.1 ~ FR-13.4)。
28072
+ *
28073
+ * 与 `#capturePageImage` 同一条顺序:授权 → 取字节 → 落产物。
28074
+ * **授权在 `fetch` 之前**不是风格问题:拒绝之后网络层必须一个请求都没发出去(AC-13.5),
28075
+ * 「先抓回来再丢掉」时用户的 IP 已经交出去了。
28076
+ */
28077
+ async #fetchRemoteImage(host, artifacts, input, runId) {
28078
+ if (runId === void 0) throw new WebSkillError("TOOL_EXECUTION_FAILED", "Downloading an image requires an active run.");
28079
+ const note = input.note === void 0 ? void 0 : sanitizeUntrustedLine(input.note);
28080
+ if (!await (this.#imageSources ??= new ImageSourceAuthorizer({ ui: this.#uiBridge })).authorize("remote-image", {
28081
+ ...note === void 0 ? {} : { note },
28082
+ url: sanitizeUntrustedLine(input.url)
28083
+ })) return {
28084
+ reason: "the user declined to let the assistant download images from the web",
28085
+ kind: "declined"
28086
+ };
28087
+ const fetched = await host.fetch(input.url, MAX_DOCUMENT_IMAGE_BYTES);
28088
+ if ("kind" in fetched) return {
28089
+ reason: fetched.reason,
28090
+ kind: fetched.kind
28091
+ };
28092
+ const seq = (this.#remoteImageSeq.get(runId) ?? 0) + 1;
28093
+ this.#remoteImageSeq.set(runId, seq);
28094
+ const path = `remote-image-${String(seq)}.${fetched.mimeType === "image/jpeg" ? "jpg" : "png"}`;
28095
+ await artifacts.createBinaryArtifact({
28096
+ runId,
28097
+ path,
28098
+ content: Uint8Array.from(atob(fetched.data), (char) => char.charCodeAt(0)),
28099
+ mimeType: fetched.mimeType,
28100
+ metadata: { resultCard: false }
28101
+ });
28102
+ return {
28103
+ artifact: `artifact:${path}`,
28104
+ width: fetched.width,
28105
+ height: fetched.height,
28106
+ bytes: fetched.bytes,
28107
+ ...input.describe === true ? {
28108
+ data: fetched.data,
28109
+ mimeType: fetched.mimeType
28110
+ } : {}
28111
+ };
28112
+ }
28113
+ /**
27408
28114
  * 这次 run 激活过的技能名。它是授权告知的主语,因此**不能**由发出 surface 的技能自报——
27409
28115
  * 那等于让被授权方自己写授权提示。
27410
28116
  *
@@ -27511,6 +28217,7 @@ var ChatEngine = class {
27511
28217
  });
27512
28218
  this.#currentSessionId = session.id;
27513
28219
  this.#visionDelegate?.forgetSessionDecision();
28220
+ this.#imageSources?.forgetSessionDecisions();
27514
28221
  if (previous !== void 0 && previous !== session.id) {
27515
28222
  this.#endSession(previous, "switched");
27516
28223
  this.#applySelectedModel(void 0);
@@ -27529,6 +28236,7 @@ var ChatEngine = class {
27529
28236
  const previous = this.#currentSessionId;
27530
28237
  this.#currentSessionId = id;
27531
28238
  if (previous !== void 0 && previous !== id) this.#endSession(previous, "switched");
28239
+ if (previous !== id) this.#imageSources?.forgetSessionDecisions();
27532
28240
  if (options.cursor === void 0) {
27533
28241
  const stored = (await this.#sessions.getMeta(id))?.modelId;
27534
28242
  this.#applySelectedModel(stored ?? [...page.items].reverse().find((message) => message.model !== void 0)?.model);
@@ -27746,12 +28454,26 @@ var ChatEngine = class {
27746
28454
  text: handle
27747
28455
  });
27748
28456
  if (attachment.kind === "text" || attachment.kind === "document-text") {
27749
- const body = await this.#adapter.storage.readText(path);
28457
+ const body = await this.#attachmentText(attachment, path);
28458
+ if (body === void 0) {
28459
+ parts.push({
28460
+ type: "text",
28461
+ text: `Attachment "${attachment.name}" (${attachment.contentType}) is attached, but no text extractor for this format is available in this environment, so its body is not inlined below.` + (this.#hasDocumentReader(attachment) ? ` Use read_document with id ${attachment.id} to read it unit by unit.` : "")
28462
+ });
28463
+ continue;
28464
+ }
27750
28465
  const routed = this.#routeText(attachment, body, attachment.kind);
27751
28466
  parts.push(routed.part);
27752
28467
  if (routed.notice !== void 0) notices.push(routed.notice);
27753
28468
  continue;
27754
28469
  }
28470
+ if (attachment.kind === "binary") {
28471
+ parts.push({
28472
+ type: "text",
28473
+ text: `Attachment "${attachment.name}" (${attachment.size} bytes) is available to tools as id ${attachment.id}. Its contents are not included here — pass the id to a tool that reads this format.`
28474
+ });
28475
+ continue;
28476
+ }
27755
28477
  const data = toBase64(await this.#adapter.storage.readBinary(path));
27756
28478
  if (attachment.kind === "image") {
27757
28479
  parts.push({
@@ -27775,7 +28497,7 @@ var ChatEngine = class {
27775
28497
  };
27776
28498
  }
27777
28499
  /**
27778
- * 文本类附件进上下文的预算(`loop.maxDocumentTextBytes`,默认 500 KB)。
28500
+ * 文本类附件进上下文的预算(`loop.maxDocumentTextBytes`,默认 5000 KB)。
27779
28501
  * 与 agentLoop 的 `#documentTooLarge` 同一个值、同一条口径。
27780
28502
  */
27781
28503
  #textBudget() {
@@ -27801,11 +28523,38 @@ var ChatEngine = class {
27801
28523
  * 给了 id 就是教模型去调一个不存在的工具。
27802
28524
  */
27803
28525
  #documentHandleLabel(attachment) {
27804
- const mime = attachment.contentType.split(";")[0]?.trim().toLowerCase();
27805
- if ((mime === SUPPORTED_DOCUMENT_MIME.pdf ? this.#adapter.pdfDocumentReader : mime === SUPPORTED_DOCUMENT_MIME.docx ? this.#adapter.docxDocumentReader : mime === SUPPORTED_DOCUMENT_MIME.xlsx ? this.#adapter.xlsxDocumentReader : void 0) === void 0) return void 0;
28526
+ if (!this.#hasDocumentReader(attachment)) return void 0;
27806
28527
  return `--- Attachment: ${attachment.name} (${attachment.contentType}, id: ${attachment.id}) ---\nread_document accepts this id while this turn lasts. Use it whenever you need the pictures, charts, diagrams or scanned pages inside this document: extracted text never contains them, and an endpoint that refuses attached files leaves you with the text alone. Do not answer "the document has no images" without reading it this way first.`;
27807
28528
  }
27808
28529
  /**
28530
+ * 这份附件的格式有没有注册文档读取器,即 `read_document` 能不能读它(0.22.0 分册 16 FR-16.1)。
28531
+ *
28532
+ * 取件号与「超限改走分批」共用这一个判据:判反了不是少一句说明,
28533
+ * 而是给出一个调不到的工具(没 reader 却给 id),或者白白丢掉全文(有 reader 却当没有)。
28534
+ */
28535
+ #hasDocumentReader(attachment) {
28536
+ const mime = attachment.contentType.split(";")[0]?.trim().toLowerCase();
28537
+ return (mime === SUPPORTED_DOCUMENT_MIME.pdf ? this.#adapter.pdfDocumentReader : mime === SUPPORTED_DOCUMENT_MIME.docx ? this.#adapter.docxDocumentReader : mime === SUPPORTED_DOCUMENT_MIME.xlsx ? this.#adapter.xlsxDocumentReader : mime === SUPPORTED_DOCUMENT_MIME.pptx ? this.#adapter.pptxDocumentReader : void 0) !== void 0;
28538
+ }
28539
+ /**
28540
+ * 文本类附件的正文。`text` 存的就是文本;`document-text` 存的是**原件**,文本在这里现抽。
28541
+ *
28542
+ * 抽取推迟到发送,是为了让 `read_document` 还能拿到原件字节去读图表与扫描页——
28543
+ * 上传时就抽掉的话,那些东西在磁盘上根本不存在了(0.22.0 分册 18)。
28544
+ *
28545
+ * 返回 `undefined` = 本环境抽不出文本。不抛错:附件是上传时被收下的,
28546
+ * 此刻让整条消息发不出去等于拿用户刚打的字陪绑,而原件还在盘上,模型仍可按 id 逐块读。
28547
+ */
28548
+ async #attachmentText(attachment, path) {
28549
+ if (attachment.kind === "text") return await this.#adapter.storage.readText(path);
28550
+ const bytes = await this.#adapter.storage.readBinary(path);
28551
+ if (!(bytes[0] === 80 && bytes[1] === 75)) return new TextDecoder().decode(bytes);
28552
+ const format = extractedDocumentFormat(attachment.contentType, attachment.name);
28553
+ const extract = format === "xlsx" ? this.#adapter.xlsxExtractor : format === "pptx" ? this.#adapter.pptxExtractor : this.#adapter.docxExtractor;
28554
+ if (extract === void 0) return void 0;
28555
+ return await extract(bytes);
28556
+ }
28557
+ /**
27809
28558
  * 文本类附件(纯文本 / docx / xlsx / 抽取出来的 PDF 文本)的发送形态。
27810
28559
  *
27811
28560
  * 预算走 `maxDocumentTextBytes` 而不是 `formatAttachmentText` 的 32 KiB 兜底:
@@ -27817,16 +28566,24 @@ var ChatEngine = class {
27817
28566
  const size = new TextEncoder().encode(body).length;
27818
28567
  if (size > max) {
27819
28568
  const locale = this.#liveConfig?.appearance.locale ?? "en";
28569
+ const params = {
28570
+ name: attachment.name,
28571
+ size: formatSize(size),
28572
+ limit: formatSize(max)
28573
+ };
28574
+ if (this.#hasDocumentReader(attachment)) return {
28575
+ part: {
28576
+ type: "text",
28577
+ text: `--- Attachment: ${attachment.name} (${attachment.contentType}, not attached, read it in parts) ---\nIts text alone is ${size} bytes, over the ${max} byte context limit, so it is not in this message. It was not truncated, because a partial document would be unusable. Call read_document with id ${attachment.id} to read it a block at a time — each result tells you how much is left and which cursor continues from there. Read as many times as the task needs before answering.`
28578
+ },
28579
+ notice: translateText(locale, "attachment.pdfAsPagedRead", params)
28580
+ };
27820
28581
  return {
27821
28582
  part: {
27822
28583
  type: "text",
27823
28584
  text: `--- Attachment: ${attachment.name} (${attachment.contentType}, not attached) ---\nIts text is ${size} bytes, over the ${max} byte context limit for attached text. It was not truncated, because a partial document would be unusable. Tell the user to raise the extracted-text limit in settings, or to attach a shorter excerpt or a specific section.`
27824
28585
  },
27825
- notice: translateText(locale, "attachment.textTooLong", {
27826
- name: attachment.name,
27827
- size: formatSize(size),
27828
- limit: formatSize(max)
27829
- })
28586
+ notice: translateText(locale, "attachment.textTooLong", params)
27830
28587
  };
27831
28588
  }
27832
28589
  return { part: {
@@ -27841,9 +28598,11 @@ var ChatEngine = class {
27841
28598
  } };
27842
28599
  }
27843
28600
  /**
27844
- * 单个 PDF 附件的发送形态(FR-18.5)。阈值内直传原件——保住扫描件、版面与图表;
27845
- * 超阈值才退到抽取文本,抽不出就**拒绝并说明**,绝不截断:半份 PDF 是坏文件,
27846
- * 半份抽取文本会让模型以为自己读全了(FR-18.5a,与 `#documentTooLarge` 同口径)。
28601
+ * 单个 PDF 附件的发送形态(FR-18.5;0.22.0 分册 16 改写超阈值那一支)。
28602
+ *
28603
+ * 阈值内直传原件——保住扫描件、版面与图表。超阈值分两种走法:
28604
+ * 该格式**注册了读取器**时交给 `read_document` 分批读(FR-16.1),正文一个字都不进消息;
28605
+ * 没有读取器时才退到抽取文本,抽不出就**拒绝并说明**,绝不截断(FR-18.5a)。
27847
28606
  */
27848
28607
  async #routeDocument(attachment, data) {
27849
28608
  const max = this.#documentBudget();
@@ -27867,6 +28626,13 @@ var ChatEngine = class {
27867
28626
  },
27868
28627
  notice: translateText(locale, key, params)
27869
28628
  });
28629
+ if (this.#hasDocumentReader(attachment)) return {
28630
+ part: {
28631
+ type: "text",
28632
+ text: `${label("not attached, read it in pages")}The document is ${data.length} bytes once encoded, over the ${max} byte request limit, so the file itself is not in this message. Its text is not here either: a document this size does not fit the context window in one piece, and a partial copy would be unusable. Call read_document with id ${attachment.id} to read it page by page — each result tells you how much is left and which cursor continues from there. Read as many times as the task needs before answering.`
28633
+ },
28634
+ notice: translateText(locale, "attachment.pdfAsPagedRead", params)
28635
+ };
27870
28636
  const extract = this.#adapter.pdfExtractor;
27871
28637
  if (extract === void 0) return refuse("This environment has no PDF text extractor, so no fallback is available. Tell the user to switch to a model that accepts large documents, or to attach the content as a Word or plain-text file instead.", "attachment.pdfNoExtractor");
27872
28638
  let body;
@@ -28227,6 +28993,7 @@ var ChatEngine = class {
28227
28993
  ...this.#adapter.downloads.consent ? { consent: this.#adapter.downloads.consent } : {},
28228
28994
  ...this.#adapter.docxExtractor ? { docxExtractor: this.#adapter.docxExtractor } : {},
28229
28995
  ...this.#adapter.xlsxExtractor ? { xlsxExtractor: this.#adapter.xlsxExtractor } : {},
28996
+ ...this.#adapter.pptxExtractor ? { pptxExtractor: this.#adapter.pptxExtractor } : {},
28230
28997
  ...this.#adapter.pdfExtractor ? { pdfExtractor: this.#adapter.pdfExtractor } : {},
28231
28998
  maxDocumentBytes: () => this.#documentBudget(),
28232
28999
  ...this.#adapter.documentAudit ? { audit: this.#adapter.documentAudit } : {}
@@ -28235,17 +29002,55 @@ var ChatEngine = class {
28235
29002
  root: `${this.#chatRoot}/artifacts`,
28236
29003
  fs
28237
29004
  });
29005
+ const readDocumentBytes = async (id, context) => {
29006
+ const attachment = this.#runAttachments.find((a) => a.id === id);
29007
+ if (attachment) {
29008
+ const bytes = await this.#adapter.storage.readBinary(`${this.attachmentsRoot}/${attachment.path}`);
29009
+ return {
29010
+ name: attachment.name,
29011
+ bytes
29012
+ };
29013
+ }
29014
+ const stored = context?.binaries?.get(id);
29015
+ if (stored) return {
29016
+ name: stored.name,
29017
+ bytes: stored.bytes
29018
+ };
29019
+ if (!downloadPolicy) return void 0;
29020
+ const downloaded = await downloadPolicy.readBytes(id);
29021
+ return {
29022
+ name: downloaded.entry.name,
29023
+ bytes: downloaded.bytes
29024
+ };
29025
+ };
29026
+ /**
29027
+ * 模板字节比 `read_document` 多认一条取件号:`weboffice:<handle>`(0.22.0 分册 42 FR-42.12)。
29028
+ *
29029
+ * 多的只是**取件**这一段——拿到字节之后走的与本地模板是同一条路(FR-42.12 后半句)。
29030
+ * 前缀是显式的:不去猜某个 id 会不会恰好是个 weboffice handle,猜错的代价是
29031
+ * 拿了另一份文档冒充用户指的那一份。前缀不匹配时连问都不问,直接当作没有。
29032
+ */
29033
+ const readTemplateBytes = async (id, context) => {
29034
+ const local = await readDocumentBytes(id, context);
29035
+ if (local) return local;
29036
+ const online = this.#adapter.webOfficeTemplateSource;
29037
+ if (!online || !id.startsWith("weboffice:")) return void 0;
29038
+ return await online.read(id.slice(10), context);
29039
+ };
28238
29040
  const baseTools = [
28239
29041
  ...pageSkillSource ? [pageSkillSource] : [],
28240
- createSpreadsheetToolSource({ writeArtifact: (path, content, mimeType, context) => {
28241
- if (!context?.runId) throw new WebSkillError("TOOL_EXECUTION_FAILED", "Writing a workbook requires an active run.");
28242
- return artifactStore.createBinaryArtifact({
28243
- runId: context.runId,
28244
- path,
28245
- content,
28246
- mimeType
28247
- });
28248
- } }),
29042
+ createSpreadsheetToolSource({
29043
+ writeArtifact: (path, content, mimeType, context) => {
29044
+ if (!context?.runId) throw new WebSkillError("TOOL_EXECUTION_FAILED", "Writing a workbook requires an active run.");
29045
+ return artifactStore.createBinaryArtifact({
29046
+ runId: context.runId,
29047
+ path,
29048
+ content,
29049
+ mimeType
29050
+ });
29051
+ },
29052
+ resolveImage: (ref, context) => this.#spreadsheetImage(ref, context?.runId)
29053
+ }),
28249
29054
  ...(rc === void 0 ? this.#options.generativeUi === true : rc.agentCapabilities.generativeUi) ? [createUiCatalogToolSource(rc ? { presets: rc.agentCapabilities.uiPresets } : {})] : [],
28250
29055
  ...rc?.agentCapabilities.todo === false ? [] : [createTodoToolSource({ store: this.#todoStore })],
28251
29056
  ...rc?.agentCapabilities.skillGeneration === true ? [createSkillGenerationToolSource({
@@ -28272,29 +29077,51 @@ var ChatEngine = class {
28272
29077
  }
28273
29078
  })] : [],
28274
29079
  ...this.#adapter.pageActions ? [createPageActionToolSource({ policy: this.#adapter.pageActions })] : [],
29080
+ ...this.#adapter.pageImage ? [createPageImageToolSource({ capture: (input, context) => this.#capturePageImage(this.#adapter.pageImage, artifactStore, input, context?.runId) })] : [],
29081
+ ...this.#adapter.remoteImage ? [createRemoteImageToolSource({ fetchImage: (input, context) => this.#fetchRemoteImage(this.#adapter.remoteImage, artifactStore, input, context?.runId) })] : [],
28275
29082
  ...this.#adapter.pageTabs ? [createTabWorksetToolSource({ policy: this.#adapter.pageTabs })] : [],
28276
29083
  ...downloadPolicy ? [createDownloadedFileToolSource({ policy: downloadPolicy })] : [],
28277
29084
  createDocumentToolSource({
28278
- files: { read: async (id) => {
28279
- const attachment = this.#runAttachments.find((a) => a.id === id);
28280
- if (attachment) {
28281
- const bytes = await this.#adapter.storage.readBinary(`${this.attachmentsRoot}/${attachment.path}`);
28282
- return {
28283
- name: attachment.name,
28284
- bytes
28285
- };
28286
- }
28287
- if (!downloadPolicy) return void 0;
28288
- const downloaded = await downloadPolicy.readBytes(id);
28289
- return {
28290
- name: downloaded.entry.name,
28291
- bytes: downloaded.bytes
28292
- };
28293
- } },
29085
+ files: { read: readDocumentBytes },
28294
29086
  ...this.#adapter.pdfDocumentReader ? { pdf: this.#adapter.pdfDocumentReader } : {},
28295
29087
  ...this.#adapter.docxDocumentReader ? { docx: this.#adapter.docxDocumentReader } : {},
28296
- ...this.#adapter.xlsxDocumentReader ? { xlsx: this.#adapter.xlsxDocumentReader } : {}
29088
+ ...this.#adapter.xlsxDocumentReader ? { xlsx: this.#adapter.xlsxDocumentReader } : {},
29089
+ ...this.#adapter.pptxDocumentReader ? { pptx: this.#adapter.pptxDocumentReader } : {}
28297
29090
  }),
29091
+ ...this.#adapter.xlsxTemplateEngine || this.#adapter.docxTemplateEngine ? [createTemplateToolSource({
29092
+ files: { read: readTemplateBytes },
29093
+ ...this.#adapter.webOfficeTemplateSource ? { idHint: `For a document open in an online WPS/WebOffice viewer, use "${WEB_OFFICE_TEMPLATE_PREFIX}<handle>" with the handle from list_weboffice_documents.` } : {},
29094
+ ...this.#adapter.xlsxTemplateEngine ? { xlsx: this.#adapter.xlsxTemplateEngine } : {},
29095
+ ...this.#adapter.docxTemplateEngine ? { docx: this.#adapter.docxTemplateEngine } : {},
29096
+ writeArtifact: (path, content, mimeType, context) => {
29097
+ if (!context?.runId) throw new WebSkillError("TOOL_EXECUTION_FAILED", "Filling a template requires an active run.");
29098
+ return artifactStore.createBinaryArtifact({
29099
+ runId: context.runId,
29100
+ path,
29101
+ content,
29102
+ mimeType
29103
+ });
29104
+ },
29105
+ resolveImage: async (ref, context) => {
29106
+ const found = await this.#spreadsheetImage(ref, context?.runId);
29107
+ return {
29108
+ bytes: found.bytes,
29109
+ extension: found.contentType.replace(/^image\//, "") || "png",
29110
+ contentType: found.contentType
29111
+ };
29112
+ }
29113
+ })] : [],
29114
+ ...this.#adapter.dwg ? [createDwgToolSource({
29115
+ load: async (file, context) => {
29116
+ const found = await readDocumentBytes(file, context);
29117
+ if (!found) throw new WebSkillError("TOOL_EXECUTION_FAILED", `No file named "${file}" is available. Use one from the download list, a page link, or an attachment.`);
29118
+ return {
29119
+ document: await this.#adapter.dwg.read(found.bytes, found.name),
29120
+ name: found.name
29121
+ };
29122
+ },
29123
+ project: (input) => this.#adapter.dwg.project(input)
29124
+ })] : [],
28298
29125
  ...this.#adapter.webOffice ? [createWebOfficeToolSource({ policy: new WebOfficePolicy({
28299
29126
  host: this.#adapter.webOffice.host,
28300
29127
  ui: this.#uiBridge,
@@ -28345,6 +29172,7 @@ var ChatEngine = class {
28345
29172
  ...this.#adapter.linkedDocuments ? { linkedDocuments: this.#adapter.linkedDocuments } : {},
28346
29173
  ...this.#adapter.docxExtractor ? { docxExtractor: this.#adapter.docxExtractor } : {},
28347
29174
  ...this.#adapter.xlsxExtractor ? { xlsxExtractor: this.#adapter.xlsxExtractor } : {},
29175
+ ...this.#adapter.pptxExtractor ? { pptxExtractor: this.#adapter.pptxExtractor } : {},
28348
29176
  ...this.#adapter.pdfExtractor ? { pdfExtractor: this.#adapter.pdfExtractor } : {},
28349
29177
  visionDelegate: this.#visionDelegate,
28350
29178
  ...this.#adapter.documentAudit ? { documentAudit: this.#adapter.documentAudit } : {},
@@ -28366,6 +29194,7 @@ var ChatEngine = class {
28366
29194
  })] : []]
28367
29195
  });
28368
29196
  runtime.events.on("*", (event) => {
29197
+ this.#runSessions.set(event.runId, event.sessionId);
28369
29198
  if (event.phase === "route") {
28370
29199
  this.#runSkills.set(event.runId, /* @__PURE__ */ new Set());
28371
29200
  this.#runPhases.delete(event.runId);
@@ -28541,6 +29370,7 @@ var ChatEngine = class {
28541
29370
  message: assistantMessage
28542
29371
  });
28543
29372
  this.#runSkills.delete(run.id);
29373
+ this.#runSessions.delete(run.id);
28544
29374
  this.#runTodos.delete(run.id);
28545
29375
  this.#runPerceptions.delete(run.id);
28546
29376
  this.#runCandidates.delete(run.id);
@@ -29821,6 +30651,44 @@ function visionDelegationDetails(details) {
29821
30651
  ...typeof rememberLabel === "string" ? { rememberLabel } : {}
29822
30652
  };
29823
30653
  }
30654
+ /**
30655
+ * 图片来源授权卡的载荷(0.22.0 分册 11 §3.3 / 分册 12)。
30656
+ *
30657
+ * `kind` 决定风险说的是哪一件事:取页面上的图、还是去互联网上下载一张。
30658
+ * 两者共用一个能力名,所以**没有 `kind` 就不能渲染**——缺省回退成另一句话
30659
+ * 等于让用户在看不见来源的情况下点同意。
30660
+ */
30661
+ function imageSourceDetails(details) {
30662
+ if (typeof details !== "object" || details === null) return void 0;
30663
+ const record = details;
30664
+ const kind = record["kind"];
30665
+ if (kind !== "page-image" && kind !== "remote-image") return void 0;
30666
+ const note = record["note"];
30667
+ const url = record["url"];
30668
+ const rememberLabelKey = record["rememberLabelKey"];
30669
+ return {
30670
+ kind,
30671
+ ...typeof note === "string" && note !== "" ? { note } : {},
30672
+ ...typeof url === "string" && url !== "" ? { url } : {},
30673
+ ...record["rememberable"] === true ? { rememberable: true } : {},
30674
+ ...record["rememberScope"] === "session" ? { rememberScope: "session" } : {},
30675
+ ...typeof rememberLabelKey === "string" ? { rememberLabelKey } : {}
30676
+ };
30677
+ }
30678
+ /**
30679
+ * 这个地址是不是明文 http。
30680
+ *
30681
+ * 只认真正的 `http:` 协议,不做子串匹配——`https://a/?next=http://b` 里的那截
30682
+ * 不是本次要发的请求,据此吓唬用户会让这条提示很快被当成噪音忽略掉。
30683
+ */
30684
+ function isInsecureUrl(url) {
30685
+ if (url === void 0) return false;
30686
+ try {
30687
+ return new URL(url).protocol === "http:";
30688
+ } catch {
30689
+ return false;
30690
+ }
30691
+ }
29824
30692
  /** 五个沙箱能力闸门的共用载荷(`gate()` 统一注入 `skill`,其余按能力各带一项) */ function sandboxDetails(details) {
29825
30693
  if (typeof details !== "object" || details === null) return void 0;
29826
30694
  const record = details;
@@ -29910,6 +30778,14 @@ function localizedAuthorizeMessage(request, t) {
29910
30778
  host: details.endpointHost
29911
30779
  });
29912
30780
  }
30781
+ if (request.capability === "readImageSource") {
30782
+ const details = imageSourceDetails(request.details);
30783
+ if (details === void 0) return void 0;
30784
+ const head = details.kind === "remote-image" && details.url !== void 0 ? t("interaction.imageSource.remoteImageBody", { url: details.url }) : t(`interaction.imageSource.${details.kind === "remote-image" ? "remoteImageAnyBody" : "pageImageBody"}`);
30785
+ const insecure = isInsecureUrl(details.url) ? ` ${t("interaction.imageSource.insecure")}` : "";
30786
+ if (details.note === void 0) return `${head}${insecure}`;
30787
+ return `${head}${insecure} ${t("interaction.imageSource.note", { note: details.note })}`;
30788
+ }
29913
30789
  }
29914
30790
 
29915
30791
  //#endregion
@@ -29976,7 +30852,7 @@ function InteractionWaitBar({ bridge, label, elsewhere = false, panel, onLocate
29976
30852
  id: INTERACTION_WAIT_ID,
29977
30853
  "data-testid": "interaction-wait",
29978
30854
  role: "status",
29979
- className: "webskill-chat-prose mx-auto flex w-full items-center gap-2 px-4 pt-2 text-xs text-muted",
30855
+ className: "webskill-chat-prose mx-auto flex w-full shrink-0 items-center gap-2 px-4 pt-2 text-xs text-muted",
29980
30856
  children: [
29981
30857
  /* @__PURE__ */ jsx(Hourglass, {
29982
30858
  className: "size-3.5 shrink-0",
@@ -42189,6 +43065,8 @@ function attachmentAccept(options) {
42189
43065
  if (options.files) parts.push(...FILE_MIME_TYPES);
42190
43066
  if (options.documents) parts.push(DOCX_MIME, ".docx");
42191
43067
  if (options.spreadsheets) parts.push(XLSX_MIME, ".xlsx");
43068
+ if (options.presentations) parts.push(PPTX_MIME, ".pptx");
43069
+ if (options.drawings) parts.push(...DWG_MIME_TYPES, ...BINARY_EXTENSIONS.map((ext) => `.${ext}`));
42192
43070
  return parts.join(",");
42193
43071
  }
42194
43072
 
@@ -42207,11 +43085,14 @@ function safeName(name) {
42207
43085
  const cleaned = (name.split(/[\\/]/).pop() ?? "attachment").replace(/[^\w.\- ]+/g, "_").replace(/^\.+/, "");
42208
43086
  return cleaned === "" ? "attachment" : cleaned.slice(0, 80);
42209
43087
  }
42210
- /** 两种待抽取格式共用 `document-text` 这一种 kind,选抽取器时要按格式再分一次 */
42211
- function isSpreadsheet(file) {
42212
- const type = file.type.toLowerCase();
42213
- if (type !== "") return type === SUPPORTED_DOCUMENT_MIME.xlsx;
42214
- return file.name.toLowerCase().endsWith(".xlsx");
43088
+ /** 三种待抽取格式共用 `document-text` 这一种 kind,选抽取器时要按格式再分一次 */
43089
+ const EXTRACTED_LABEL = {
43090
+ docx: "Word documents",
43091
+ xlsx: "Excel workbooks",
43092
+ pptx: "PowerPoint presentations"
43093
+ };
43094
+ function formatOf(file) {
43095
+ return extractedDocumentFormat(file.type, file.name);
42215
43096
  }
42216
43097
  /**
42217
43098
  * 附件落盘到 `<attachmentsRoot>/<sessionId>/`,`ChatEngine.send` 再按路径读回构造 `LlmContentPart[]`。
@@ -42230,7 +43111,9 @@ var WebSkillAttachmentAdapter = class {
42230
43111
  images: this.#imagesAllowed(),
42231
43112
  files: true,
42232
43113
  documents: this.#options.docxExtractor !== void 0,
42233
- spreadsheets: this.#options.xlsxExtractor !== void 0
43114
+ spreadsheets: this.#options.xlsxExtractor !== void 0,
43115
+ presentations: this.#options.pptxExtractor !== void 0,
43116
+ drawings: this.#options.drawingsSupported?.() ?? false
42234
43117
  });
42235
43118
  }
42236
43119
  #multimodal() {
@@ -42240,7 +43123,10 @@ var WebSkillAttachmentAdapter = class {
42240
43123
  return this.#multimodal().imageAttachments && (this.#options.imageCapable?.() ?? false);
42241
43124
  }
42242
43125
  #extractorFor(file) {
42243
- return isSpreadsheet(file) ? this.#options.xlsxExtractor : this.#options.docxExtractor;
43126
+ const format = formatOf(file);
43127
+ if (format === "xlsx") return this.#options.xlsxExtractor;
43128
+ if (format === "pptx") return this.#options.pptxExtractor;
43129
+ return this.#options.docxExtractor;
42244
43130
  }
42245
43131
  async add({ file }) {
42246
43132
  const kind = classifyAttachment(file.type, file.name);
@@ -42248,11 +43134,12 @@ var WebSkillAttachmentAdapter = class {
42248
43134
  if (kind === "image") {
42249
43135
  if (!this.#multimodal().imageAttachments) throw new WebSkillError("ATTACHMENT_TYPE_REJECTED", "Image attachments are turned off. Enable them in settings before sending images.");
42250
43136
  if (!(this.#options.imageCapable?.() ?? false)) throw new WebSkillError("MODEL_IMAGE_UNSUPPORTED", "Current model does not support image input. Switch model or remove the image.");
42251
- } else if (kind === "document-text" && this.#extractorFor(file) === void 0) throw new WebSkillError("ATTACHMENT_TYPE_REJECTED", `${isSpreadsheet(file) ? "Excel workbooks" : "Word documents"} are not supported in this environment: no ${isSpreadsheet(file) ? "xlsx" : "docx"} extractor is wired up.`);
43137
+ } else if (kind === "document-text" && this.#extractorFor(file) === void 0) throw new WebSkillError("ATTACHMENT_TYPE_REJECTED", `${EXTRACTED_LABEL[formatOf(file)]} are not supported in this environment: no ${formatOf(file)} extractor is wired up.`);
43138
+ else if (kind === "binary" && !(this.#options.drawingsSupported?.() ?? false)) throw new WebSkillError("ATTACHMENT_TYPE_REJECTED", `Drawings are not supported in this environment: no CAD reader is wired up.`);
42252
43139
  const id = `att-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
42253
43140
  const relative = `${this.#options.sessionId() ?? "pending"}/${id}-${safeName(file.name)}`;
42254
43141
  const path = `${this.#options.root}/${relative}`;
42255
- const contentType = file.type === "" ? "text/plain" : file.type;
43142
+ const contentType = file.type !== "" ? file.type : kind === "binary" ? "application/octet-stream" : "text/plain";
42256
43143
  try {
42257
43144
  this.#stored.set(id, await this.#store({
42258
43145
  id,
@@ -42296,9 +43183,8 @@ var WebSkillAttachmentAdapter = class {
42296
43183
  };
42297
43184
  }
42298
43185
  if (kind === "document-text") {
42299
- const extract = this.#extractorFor(file);
42300
- if (extract === void 0) throw new WebSkillError("ATTACHMENT_TYPE_REJECTED", "No document text extractor is wired up in this environment.");
42301
- await this.#options.storage.writeText(path, await extract(new Uint8Array(await file.arrayBuffer())));
43186
+ if (this.#extractorFor(file) === void 0) throw new WebSkillError("ATTACHMENT_TYPE_REJECTED", "No document text extractor is wired up in this environment.");
43187
+ await this.#writeStreaming(path, file);
42302
43188
  return {
42303
43189
  ...base,
42304
43190
  size: file.size
@@ -42311,6 +43197,13 @@ var WebSkillAttachmentAdapter = class {
42311
43197
  size: file.size
42312
43198
  };
42313
43199
  }
43200
+ if (kind === "binary") {
43201
+ await this.#writeStreaming(path, file);
43202
+ return {
43203
+ ...base,
43204
+ size: file.size
43205
+ };
43206
+ }
42314
43207
  const compressed = await compressImageToBudget(file, this.#multimodal().maxImageBytes);
42315
43208
  await this.#options.storage.writeBinary(path, new Uint8Array(await compressed.blob.arrayBuffer()));
42316
43209
  const recoded = compressed.compressedBytes !== compressed.originalBytes;
@@ -42832,8 +43725,9 @@ function ActiveInteractionCard({ request, bridge }) {
42832
43725
  const downloadedFile = isAuthorize && request.capability === "readDownloadedFile" ? downloadedFileDetails(request.details) : void 0;
42833
43726
  const webOffice = isAuthorize && request.capability === "readWebOfficeDocument" ? webOfficeDetails(request.details) : void 0;
42834
43727
  const visionDelegation = isAuthorize && request.capability === "delegateVision" ? visionDelegationDetails(request.details) : void 0;
43728
+ const imageSource = isAuthorize && request.capability === "readImageSource" ? imageSourceDetails(request.details) : void 0;
42835
43729
  const traceEvidence = isAuthorize ? traceEvidenceDetails(request.details) : void 0;
42836
- const localized = pageAction !== void 0 || downloadedFile !== void 0 || webOffice !== void 0 || visionDelegation !== void 0;
43730
+ const localized = pageAction !== void 0 || downloadedFile !== void 0 || webOffice !== void 0 || visionDelegation !== void 0 || imageSource !== void 0;
42837
43731
  const spec = interactionToUiSpec(request.type === "authorize" && localized ? {
42838
43732
  ...request,
42839
43733
  message: ""
@@ -43036,6 +43930,37 @@ function ActiveInteractionCard({ request, bridge }) {
43036
43930
  }), visionDelegation.rememberLabel ?? t(visionDelegation.rememberScope === "endpoint" ? "interaction.visionDelegation.rememberFallback" : "interaction.visionDelegation.rememberSession")]
43037
43931
  }) : null
43038
43932
  ] }) : null,
43933
+ imageSource ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
43934
+ /* @__PURE__ */ jsx("p", {
43935
+ "data-testid": "interaction-image-source-title",
43936
+ className: "text-sm font-medium text-ink",
43937
+ children: t("interaction.imageSource.title")
43938
+ }),
43939
+ /* @__PURE__ */ jsx("p", {
43940
+ "data-testid": "interaction-image-source-body",
43941
+ className: "text-sm text-ink",
43942
+ children: imageSource.kind === "remote-image" ? imageSource.url !== void 0 ? t("interaction.imageSource.remoteImageBody", { url: imageSource.url }) : t("interaction.imageSource.remoteImageAnyBody") : t("interaction.imageSource.pageImageBody")
43943
+ }),
43944
+ isInsecureUrl(imageSource.url) ? /* @__PURE__ */ jsx("p", {
43945
+ "data-testid": "interaction-image-source-insecure",
43946
+ className: "text-sm text-destructive",
43947
+ children: t("interaction.imageSource.insecure")
43948
+ }) : null,
43949
+ imageSource.note !== void 0 ? /* @__PURE__ */ jsx("p", {
43950
+ "data-testid": "interaction-image-source-note",
43951
+ className: "text-sm text-muted",
43952
+ children: t("interaction.imageSource.note", { note: imageSource.note })
43953
+ }) : null,
43954
+ imageSource.rememberable === true ? /* @__PURE__ */ jsxs("label", {
43955
+ className: "flex items-center gap-2 text-sm text-ink",
43956
+ children: [/* @__PURE__ */ jsx("input", {
43957
+ type: "checkbox",
43958
+ "data-testid": "interaction-image-source-remember",
43959
+ checked: remember,
43960
+ onChange: (event) => setRemember(event.currentTarget.checked)
43961
+ }), t(imageSource.rememberLabelKey ?? "interaction.imageSource.rememberFallback")]
43962
+ }) : null
43963
+ ] }) : null,
43039
43964
  traceEvidence ? /* @__PURE__ */ jsxs("div", {
43040
43965
  "data-testid": "interaction-trace-evidence",
43041
43966
  children: [/* @__PURE__ */ jsx("div", {
@@ -43694,6 +44619,7 @@ function Chatbot({ adapter, config, locale: localeProp, theme: themeProp, render
43694
44619
  multimodal: () => multimodalRef.current,
43695
44620
  imageCapable: () => imageCapableRef.current,
43696
44621
  notifyStorageError: (fileName) => storageToastRef.current?.(fileName),
44622
+ drawingsSupported: () => adapter.dwg !== void 0,
43697
44623
  ...adapter.docxExtractor ? { docxExtractor: adapter.docxExtractor } : {},
43698
44624
  ...adapter.xlsxExtractor ? { xlsxExtractor: adapter.xlsxExtractor } : {}
43699
44625
  }), [adapter, engine]);
@@ -43957,6 +44883,19 @@ function Chatbot({ adapter, config, locale: localeProp, theme: themeProp, render
43957
44883
  const bytes = await adapter.storage.readBinary(`${engine.attachmentsRoot}/${attachment.path}`);
43958
44884
  return URL.createObjectURL(new Blob([bytes], { type: attachment.contentType }));
43959
44885
  }, [adapter, engine]);
44886
+ const messagesRef = useRef(messages);
44887
+ messagesRef.current = messages;
44888
+ /** `![](upload:{id})` → 附件字节的 blob URL。模型输出不了 base64,正文贴附件图只能靠引用 */
44889
+ const resolveImageRef = useCallback(async (ref) => {
44890
+ const separator = ref.indexOf(":");
44891
+ if (separator === -1) return void 0;
44892
+ const kind = ref.slice(0, separator);
44893
+ const rest = ref.slice(separator + 1);
44894
+ if (kind !== "upload") return void 0;
44895
+ const attachment = findAttachmentByRef([...messagesRef.current].reverse().flatMap((message) => message.attachments ?? []), rest);
44896
+ if (attachment === void 0) return void 0;
44897
+ return resolveAttachmentUrl(attachment);
44898
+ }, [resolveAttachmentUrl]);
43960
44899
  const handleRetry = useCallback((messageId) => {
43961
44900
  flushPendingDeletes().then(() => engine.retry(messageId)).catch((e) => setError({ message: toErrorMessage(e) }));
43962
44901
  }, [engine, flushPendingDeletes]);
@@ -44123,187 +45062,190 @@ function Chatbot({ adapter, config, locale: localeProp, theme: themeProp, render
44123
45062
  onArchiveSession: handleArchiveSession,
44124
45063
  onUnarchiveSession: handleUnarchiveSession,
44125
45064
  onDeleteSession: handleDeleteSession,
44126
- children: /* @__PURE__ */ jsx(ThemeScope, {
44127
- ref: shellRef,
44128
- theme,
44129
- "data-layout": resolvedLayout,
44130
- "data-session-list": resolvedSessionList,
44131
- "data-session-narrow": narrow ? "true" : "false",
44132
- className: "webskill-aui-shell relative flex h-full min-h-0 flex-row font-sans text-ink",
44133
- children: /* @__PURE__ */ jsxs(LocalizedUndoToastProvider, { children: [/* @__PURE__ */ jsxs(Fragment$1, { children: [
44134
- /* @__PURE__ */ jsx(AttachmentStorageToastBridge, { sink: storageToastRef }),
44135
- narrow && sessionsExpanded ? /* @__PURE__ */ jsx("div", {
44136
- "data-testid": "session-list-overlay",
44137
- "aria-hidden": true,
44138
- onClick: () => setSessionsOpen(false),
44139
- className: "absolute inset-0 z-[var(--webskill-z-raised)] bg-scrim"
44140
- }) : null,
44141
- /* @__PURE__ */ jsx(AssistantUiSessionList, {
44142
- className: "webskill-aui-session-list",
44143
- onCollapse: () => setSessionsOpen(false),
44144
- ...narrow ? { onNavigate: () => setSessionsOpen(false) } : {},
44145
- hasEarlier: sessionsCursor !== void 0,
44146
- loadingEarlier: loadingEarlierSessions,
44147
- onLoadEarlier: handleLoadEarlierSessions,
44148
- source: sessionsSource,
44149
- skipped: skippedSessions
44150
- })
44151
- ] }), /* @__PURE__ */ jsxs("div", {
44152
- className: "webskill-chat-column flex min-h-0 min-w-0 flex-1 flex-col",
44153
- children: [
44154
- /* @__PURE__ */ jsx(AssistantUiThreadToolbar, {
44155
- ...config?.title ? { title: config.title } : {},
44156
- onOpenSessions: () => setSessionsOpen(true),
44157
- onCreateSession: handleCreate,
44158
- ...onOpenSettings ? { onOpenSettings: () => onOpenSettings() } : {},
44159
- ...docked !== void 0 ? { docked } : {},
44160
- ...onDock ? { onDock } : {},
44161
- ...onClose ? { onClose } : {}
44162
- }),
44163
- /* @__PURE__ */ jsx(InterruptedBanner, {
44164
- engine,
44165
- ...currentSessionId ? { sessionId: currentSessionId } : {}
44166
- }),
44167
- /* @__PURE__ */ jsxs("div", {
44168
- className: `flex min-h-0 flex-1 flex-col ${composerCentered ? "justify-center" : ""}`,
44169
- children: [
44170
- /* @__PURE__ */ jsx(AssistantUiViewport, {
44171
- grow: !composerCentered,
44172
- children: /* @__PURE__ */ jsxs("div", {
44173
- className: "webskill-chat-prose mx-auto flex w-full min-w-0 flex-col gap-4 px-4 py-6",
44174
- children: [
44175
- messages.length === 0 && !sending ? /* @__PURE__ */ jsx(AssistantUiEmptyState, {
44176
- ...config?.title ? { title: config.title } : {},
44177
- ...staticQuickPrompts ? { prompts: staticQuickPrompts } : {},
44178
- ...config?.dynamicQuickPrompts ? { dynamicPrompts: config.dynamicQuickPrompts } : {},
44179
- limit: quickPromptLimit,
44180
- disabled: sending,
44181
- onPrompt: handleSend
44182
- }) : null,
44183
- /* @__PURE__ */ jsx(MessageDeleteOutlet, {
44184
- queue: pendingDeletes,
44185
- messages,
44186
- ...currentSessionId ? { sessionId: currentSessionId } : {},
44187
- onRemove: (messageId) => setMessages((prev) => prev.filter((m) => m.id !== messageId)),
44188
- children: (handleDeleteMessage) => /* @__PURE__ */ jsx(AssistantUiMessageList, {
45065
+ children: /* @__PURE__ */ jsx(MarkdownImageResolverProvider, {
45066
+ resolve: resolveImageRef,
45067
+ children: /* @__PURE__ */ jsx(ThemeScope, {
45068
+ ref: shellRef,
45069
+ theme,
45070
+ "data-layout": resolvedLayout,
45071
+ "data-session-list": resolvedSessionList,
45072
+ "data-session-narrow": narrow ? "true" : "false",
45073
+ className: "webskill-aui-shell relative flex h-full min-h-0 flex-row overflow-hidden font-sans text-ink",
45074
+ children: /* @__PURE__ */ jsxs(LocalizedUndoToastProvider, { children: [/* @__PURE__ */ jsxs(Fragment$1, { children: [
45075
+ /* @__PURE__ */ jsx(AttachmentStorageToastBridge, { sink: storageToastRef }),
45076
+ narrow && sessionsExpanded ? /* @__PURE__ */ jsx("div", {
45077
+ "data-testid": "session-list-overlay",
45078
+ "aria-hidden": true,
45079
+ onClick: () => setSessionsOpen(false),
45080
+ className: "absolute inset-0 z-[var(--webskill-z-raised)] bg-scrim"
45081
+ }) : null,
45082
+ /* @__PURE__ */ jsx(AssistantUiSessionList, {
45083
+ className: "webskill-aui-session-list",
45084
+ onCollapse: () => setSessionsOpen(false),
45085
+ ...narrow ? { onNavigate: () => setSessionsOpen(false) } : {},
45086
+ hasEarlier: sessionsCursor !== void 0,
45087
+ loadingEarlier: loadingEarlierSessions,
45088
+ onLoadEarlier: handleLoadEarlierSessions,
45089
+ source: sessionsSource,
45090
+ skipped: skippedSessions
45091
+ })
45092
+ ] }), /* @__PURE__ */ jsxs("div", {
45093
+ className: "webskill-chat-column flex min-h-0 min-w-0 flex-1 flex-col",
45094
+ children: [
45095
+ /* @__PURE__ */ jsx(AssistantUiThreadToolbar, {
45096
+ ...config?.title ? { title: config.title } : {},
45097
+ onOpenSessions: () => setSessionsOpen(true),
45098
+ onCreateSession: handleCreate,
45099
+ ...onOpenSettings ? { onOpenSettings: () => onOpenSettings() } : {},
45100
+ ...docked !== void 0 ? { docked } : {},
45101
+ ...onDock ? { onDock } : {},
45102
+ ...onClose ? { onClose } : {}
45103
+ }),
45104
+ /* @__PURE__ */ jsx(InterruptedBanner, {
45105
+ engine,
45106
+ ...currentSessionId ? { sessionId: currentSessionId } : {}
45107
+ }),
45108
+ /* @__PURE__ */ jsxs("div", {
45109
+ className: `flex min-h-0 flex-1 flex-col ${composerCentered ? "justify-center" : ""}`,
45110
+ children: [
45111
+ /* @__PURE__ */ jsx(AssistantUiViewport, {
45112
+ grow: !composerCentered,
45113
+ children: /* @__PURE__ */ jsxs("div", {
45114
+ className: "webskill-chat-prose mx-auto flex w-full min-w-0 flex-col gap-4 px-4 py-6",
45115
+ children: [
45116
+ messages.length === 0 && !sending ? /* @__PURE__ */ jsx(AssistantUiEmptyState, {
45117
+ ...config?.title ? { title: config.title } : {},
45118
+ ...staticQuickPrompts ? { prompts: staticQuickPrompts } : {},
45119
+ ...config?.dynamicQuickPrompts ? { dynamicPrompts: config.dynamicQuickPrompts } : {},
45120
+ limit: quickPromptLimit,
45121
+ disabled: sending,
45122
+ onPrompt: handleSend
45123
+ }) : null,
45124
+ /* @__PURE__ */ jsx(MessageDeleteOutlet, {
45125
+ queue: pendingDeletes,
44189
45126
  messages,
44190
- ...messagesCursor !== void 0 ? { earlier: {
44191
- loading: loadingEarlierMessages,
44192
- onLoad: handleLoadEarlierMessages
44193
- } } : {},
44194
- renderMessage: (message) => /* @__PURE__ */ jsx(AssistantUiMessage, {
44195
- message,
44196
- onRetry: handleRetry,
44197
- onDelete: handleDeleteMessage,
44198
- onDownload: handleDownload,
44199
- modelLabels,
44200
- resolveAttachmentUrl,
44201
- ...surfaceRegistry ? { surfaceRegistry } : {},
44202
- ...engine.documentSurface ? { documentSurface: engine.documentSurface } : {},
44203
- renderer,
44204
- bridge: engine.bridge,
44205
- ...adapter.navigation.openConsoleTrace ? { onViewTrace: adapter.navigation.openConsoleTrace } : {},
44206
- ...adapter.navigation.openConsoleCandidate ? { onOpenCandidate: adapter.navigation.openConsoleCandidate } : {}
44207
- })
44208
- })
44209
- }),
44210
- (() => {
44211
- const interactionVisible = interactionSessionId === void 0 || interactionSessionId === currentSessionId;
44212
- return /* @__PURE__ */ jsxs(Fragment$1, { children: [renderer !== "native" ? /* @__PURE__ */ jsx("div", {
44213
- "data-testid": "spec-interaction-slot",
44214
- ...interactionVisible ? { "data-webskill-interaction-anchor": "" } : {},
44215
- className: interactionVisible ? void 0 : "hidden",
44216
- children: /* @__PURE__ */ jsx(SpecInteraction, {
44217
- channel: engine.interactionChannel,
44218
- renderer
45127
+ ...currentSessionId ? { sessionId: currentSessionId } : {},
45128
+ onRemove: (messageId) => setMessages((prev) => prev.filter((m) => m.id !== messageId)),
45129
+ children: (handleDeleteMessage) => /* @__PURE__ */ jsx(AssistantUiMessageList, {
45130
+ messages,
45131
+ ...messagesCursor !== void 0 ? { earlier: {
45132
+ loading: loadingEarlierMessages,
45133
+ onLoad: handleLoadEarlierMessages
45134
+ } } : {},
45135
+ renderMessage: (message) => /* @__PURE__ */ jsx(AssistantUiMessage, {
45136
+ message,
45137
+ onRetry: handleRetry,
45138
+ onDelete: handleDeleteMessage,
45139
+ onDownload: handleDownload,
45140
+ modelLabels,
45141
+ resolveAttachmentUrl,
45142
+ ...surfaceRegistry ? { surfaceRegistry } : {},
45143
+ ...engine.documentSurface ? { documentSurface: engine.documentSurface } : {},
45144
+ renderer,
45145
+ bridge: engine.bridge,
45146
+ ...adapter.navigation.openConsoleTrace ? { onViewTrace: adapter.navigation.openConsoleTrace } : {},
45147
+ ...adapter.navigation.openConsoleCandidate ? { onOpenCandidate: adapter.navigation.openConsoleCandidate } : {}
45148
+ })
44219
45149
  })
44220
- }) : null, interactionVisible ? /* @__PURE__ */ jsx(InteractionCard, { bridge: engine.bridge }) : null] });
44221
- })(),
44222
- /* @__PURE__ */ jsx(ResultBlocksPro, {
44223
- bridge: engine.bridge,
44224
- activeRunId: currentRunId,
44225
- onDownload: handleDownload
44226
- }),
44227
- error ? /* @__PURE__ */ jsx(ErrorCard, {
44228
- message: error.message,
44229
- ...error.code ? { code: error.code } : {},
44230
- loopLimits,
44231
- ...onOpenSettings ? { onOpenSettings: () => onOpenSettings("settings.runtime") } : {},
44232
- onDismiss: () => setError(void 0)
44233
- }) : null
44234
- ]
44235
- })
44236
- }),
44237
- /* @__PURE__ */ jsx(InteractionWaitBar, {
44238
- bridge: engine.bridge,
44239
- ...interactionPending && interactionTarget !== void 0 ? { label: interactionTarget } : {},
44240
- ...interactionElsewhere ? {
44241
- elsewhere: true,
44242
- ...interactionPanel !== void 0 ? { panel: interactionPanel } : {}
44243
- } : {},
44244
- onLocate: handleLocateInteraction
44245
- }),
44246
- (() => {
44247
- const items = visiblePinnedTodos(pinnedTodos);
44248
- return items ? /* @__PURE__ */ jsx("div", {
44249
- "data-testid": "pinned-todo-panel",
44250
- className: "webskill-chat-prose mx-auto w-full shrink-0 px-3",
44251
- children: /* @__PURE__ */ jsx(TodoPanel, {
44252
- items,
44253
- scrollable: true,
44254
- onDismiss: () => setPinnedTodos(dismissPinnedTodos)
45150
+ }),
45151
+ (() => {
45152
+ const interactionVisible = interactionSessionId === void 0 || interactionSessionId === currentSessionId;
45153
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [renderer !== "native" ? /* @__PURE__ */ jsx("div", {
45154
+ "data-testid": "spec-interaction-slot",
45155
+ ...interactionVisible ? { "data-webskill-interaction-anchor": "" } : {},
45156
+ className: interactionVisible ? void 0 : "hidden",
45157
+ children: /* @__PURE__ */ jsx(SpecInteraction, {
45158
+ channel: engine.interactionChannel,
45159
+ renderer
45160
+ })
45161
+ }) : null, interactionVisible ? /* @__PURE__ */ jsx(InteractionCard, { bridge: engine.bridge }) : null] });
45162
+ })(),
45163
+ /* @__PURE__ */ jsx(ResultBlocksPro, {
45164
+ bridge: engine.bridge,
45165
+ activeRunId: currentRunId,
45166
+ onDownload: handleDownload
45167
+ }),
45168
+ error ? /* @__PURE__ */ jsx(ErrorCard, {
45169
+ message: error.message,
45170
+ ...error.code ? { code: error.code } : {},
45171
+ loopLimits,
45172
+ ...onOpenSettings ? { onOpenSettings: () => onOpenSettings("settings.runtime") } : {},
45173
+ onDismiss: () => setError(void 0)
45174
+ }) : null
45175
+ ]
44255
45176
  })
44256
- }) : null;
44257
- })(),
44258
- (() => {
44259
- const window = selectedEntry?.contextWindow;
44260
- const usage = formatContextUsage(contextTokens, window);
44261
- if (!queuedSend && usage === void 0) return null;
44262
- return /* @__PURE__ */ jsxs("div", {
44263
- className: "webskill-chat-prose text-muted-foreground mx-auto flex w-full shrink-0 items-center justify-between gap-2 px-3 text-xs",
44264
- children: [queuedSend ? /* @__PURE__ */ jsx("span", {
44265
- "data-testid": "queued-send-hint",
44266
- children: locale === "zh" ? "前一条还在处理,这条排在后面" : "Waiting for the previous message to finish"
44267
- }) : /* @__PURE__ */ jsx("span", {}), usage !== void 0 ? /* @__PURE__ */ jsx("span", {
44268
- "data-testid": "context-usage",
44269
- children: locale === "zh" ? `上下文 ${usage}` : `Context ${usage}`
44270
- }) : null]
44271
- });
44272
- })(),
44273
- /* @__PURE__ */ jsx(AssistantUiComposer, {
44274
- ...modelCapabilities ? { capabilities: {
44275
- tools: modelCapabilities.tools,
44276
- image: modelCapabilities.image
44277
- } } : {},
44278
- running: busy,
44279
- disabled: interactionPending,
44280
- ...interactionTarget !== void 0 ? { waitTarget: interactionTarget } : {},
44281
- centered: composerCentered,
44282
- attachments: attachmentsReady,
44283
- attachmentInfo: (id) => attachmentAdapter.describe(id),
44284
- attachmentImages: {
44285
- enabled: multimodal.imageAttachments,
44286
- imageCapable: modelTakesImages
44287
- },
44288
- ...dictationLang !== "" ? { dictationLang } : {},
44289
- ...onDictationPermissionDenied ? { onDictationPermissionDenied } : {},
44290
- ...onCameraPermissionDenied ? { onCameraPermissionDenied } : {},
44291
- ...composerModels && !noUsableModel ? { models: composerModels } : {},
44292
- ...noUsableModel ? { noModel: { onConfigure: () => setNoModelGuideOpen(true) } } : {},
44293
- ...config?.placeholder ? { placeholder: config.placeholder } : {}
44294
- }),
44295
- /* @__PURE__ */ jsx(NoModelGuideDialog, {
44296
- open: noModelGuideOpen,
44297
- onClose: () => setNoModelGuideOpen(false),
44298
- ...onOpenSettings ? { onConfigure: () => {
44299
- setNoModelGuideOpen(false);
44300
- onOpenSettings("connections.models");
44301
- } } : {}
44302
- })
44303
- ]
44304
- })
44305
- ]
44306
- })] })
45177
+ }),
45178
+ /* @__PURE__ */ jsx(InteractionWaitBar, {
45179
+ bridge: engine.bridge,
45180
+ ...interactionPending && interactionTarget !== void 0 ? { label: interactionTarget } : {},
45181
+ ...interactionElsewhere ? {
45182
+ elsewhere: true,
45183
+ ...interactionPanel !== void 0 ? { panel: interactionPanel } : {}
45184
+ } : {},
45185
+ onLocate: handleLocateInteraction
45186
+ }),
45187
+ (() => {
45188
+ const items = visiblePinnedTodos(pinnedTodos);
45189
+ return items ? /* @__PURE__ */ jsx("div", {
45190
+ "data-testid": "pinned-todo-panel",
45191
+ className: "webskill-chat-prose mx-auto w-full shrink-0 px-3",
45192
+ children: /* @__PURE__ */ jsx(TodoPanel, {
45193
+ items,
45194
+ scrollable: true,
45195
+ onDismiss: () => setPinnedTodos(dismissPinnedTodos)
45196
+ })
45197
+ }) : null;
45198
+ })(),
45199
+ (() => {
45200
+ const window = selectedEntry?.contextWindow;
45201
+ const usage = formatContextUsage(contextTokens, window);
45202
+ if (!queuedSend && usage === void 0) return null;
45203
+ return /* @__PURE__ */ jsxs("div", {
45204
+ className: "webskill-chat-prose text-muted-foreground mx-auto flex w-full shrink-0 items-center justify-between gap-2 px-3 text-xs",
45205
+ children: [queuedSend ? /* @__PURE__ */ jsx("span", {
45206
+ "data-testid": "queued-send-hint",
45207
+ children: locale === "zh" ? "前一条还在处理,这条排在后面" : "Waiting for the previous message to finish"
45208
+ }) : /* @__PURE__ */ jsx("span", {}), usage !== void 0 ? /* @__PURE__ */ jsx("span", {
45209
+ "data-testid": "context-usage",
45210
+ children: locale === "zh" ? `上下文 ${usage}` : `Context ${usage}`
45211
+ }) : null]
45212
+ });
45213
+ })(),
45214
+ /* @__PURE__ */ jsx(AssistantUiComposer, {
45215
+ ...modelCapabilities ? { capabilities: {
45216
+ tools: modelCapabilities.tools,
45217
+ image: modelCapabilities.image
45218
+ } } : {},
45219
+ running: busy,
45220
+ disabled: interactionPending,
45221
+ ...interactionTarget !== void 0 ? { waitTarget: interactionTarget } : {},
45222
+ centered: composerCentered,
45223
+ attachments: attachmentsReady,
45224
+ attachmentInfo: (id) => attachmentAdapter.describe(id),
45225
+ attachmentImages: {
45226
+ enabled: multimodal.imageAttachments,
45227
+ imageCapable: modelTakesImages
45228
+ },
45229
+ ...dictationLang !== "" ? { dictationLang } : {},
45230
+ ...onDictationPermissionDenied ? { onDictationPermissionDenied } : {},
45231
+ ...onCameraPermissionDenied ? { onCameraPermissionDenied } : {},
45232
+ ...composerModels && !noUsableModel ? { models: composerModels } : {},
45233
+ ...noUsableModel ? { noModel: { onConfigure: () => setNoModelGuideOpen(true) } } : {},
45234
+ ...config?.placeholder ? { placeholder: config.placeholder } : {}
45235
+ }),
45236
+ /* @__PURE__ */ jsx(NoModelGuideDialog, {
45237
+ open: noModelGuideOpen,
45238
+ onClose: () => setNoModelGuideOpen(false),
45239
+ ...onOpenSettings ? { onConfigure: () => {
45240
+ setNoModelGuideOpen(false);
45241
+ onOpenSettings("connections.models");
45242
+ } } : {}
45243
+ })
45244
+ ]
45245
+ })
45246
+ ]
45247
+ })] })
45248
+ })
44307
45249
  })
44308
45250
  }) })
44309
45251
  });
@@ -44364,7 +45306,7 @@ function VercelPayloadPreview({ bridge }) {
44364
45306
  * Version of the published `@webskill/chatbot` package, injected at build time.
44365
45307
  * @stable
44366
45308
  */
44367
- const CHATBOT_VERSION = "0.18.0";
45309
+ const CHATBOT_VERSION = "0.19.0";
44368
45310
 
44369
45311
  //#endregion
44370
- export { A2uiSurfaceHost, A2uiSurfaceSnapshotHost, CHATBOT_VERSION, ChatEngine, Chatbot, ChatbotVisionDelegate, CompositeUiBridge, DEFAULT_RENDERER_CAPABILITIES, DEFAULT_RUNTIME_CONFIG_STORAGE_KEY, ErrorCard, InteractionCard, InterruptedBanner, OpenUiSurfaceHost, OpenUiSurfaceSnapshotHost, ResultBlockList, ResultBlocksPro, SkillBadges, SpecInteraction, VercelPayloadPreview, VercelSurfaceHost, VercelSurfaceSnapshotHost, chatbotDictionary, configureA2uiMarkdown, createLocalStorageRuntimeConfigStore, createMemoryRuntimeConfigStore, isLlmEntryUsable, pickUsableLlmEntry, probeA2uiAvailability, probeOpenUiAvailability, sandboxExecutorDeps, useT };
45312
+ export { A2uiSurfaceHost, A2uiSurfaceSnapshotHost, CHATBOT_VERSION, ChatEngine, Chatbot, ChatbotVisionDelegate, CompositeUiBridge, DEFAULT_RENDERER_CAPABILITIES, DEFAULT_RUNTIME_CONFIG_STORAGE_KEY, ErrorCard, InteractionCard, InterruptedBanner, OpenUiSurfaceHost, OpenUiSurfaceSnapshotHost, ResultBlockList, ResultBlocksPro, SkillBadges, SpecInteraction, VercelPayloadPreview, VercelSurfaceHost, VercelSurfaceSnapshotHost, WEB_OFFICE_TEMPLATE_PREFIX, chatbotDictionary, configureA2uiMarkdown, createLocalStorageRuntimeConfigStore, createMemoryRuntimeConfigStore, isLlmEntryUsable, pickUsableLlmEntry, probeA2uiAvailability, probeOpenUiAvailability, sandboxExecutorDeps, useT };