@webskill/chatbot 0.18.0 → 0.20.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, SkillReader, 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,20 @@ 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.delegation.title": "Run parts of this task in parallel?",
26516
+ "interaction.delegation.body": "Sub-agents can work on independent parts of this task at the same time. What they do is not recorded in this conversation, so this conversation can no longer be saved as a new skill.",
26517
+ "interaction.delegation.declineNote": "If you decline, the assistant does the work itself in this conversation and will not ask again.",
26518
+ "interaction.delegation.concurrency": "Run at most",
26519
+ "interaction.delegation.suggestedNote": "The assistant suggested {count}.",
26520
+ "interaction.imageSource.title": "Put a picture into the document?",
26521
+ "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.",
26522
+ "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.",
26523
+ "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.",
26524
+ "interaction.imageSource.insecure": "This address is plain http://, so the image can be read or swapped by anyone on the network in between.",
26525
+ "interaction.imageSource.note": "The assistant says it is for: {note}",
26526
+ "interaction.imageSource.rememberFallback": "Don’t ask again in this conversation",
26527
+ "interaction.imageSource.rememberPageImage": "Don’t ask again in this conversation before taking pictures from the page",
26528
+ "interaction.imageSource.rememberRemoteImage": "Don’t ask again in this conversation before downloading images from the web",
26122
26529
  "interaction.uploadFile.read": "Allow the skill “{skill}” to read the file you uploaded?",
26123
26530
  "interaction.uploadFile.file": "{name} · {type} · {size}",
26124
26531
  "interaction.sandbox.readReference": "Allow the skill “{skill}” to read the reference file “{path}”?",
@@ -26184,6 +26591,7 @@ const chatbotDictionary = defineDictionary({
26184
26591
  "composer.attachment.kind.image": "Image",
26185
26592
  "composer.attachment.kind.file": "File",
26186
26593
  "composer.attachment.kind.document-text": "Doc text",
26594
+ "composer.attachment.kind.binary": "Drawing",
26187
26595
  "composer.attachment.compressed": "Compressed {from} → {to}",
26188
26596
  "composer.model": "Model",
26189
26597
  "composer.model.unconfigured": "Not usable: this entry has no API key or base URL. Configure it in the console.",
@@ -26198,6 +26606,7 @@ const chatbotDictionary = defineDictionary({
26198
26606
  "attachment.storeFailed.title": "Not enough space on this device",
26199
26607
  "attachment.storeFailed": "“{name}” could not be saved to this device’s storage, so it was not attached. Free up space and try again.",
26200
26608
  "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.",
26609
+ "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
26610
  "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
26611
  "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
26612
  "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 +26911,8 @@ const chatbotDictionary = defineDictionary({
26502
26911
  "interaction.pageAction.attach": "允许助手向「{target}」附加文件吗?",
26503
26912
  "interaction.pageAction.scroll.down": "允许助手在「{target}」所在区域向下滚动吗?",
26504
26913
  "interaction.pageAction.scroll.up": "允许助手在「{target}」所在区域向上滚动吗?",
26914
+ "interaction.pageAction.hover": "允许助手将鼠标悬停在「{target}」上吗?",
26915
+ "interaction.pageAction.drag": "允许助手将「{target}」拖到另一个位置吗?",
26505
26916
  "interaction.pageAction.back": "允许助手在「{target}」上返回上一页吗?",
26506
26917
  "interaction.pageAction.role.click": "这个控件",
26507
26918
  "interaction.pageAction.role.fill": "这个输入框",
@@ -26510,6 +26921,8 @@ const chatbotDictionary = defineDictionary({
26510
26921
  "interaction.pageAction.role.set": "这个开关",
26511
26922
  "interaction.pageAction.role.attach": "这个上传控件",
26512
26923
  "interaction.pageAction.role.scroll": "这一块",
26924
+ "interaction.pageAction.role.hover": "这个元素",
26925
+ "interaction.pageAction.role.drag": "这一项",
26513
26926
  "interaction.pageAction.role.back": "这个页面",
26514
26927
  "interaction.pageAction.remember": "以后不再询问此类操作",
26515
26928
  "interaction.pageAction.frame": "目标位于嵌入帧「{frame}」中。",
@@ -26533,6 +26946,20 @@ const chatbotDictionary = defineDictionary({
26533
26946
  "interaction.visionDelegation.rememberFallback": "以后不再为该端点询问",
26534
26947
  "interaction.visionDelegation.rememberSession": "本次对话不再询问,默认同意",
26535
26948
  "interaction.visionDelegation.body": "你选中的模型看不了图片。是否把「{source}」产出的 {images} 张图发往「{label}」({host})请它描述?不同意则什么都不会发出,这一轮就当没看到图。",
26949
+ "interaction.delegation.title": "把这件任务拆成几个并行去做?",
26950
+ "interaction.delegation.body": "子助手可以同时处理这件任务里互不相干的部分。它们做过什么不会记进本次会话,因此这次会话之后就存不成新技能了。",
26951
+ "interaction.delegation.declineNote": "不同意则助手自己在本次会话里做完,之后也不会再问。",
26952
+ "interaction.delegation.concurrency": "最多同时跑",
26953
+ "interaction.delegation.suggestedNote": "助手建议 {count} 个。",
26954
+ "interaction.imageSource.title": "把图片放进文档?",
26955
+ "interaction.imageSource.pageImageBody": "允许助手把当前页面上的一张图放进它正在生成的文档吗?该图会随本次会话保存,直到你删除这次会话。",
26956
+ "interaction.imageSource.remoteImageBody": "允许助手从 {url} 下载一张图片放进它正在生成的文档吗?下载会把你的网络地址与访问时间告知该服务器。",
26957
+ "interaction.imageSource.remoteImageAnyBody": "允许助手从互联网下载图片放进它正在生成的文档吗?下载会把你的网络地址与访问时间告知对方服务器。",
26958
+ "interaction.imageSource.insecure": "这个地址是明文 http://,图片内容可被链路上的任何人看到或替换。",
26959
+ "interaction.imageSource.note": "助手声称的用途:{note}",
26960
+ "interaction.imageSource.rememberFallback": "本次对话不再询问",
26961
+ "interaction.imageSource.rememberPageImage": "本次对话不再询问取用页面上的图",
26962
+ "interaction.imageSource.rememberRemoteImage": "本次对话不再询问从互联网下载图片",
26536
26963
  "interaction.uploadFile.read": "允许技能「{skill}」读取你上传的这份文件吗?",
26537
26964
  "interaction.uploadFile.file": "{name} · {type} · {size}",
26538
26965
  "interaction.sandbox.readReference": "允许技能「{skill}」读取参考资料「{path}」吗?",
@@ -26598,6 +27025,7 @@ const chatbotDictionary = defineDictionary({
26598
27025
  "composer.attachment.kind.image": "图片",
26599
27026
  "composer.attachment.kind.file": "文件",
26600
27027
  "composer.attachment.kind.document-text": "文档正文",
27028
+ "composer.attachment.kind.binary": "图纸",
26601
27029
  "composer.attachment.compressed": "已压缩 {from} → {to}",
26602
27030
  "composer.model": "模型",
26603
27031
  "composer.model.unconfigured": "不可用:该条目既没有 API 密钥也没有基础地址,请在控制台补全。",
@@ -26612,6 +27040,7 @@ const chatbotDictionary = defineDictionary({
26612
27040
  "attachment.storeFailed.title": "本机存储不够了",
26613
27041
  "attachment.storeFailed": "「{name}」没能存入本机存储,因此未作为附件添加。请腾出空间后重试。",
26614
27042
  "attachment.pdfAsText": "「{name}」有 {size},超过单次请求能带的 {limit},已改为发送从中抽取的文本,而不是原件。版面、图表与扫描页不在其中。",
27043
+ "attachment.pdfAsPagedRead": "「{name}」有 {size},超过单次请求能带的 {limit},因此未作为附件发送。助手改为逐页读取,长文档会读好几轮。",
26615
27044
  "attachment.pdfRefused": "「{name}」有 {size},超过单次请求能带的 {limit},而且从中抽不出文本——它多半是扫描件。该文件未发送。请改用更小的文件、指定页码范围,或换文本版本。",
26616
27045
  "attachment.pdfNoExtractor": "「{name}」有 {size},超过单次请求能带的 {limit},而本环境无法抽取 PDF 文本。该文件未发送。请换一个能收大文档的模型,或改传 Word / 纯文本版本。",
26617
27046
  "attachment.textTooLong": "「{name}」的文本有 {size},超过单次请求能放进模型上下文的 {limit}。该文件未发送,也没有截断——半份文档比没有更糟。请在「设置 → 智能体运行」里调高抽取文本上限,或改附一段节选。",
@@ -26785,6 +27214,24 @@ function translateText(locale, key, params) {
26785
27214
  return interpolate$1(chatbotDictionary[locale]?.[key] ?? chatbotDictionary.en[key] ?? key, params);
26786
27215
  }
26787
27216
 
27217
+ //#endregion
27218
+ //#region src/core/attachmentRef.ts
27219
+ /**
27220
+ * `upload:` 引用落到哪条附件上。
27221
+ *
27222
+ * 引擎侧(投放文档里的图片块)与组件侧(正文 markdown 里的 `![](upload:…)`)读的是
27223
+ * 两份不同的数据——一份从会话存储读回,一份是 React 手上的消息数组——但**判定必须是同一条**,
27224
+ * 否则会出现「文档里贴得出、对话里贴不出」这种没法向用户解释的差异。
27225
+ *
27226
+ * id 先行:它唯一。文件名兜底是必需的而不是宽容——附件 id 是 `att-…` 随机串,
27227
+ * 模型上下文里最显眼的始终是文件名,只认 id 的结果就是整篇文档一张图都没有。
27228
+ * 重名时取最先那条:这一路的候选全部来自用户自己的消息,没有越权面,
27229
+ * 取错一张也远好过整批丢图。
27230
+ */
27231
+ function findAttachmentByRef(attachments, ref) {
27232
+ return attachments.find((a) => a.id === ref) ?? attachments.find((a) => a.name === ref);
27233
+ }
27234
+
26788
27235
  //#endregion
26789
27236
  //#region src/core/runPhases.ts
26790
27237
  /** 展示用相位:discover 并进 route,prepare/observe 并进 execute(07 号文档 §4) */
@@ -26940,6 +27387,24 @@ function redactInteractionValues(value, request) {
26940
27387
  */
26941
27388
  const CHATBOT_PROFILE_USER_ID = "local";
26942
27389
  /**
27390
+ * 并发子 agent 的宿主硬上限(0.23.0 分册 19 FR-19.16)。
27391
+ *
27392
+ * 它是模型建议值与用户选定值之上的天花板:模型建议 50、用户随手确认了,
27393
+ * 仍然只有这么多能同时跑。并发子 agent 会同时抢模型配额、页面操作通道与用户注意力,
27394
+ * 这个数字不是保守,是止损点。
27395
+ */
27396
+ const DELEGATION_MAX_CONCURRENCY = 4;
27397
+ /**
27398
+ * 从确认卡的响应里读用户选定的并发数。
27399
+ *
27400
+ * 卡回的是 `{ concurrency: n }`(渲染层带了选择器)或 `true`(宿主用的是通用授权渲染,
27401
+ * 没有选择器)。后一种情况沿用建议值——用户点了同意,但没有表达过别的数字。
27402
+ */
27403
+ function readChosenConcurrency(value, fallback, max) {
27404
+ const raw = typeof value === "object" && value !== null ? value["concurrency"] : void 0;
27405
+ return Math.min(Math.max(1, typeof raw === "number" && Number.isFinite(raw) ? Math.floor(raw) : fallback), Math.max(1, max));
27406
+ }
27407
+ /**
26943
27408
  * 终止原因 → 稳定错误码(FR-21.2)。不再全部压平为 RUN_FAILED:
26944
27409
  * “跑超时了”与“模型没配对”对用户是两件事,得给不同的下一步。
26945
27410
  */
@@ -26987,6 +27452,45 @@ function fromBase64(data) {
26987
27452
  function formatSize(bytes) {
26988
27453
  return bytes >= 1024 * 1024 ? `${(bytes / 1024 / 1024).toFixed(1)} MB` : `${Math.max(1, Math.round(bytes / 1024))} KB`;
26989
27454
  }
27455
+ /** 技能写在图片块上的引用,由 publish 脚本渲染成这个属性;字节在这里填回去 */
27456
+ const IMAGE_REF_ATTR = "data-webskill-image";
27457
+ /**
27458
+ * 单张图进文档的字节上限。超了先压(`compressImageToBudget`),压不下去按 FR-11.8 归因丢弃,
27459
+ * 不许原样塞进载荷去撞分册 10 的硬兜底——那会让一张图带走整份文档。
27460
+ */
27461
+ const MAX_DOCUMENT_IMAGE_BYTES = 8e6;
27462
+ /**
27463
+ * 在线 WebOffice 文档作模板时的取件号前缀(0.22.0 分册 42 · FR-42.12)。
27464
+ *
27465
+ * 前缀是显式的,不靠猜:`inspect_template` 的取件号绝大多数是附件 id,
27466
+ * 拿一个普通 id 去在线文档那边碰运气,猜中一次的收益远小于猜错一次的代价——
27467
+ * 那是把另一份文档当成用户指的那一份填了出去。
27468
+ * @experimental
27469
+ */
27470
+ const WEB_OFFICE_TEMPLATE_PREFIX = "weboffice:";
27471
+ /**
27472
+ * 技能自带文件作模板时的取件号前缀(0.23.0 分册 23 · FR-23.2)。
27473
+ *
27474
+ * 形态是 `skill:<技能名>/<技能目录内的相对路径>`。技能名的字符集
27475
+ * (`SKILL_NAME_PATTERN`)既不含 `/` 也不含 `:`,所以前缀之后的第一个 `/`
27476
+ * 就是唯一的分隔点,不需要第二个分隔符。
27477
+ *
27478
+ * 与 `WEB_OFFICE_TEMPLATE_PREFIX` 不同,这个常量**不进公开导出**:宿主不需要拼它,
27479
+ * 拼它的是模型,而模型看到的是工具描述里那句话(设计 24 §10 的零契约变更)。
27480
+ */
27481
+ const SKILL_FILE_PREFIX = "skill:";
27482
+ /** 模板取件号里所有"指名道姓"的前缀:认出来就只走它自己那条通路,不回落到本地取件 */
27483
+ const PREFIXED_TEMPLATE_IDS = [SKILL_FILE_PREFIX, WEB_OFFICE_TEMPLATE_PREFIX];
27484
+ /** `skill:quotation/assets/tpl.xlsx` → `{ skill: 'quotation', path: 'assets/tpl.xlsx' }` */
27485
+ function parseSkillFileId(id) {
27486
+ const rest = id.slice(6);
27487
+ const cut = rest.indexOf("/");
27488
+ if (cut <= 0 || cut === rest.length - 1) return void 0;
27489
+ return {
27490
+ skill: rest.slice(0, cut),
27491
+ path: rest.slice(cut + 1)
27492
+ };
27493
+ }
26990
27494
  /**
26991
27495
  * 图片附件的指认说明(0.21.0 分册 22 · FR-22.4b)。
26992
27496
  *
@@ -26998,7 +27502,8 @@ function formatSize(bytes) {
26998
27502
  * 与图分开成片:端点拒收图时 `#dropRejectedImages` 换掉的只有图那一片,说明留在原地。
26999
27503
  */
27000
27504
  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.`;
27505
+ 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.
27506
+ 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
27507
  }
27003
27508
  /** 内置演示 LLM 的应答序列(用尽后从头循环) */
27004
27509
  const DEMO_REPLIES = [
@@ -27135,6 +27640,8 @@ var ChatEngine = class {
27135
27640
  #ready;
27136
27641
  /** 已装配的 runtime(cancel 等同步委托用;未装配时为 undefined) */
27137
27642
  #runtime;
27643
+ /** 技能名 → 技能根目录,`skill:` 取件号用(0.23.0 分册 23)。惰性建,技能仓变更时清 */
27644
+ #skillIndex;
27138
27645
  /**
27139
27646
  * 每轮 `send()` 开头刷新的运行时配置快照(0.15.0 分册 15 · FR-15.6)。
27140
27647
  * 只给那些必须同步回答、又不能等到重建运行时的开关用。
@@ -27153,6 +27660,11 @@ var ChatEngine = class {
27153
27660
  #listeners = /* @__PURE__ */ new Set();
27154
27661
  /** runId → 已激活技能累积(skill-activated 事件用;终态清理) */
27155
27662
  #runSkills = /* @__PURE__ */ new Map();
27663
+ /**
27664
+ * runId → sessionId。trace 要等 run 结束才落盘,而导出工具在 run **进行中**
27665
+ * 就要按会话找附件与历史产物,只读 trace 时它们一律扑空(分册 21 §2.6)。
27666
+ */
27667
+ #runSessions = /* @__PURE__ */ new Map();
27156
27668
  /** 文档投放端口只构造一次:渲染侧把它放进 memo 依赖,每次取新对象会触发无谓重渲染 */
27157
27669
  #documentSurfacePort;
27158
27670
  /** 当前渲染框架档(复合 uiBridge 分发;setRenderer 热切换) */
@@ -27181,6 +27693,15 @@ var ChatEngine = class {
27181
27693
  * 拒绝的记忆挂在它身上,因此 `createSession()` 要能找到它把那一位清掉。
27182
27694
  */
27183
27695
  #visionDelegate;
27696
+ /**
27697
+ * 图片来源授权(0.22.0 分册 11 §3.3)。记忆随会话生灭,因此与视觉委派一样,
27698
+ * 切换 / 新建会话时要把那一份集合清掉。
27699
+ */
27700
+ #imageSources;
27701
+ /** run → 已落下的页面图张数;产物名 `page-image-N.png` 的 N 从这里来 */
27702
+ #pageImageSeq = /* @__PURE__ */ new Map();
27703
+ /** run → 已落下的外链图张数;与页面图分开编号,产物名一眼看得出来源 */
27704
+ #remoteImageSeq = /* @__PURE__ */ new Map();
27184
27705
  /** 当前 run('route' 事件捕获,终态清理);清单与页面感知的事件归属共用 */
27185
27706
  #activeRunId;
27186
27707
  /** dispose() 之后拒绝再发起 run:卸载后的组件不该还能悄悄跑一轮 */
@@ -27386,17 +27907,265 @@ var ChatEngine = class {
27386
27907
  const dir = `${this.#chatRoot}/artifacts/${runId}`;
27387
27908
  const html = await this.#adapter.storage.readText(this.#artifactPath(dir, artifact));
27388
27909
  const css = style === void 0 ? void 0 : await this.#adapter.storage.readText(this.#artifactPath(dir, style));
27910
+ const resolved = await this.#resolveDocumentImages(html, runId);
27389
27911
  await host.open({
27390
27912
  skillName: await this.#skillNamesOfRun(runId),
27391
27913
  dataSource: sanitizeUntrustedLine(dataSource),
27392
27914
  document: {
27393
- html,
27915
+ html: resolved.html,
27394
27916
  ...css === void 0 ? {} : { css }
27395
- }
27917
+ },
27918
+ ...resolved.images.length > 0 ? { images: resolved.images } : {},
27919
+ ...resolved.omissions.length > 0 ? { omissions: resolved.omissions } : {}
27396
27920
  });
27397
27921
  } };
27398
27922
  }
27399
27923
  /**
27924
+ * 把产物 HTML 里的图片占位换成可直接显示的 `src`(0.22.0 FR-11.6)。
27925
+ *
27926
+ * 落在引擎而不是宿主或投放面,理由与 `documentSurface` 端口本身相同:要读产物就得知道
27927
+ * 产物目录布局,那是引擎独有的知识。`startViewerShell` 因此一行不改。
27928
+ *
27929
+ * 解析发生在宿主的确认框之前,用户取消时白做一次——接受这个代价,因为**体积与像素的
27930
+ * 度量必须看到已填字节的文档**,否则量到的是一份全是占位的空壳。
27931
+ */
27932
+ async #resolveDocumentImages(html, runId) {
27933
+ if (!html.includes(IMAGE_REF_ATTR)) return {
27934
+ html,
27935
+ images: [],
27936
+ omissions: []
27937
+ };
27938
+ const doc = new DOMParser().parseFromString(html, "text/html");
27939
+ const images = [];
27940
+ const omissions = [];
27941
+ for (const element of [...doc.querySelectorAll(`img[${IMAGE_REF_ATTR}]`)]) {
27942
+ const ref = element.getAttribute(IMAGE_REF_ATTR) ?? "";
27943
+ const alt = element.getAttribute("alt") ?? "";
27944
+ element.removeAttribute(IMAGE_REF_ATTR);
27945
+ try {
27946
+ const src = await this.#documentImageSrc(ref, runId, images);
27947
+ element.setAttribute("src", src);
27948
+ } catch (e) {
27949
+ const reason = messageOf(e);
27950
+ omissions.push({
27951
+ ref,
27952
+ alt,
27953
+ stage: "resolve",
27954
+ reason
27955
+ });
27956
+ const note = doc.createElement("p");
27957
+ note.className = "wsdoc__image-missing";
27958
+ note.textContent = `[image unavailable] ${alt} — ${reason}`;
27959
+ element.replaceWith(note);
27960
+ }
27961
+ }
27962
+ return {
27963
+ html: doc.body.innerHTML,
27964
+ images,
27965
+ omissions
27966
+ };
27967
+ }
27968
+ /** 解析一条引用并把实测事实记进 `images`;失败一律抛错,由调用方转成归因 */
27969
+ async #documentImageSrc(ref, runId, images) {
27970
+ const separator = ref.indexOf(":");
27971
+ const kind = separator === -1 ? "" : ref.slice(0, separator);
27972
+ const rest = ref.slice(separator + 1);
27973
+ if (kind === "remote") {
27974
+ assertRemoteUrlAllowed(rest, { allowHttp: true });
27975
+ return rest;
27976
+ }
27977
+ const image = await toDocumentImage(await this.#documentImageBytes(kind, rest, runId), { maxBytes: MAX_DOCUMENT_IMAGE_BYTES });
27978
+ const src = `data:${image.mimeType};base64,${image.data}`;
27979
+ images.push({
27980
+ ref,
27981
+ width: image.width,
27982
+ height: image.height,
27983
+ units: src.length
27984
+ });
27985
+ return src;
27986
+ }
27987
+ async #documentImageBytes(kind, rest, runId) {
27988
+ if (kind === "artifact") {
27989
+ assertSafePathSegment(rest, "artifact");
27990
+ const read = (owner) => this.#adapter.storage.readBinary(resolveInsideRoot(`${this.#chatRoot}/artifacts/${owner}`, rest));
27991
+ try {
27992
+ return await read(runId);
27993
+ } catch {}
27994
+ for (const prior of await this.#priorRunIds(runId)) try {
27995
+ return await read(prior);
27996
+ } catch {}
27997
+ throw new WebSkillError("FS_NOT_FOUND", `artifact "${rest}" was not produced anywhere in this conversation`);
27998
+ }
27999
+ if (kind === "upload") {
28000
+ const attachment = await this.#uploadOfRun(runId, rest);
28001
+ 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`);
28002
+ return this.#adapter.storage.readBinary(resolveInsideRoot(this.attachmentsRoot, attachment.path));
28003
+ }
28004
+ throw new WebSkillError("VALIDATION_FAILED", `image reference "${kind}:${rest}" has no known prefix; expected artifact:, upload: or remote:`);
28005
+ }
28006
+ /**
28007
+ * 技能生成里被指认的那个附件(0.23.0 分册 20 FR-20.2)。
28008
+ *
28009
+ * **只在当前会话的消息里查**:候选集合恰好等于用户自己这次会话上传的东西,
28010
+ * 别的会话的 id 在这里查不到,也就谈不上被复制进技能包(AC-20.4)。
28011
+ * 路径一寸都不来自模型——它给的是 id,路径从附件记录里取,与 `#uploadOfRun` 同一范式。
28012
+ *
28013
+ * 落到哪条附件上仍走 `findAttachmentByRef`:模型上下文里最显眼的是文件名而不是
28014
+ * `att-…` 随机串,只认 id 的结果就是这条能力形同虚设。
28015
+ */
28016
+ async #skillAttachment(id) {
28017
+ const sessionId = this.#currentSessionId;
28018
+ if (sessionId === void 0) return void 0;
28019
+ const messages = (await this.#sessions.get(sessionId).catch(() => void 0))?.messages ?? [];
28020
+ for (let i = messages.length - 1; i >= 0; i -= 1) {
28021
+ const message = messages[i];
28022
+ if (message?.role !== "user") continue;
28023
+ const hit = findAttachmentByRef(message.attachments ?? [], id);
28024
+ if (hit === void 0) continue;
28025
+ return {
28026
+ bytes: await this.#adapter.storage.readBinary(resolveInsideRoot(this.attachmentsRoot, hit.path)),
28027
+ name: hit.name,
28028
+ contentType: hit.contentType
28029
+ };
28030
+ }
28031
+ }
28032
+ /**
28033
+ * `skill:<技能名>/<路径>` 的取件(0.23.0 分册 23 FR-23.1)。
28034
+ *
28035
+ * 这条通路**不经过** `DownloadedFilePolicy`,所以既不弹授权卡也不列下载目录——
28036
+ * 是结构性的,不是"记得别弹"(FR-23.8)。路径越界由 `SkillReader` 内建的
28037
+ * `resolveInsideRoot` 挡(FR-23.5)。
28038
+ *
28039
+ * 取不到一律**抛**而不是返回 `undefined`:模板工具对 `undefined` 会回一句
28040
+ * 「换个下载 id 再试」,那正是本册要根除的那句话(FR-23.9)。
28041
+ */
28042
+ async #readSkillTemplate(id, fs) {
28043
+ const parsed = parseSkillFileId(id);
28044
+ if (!parsed) throw new WebSkillError("TOOL_UNSUPPORTED", `Malformed skill file id "${id}". Use "${SKILL_FILE_PREFIX}<skill name>/<path inside the skill>", for example "${SKILL_FILE_PREFIX}quotation/assets/template.xlsx".`);
28045
+ const { skill, path } = parsed;
28046
+ let index = await this.#skillFileIndex(fs);
28047
+ if (!index.has(skill)) {
28048
+ this.#skillIndex = void 0;
28049
+ index = await this.#skillFileIndex(fs);
28050
+ }
28051
+ if (!index.has(skill)) throw new WebSkillError("SKILL_NOT_FOUND", `No installed skill is named "${skill}". Skill file ids look like "${SKILL_FILE_PREFIX}<skill name>/<path>".`);
28052
+ const canRead = this.#options.governance?.skillStateGuard?.canRead;
28053
+ if (canRead && await canRead(skill) === false) throw new WebSkillError("SKILL_DISABLED", `The skill "${skill}" is currently disabled or quarantined, so its files cannot be read. Tell the user instead of trying another path.`);
28054
+ try {
28055
+ const bytes = await new SkillReader(fs, index).readSkillBinary(skill, path);
28056
+ return {
28057
+ name: path.split("/").pop() ?? path,
28058
+ bytes
28059
+ };
28060
+ } catch (error) {
28061
+ throw new WebSkillError(error instanceof WebSkillError ? error.code : "TOOL_EXECUTION_FAILED", `The skill "${skill}" has no readable file at "${path}": ${messageOf(error)}`);
28062
+ }
28063
+ }
28064
+ /** 技能名 → 技能根目录。惰性建:不用这种取件号的会话一次目录 IO 都不多花 */
28065
+ async #skillFileIndex(fs) {
28066
+ this.#skillIndex ??= (async () => {
28067
+ const result = await new SkillDiscovery(fs, this.#adapter.skillRoots).discover();
28068
+ return new Map(result.entries.map((entry) => [entry.name, entry.root]));
28069
+ })();
28070
+ return await this.#skillIndex;
28071
+ }
28072
+ /**
28073
+ * 技能根目录下有没有非 Markdown 的资源文件(0.23.0 分册 23 FR-23.3 / AC-23.8)。
28074
+ *
28075
+ * 一个都没有时工具描述里不提 `skill:` 取件号——凭空多一种读不到东西的说法,
28076
+ * 只会让模型多烧一轮。只列目录、不读也不解析 SKILL.md:这里要的是一个是/否。
28077
+ *
28078
+ * **漏判的方向是安全的**:资源放在 assets/references 之外的子目录时这里看不见,
28079
+ * 结果是描述里少一句,而 SKILL.md 自己会写明该用什么号(FR-23.4)——那是第二条、
28080
+ * 也是更可靠的一条告知通路。反过来(一个技能都没有却说有)才是要防的。
28081
+ */
28082
+ async #hasSkillResources(fs) {
28083
+ const isResource = (stat) => stat.type === "file" && !stat.path.toLowerCase().endsWith(".md");
28084
+ const list = async (path) => await fs.list(path).catch(() => []);
28085
+ for (const root of this.#adapter.skillRoots) for (const dir of await list(root)) {
28086
+ if (dir.type !== "directory") continue;
28087
+ const entries = await list(dir.path);
28088
+ if (!entries.some((e) => e.type === "file" && e.path.endsWith("/SKILL.md"))) continue;
28089
+ if (entries.some(isResource)) return true;
28090
+ for (const sub of ["assets", "references"]) if ((await list(`${dir.path}/${sub}`)).some(isResource)) return true;
28091
+ }
28092
+ return false;
28093
+ }
28094
+ /**
28095
+ * 这一次 run 属于哪个会话。
28096
+ *
28097
+ * 进行中读内存,已结束读 trace——与 `#skillNamesOfRun` 同一范式。
28098
+ * 只读 trace 会让**工具调用期**的每一次查找扑空:trace 要等 run 收尾才 `put`,
28099
+ * 于是 `export_spreadsheet` 当场拿不到会话,而文档投放(用户事后点按钮)却正常,
28100
+ * 表现成「同一张图 Word 放得进、Excel 放不进」。
28101
+ */
28102
+ async #sessionOfRun(runId) {
28103
+ return this.#runSessions.get(runId) ?? (await this.#traceStore.get(runId).catch(() => void 0))?.sessionId;
28104
+ }
28105
+ /**
28106
+ * 本会话里 `runId` 之前的那些 run,由近及远。
28107
+ *
28108
+ * 「先出 Word,再出一份 PPT」是两轮 run,图片产物却落在第一轮的目录里;
28109
+ * 只认当前 run 会让第二份文档必然缺图,而且缺得毫无道理——用户看来那张图刚刚还在。
28110
+ *
28111
+ * 放宽止于**本会话**:目录仍由引擎按 runId 拼,产物名仍过 `assertSafePathSegment`,
28112
+ * 模型既给不出别的会话,也给不出路径(FR-11.7 的守卫面一寸没让)。
28113
+ */
28114
+ async #priorRunIds(runId) {
28115
+ const sessionId = await this.#sessionOfRun(runId);
28116
+ if (sessionId === void 0) return [];
28117
+ const messages = (await this.#sessions.get(sessionId).catch(() => void 0))?.messages ?? [];
28118
+ const ids = [];
28119
+ for (const message of messages) {
28120
+ const id = message.runId;
28121
+ if (message.role !== "assistant" || id === void 0 || id === runId || ids.includes(id)) continue;
28122
+ ids.push(id);
28123
+ }
28124
+ return ids.reverse();
28125
+ }
28126
+ /**
28127
+ * 表格里那张图的字节(0.22.0 分册 20 FR-20.8)。
28128
+ *
28129
+ * 与投放文档共用 `#documentImageBytes`:AC-G1 要的不是几条通道行为一致,而是只有一条通道。
28130
+ * 「同一张图 Word 放得进、Excel 放不进」正是分叉出第二条通道的代价。
28131
+ *
28132
+ * 外链在这里是硬拒:xlsx 里没有「链着一张图」这回事,工作簿必须自带字节。
28133
+ */
28134
+ async #spreadsheetImage(ref, runId) {
28135
+ if (runId === void 0) throw new WebSkillError("TOOL_EXECUTION_FAILED", "Putting a picture in a workbook requires an active run.");
28136
+ const separator = ref.indexOf(":");
28137
+ const kind = separator === -1 ? "" : ref.slice(0, separator);
28138
+ const rest = ref.slice(separator + 1);
28139
+ 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.`);
28140
+ const image = await toDocumentImage(await this.#documentImageBytes(kind, rest, runId), { maxBytes: SPREADSHEET_LIMITS.imageBytes });
28141
+ return {
28142
+ bytes: Uint8Array.from(atob(image.data), (char) => char.charCodeAt(0)),
28143
+ contentType: image.mimeType
28144
+ };
28145
+ }
28146
+ /**
28147
+ * 会话里的上传件,从发起本 run 的那条用户消息起往前找。
28148
+ *
28149
+ * 与 `#priorRunIds` 同一个理由:「上传照片 → 出 Word → 再出 PPT」是两轮,
28150
+ * 第二轮那条用户消息上没有附件,只认本轮就必然缺图。附件不随轮次过期。
28151
+ *
28152
+ * 仍然**在消息里查**而不是拿引用当路径拼:候选全部是用户自己在这个会话里上传的东西,
28153
+ * 路径一寸都不来自模型,跨会话与路径穿越都无从谈起(FR-11.7)。
28154
+ * 落到哪条附件上由 `findAttachmentByRef` 判,与正文 markdown 那一侧共用一份规则。
28155
+ */
28156
+ async #uploadOfRun(runId, ref) {
28157
+ const sessionId = await this.#sessionOfRun(runId);
28158
+ if (sessionId === void 0) return void 0;
28159
+ const messages = (await this.#sessions.get(sessionId).catch(() => void 0))?.messages ?? [];
28160
+ const started = messages.findIndex((message) => message.role === "assistant" && message.runId === runId);
28161
+ for (let i = (started === -1 ? messages.length : started) - 1; i >= 0; i -= 1) {
28162
+ const message = messages[i];
28163
+ if (message?.role !== "user") continue;
28164
+ const hit = findAttachmentByRef(message.attachments ?? [], ref);
28165
+ if (hit !== void 0) return hit;
28166
+ }
28167
+ }
28168
+ /**
27400
28169
  * 产物名 → 绝对路径。`assertSafePathSegment` + `resolveInsideRoot` 两道都不是可选写法:
27401
28170
  * 少了它们,「组件只收产物名」就退化成「组件收完整路径」,凭空开出一个读取面。
27402
28171
  */
@@ -27405,6 +28174,84 @@ var ChatEngine = class {
27405
28174
  return resolveInsideRoot(dir, artifact);
27406
28175
  }
27407
28176
  /**
28177
+ * 把宿主页面上的一张图落成本 run 的产物(0.22.0 FR-12.1 / FR-12.7)。
28178
+ *
28179
+ * 三件事按顺序发生且都在引擎侧:授权 → 取字节 → 落产物。宿主只做中间那一步,
28180
+ * 因为「run 目录在哪、会话什么时候换」只有引擎知道。
28181
+ */
28182
+ async #capturePageImage(host, artifacts, input, runId) {
28183
+ if (runId === void 0) throw new WebSkillError("TOOL_EXECUTION_FAILED", "Capturing a picture requires an active run.");
28184
+ const note = input.note === void 0 ? void 0 : sanitizeUntrustedLine(input.note);
28185
+ if (!await (this.#imageSources ??= new ImageSourceAuthorizer({ ui: this.#uiBridge })).authorize("page-image", note === void 0 ? {} : { note })) return {
28186
+ reason: "the user declined to let the assistant copy pictures from the page",
28187
+ triedLevels: []
28188
+ };
28189
+ const captured = await host.capture(input.ref, MAX_DOCUMENT_IMAGE_BYTES);
28190
+ if ("reason" in captured) return {
28191
+ reason: captured.reason,
28192
+ triedLevels: captured.triedLevels
28193
+ };
28194
+ const seq = (this.#pageImageSeq.get(runId) ?? 0) + 1;
28195
+ this.#pageImageSeq.set(runId, seq);
28196
+ const path = `page-image-${String(seq)}.${captured.mimeType === "image/jpeg" ? "jpg" : "png"}`;
28197
+ await artifacts.createBinaryArtifact({
28198
+ runId,
28199
+ path,
28200
+ content: Uint8Array.from(atob(captured.data), (char) => char.charCodeAt(0)),
28201
+ mimeType: captured.mimeType,
28202
+ metadata: { resultCard: false }
28203
+ });
28204
+ return {
28205
+ artifact: `artifact:${path}`,
28206
+ width: captured.width,
28207
+ height: captured.height,
28208
+ bytes: captured.bytes
28209
+ };
28210
+ }
28211
+ /**
28212
+ * 把一张外链图落成本 run 的产物(0.22.0 FR-13.1 ~ FR-13.4)。
28213
+ *
28214
+ * 与 `#capturePageImage` 同一条顺序:授权 → 取字节 → 落产物。
28215
+ * **授权在 `fetch` 之前**不是风格问题:拒绝之后网络层必须一个请求都没发出去(AC-13.5),
28216
+ * 「先抓回来再丢掉」时用户的 IP 已经交出去了。
28217
+ */
28218
+ async #fetchRemoteImage(host, artifacts, input, runId) {
28219
+ if (runId === void 0) throw new WebSkillError("TOOL_EXECUTION_FAILED", "Downloading an image requires an active run.");
28220
+ const note = input.note === void 0 ? void 0 : sanitizeUntrustedLine(input.note);
28221
+ if (!await (this.#imageSources ??= new ImageSourceAuthorizer({ ui: this.#uiBridge })).authorize("remote-image", {
28222
+ ...note === void 0 ? {} : { note },
28223
+ url: sanitizeUntrustedLine(input.url)
28224
+ })) return {
28225
+ reason: "the user declined to let the assistant download images from the web",
28226
+ kind: "declined"
28227
+ };
28228
+ const fetched = await host.fetch(input.url, MAX_DOCUMENT_IMAGE_BYTES);
28229
+ if ("kind" in fetched) return {
28230
+ reason: fetched.reason,
28231
+ kind: fetched.kind
28232
+ };
28233
+ const seq = (this.#remoteImageSeq.get(runId) ?? 0) + 1;
28234
+ this.#remoteImageSeq.set(runId, seq);
28235
+ const path = `remote-image-${String(seq)}.${fetched.mimeType === "image/jpeg" ? "jpg" : "png"}`;
28236
+ await artifacts.createBinaryArtifact({
28237
+ runId,
28238
+ path,
28239
+ content: Uint8Array.from(atob(fetched.data), (char) => char.charCodeAt(0)),
28240
+ mimeType: fetched.mimeType,
28241
+ metadata: { resultCard: false }
28242
+ });
28243
+ return {
28244
+ artifact: `artifact:${path}`,
28245
+ width: fetched.width,
28246
+ height: fetched.height,
28247
+ bytes: fetched.bytes,
28248
+ ...input.describe === true ? {
28249
+ data: fetched.data,
28250
+ mimeType: fetched.mimeType
28251
+ } : {}
28252
+ };
28253
+ }
28254
+ /**
27408
28255
  * 这次 run 激活过的技能名。它是授权告知的主语,因此**不能**由发出 surface 的技能自报——
27409
28256
  * 那等于让被授权方自己写授权提示。
27410
28257
  *
@@ -27474,6 +28321,7 @@ var ChatEngine = class {
27474
28321
  this.#handles.clear();
27475
28322
  this.#maxHistoryMessages = void 0;
27476
28323
  this.#contextWindow = void 0;
28324
+ this.#skillIndex = void 0;
27477
28325
  }
27478
28326
  /**
27479
28327
  * 技能仓变更后调用(安装/卸载/发布):作废 runtime 缓存的技能目录,下次 send 重新扫描。
@@ -27484,6 +28332,7 @@ var ChatEngine = class {
27484
28332
  * @experimental
27485
28333
  */
27486
28334
  invalidateSkills() {
28335
+ this.#skillIndex = void 0;
27487
28336
  this.#ready?.then((runtime) => runtime.invalidate(), () => void 0);
27488
28337
  }
27489
28338
  /**
@@ -27511,6 +28360,7 @@ var ChatEngine = class {
27511
28360
  });
27512
28361
  this.#currentSessionId = session.id;
27513
28362
  this.#visionDelegate?.forgetSessionDecision();
28363
+ this.#imageSources?.forgetSessionDecisions();
27514
28364
  if (previous !== void 0 && previous !== session.id) {
27515
28365
  this.#endSession(previous, "switched");
27516
28366
  this.#applySelectedModel(void 0);
@@ -27529,6 +28379,7 @@ var ChatEngine = class {
27529
28379
  const previous = this.#currentSessionId;
27530
28380
  this.#currentSessionId = id;
27531
28381
  if (previous !== void 0 && previous !== id) this.#endSession(previous, "switched");
28382
+ if (previous !== id) this.#imageSources?.forgetSessionDecisions();
27532
28383
  if (options.cursor === void 0) {
27533
28384
  const stored = (await this.#sessions.getMeta(id))?.modelId;
27534
28385
  this.#applySelectedModel(stored ?? [...page.items].reverse().find((message) => message.model !== void 0)?.model);
@@ -27746,12 +28597,26 @@ var ChatEngine = class {
27746
28597
  text: handle
27747
28598
  });
27748
28599
  if (attachment.kind === "text" || attachment.kind === "document-text") {
27749
- const body = await this.#adapter.storage.readText(path);
28600
+ const body = await this.#attachmentText(attachment, path);
28601
+ if (body === void 0) {
28602
+ parts.push({
28603
+ type: "text",
28604
+ 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.` : "")
28605
+ });
28606
+ continue;
28607
+ }
27750
28608
  const routed = this.#routeText(attachment, body, attachment.kind);
27751
28609
  parts.push(routed.part);
27752
28610
  if (routed.notice !== void 0) notices.push(routed.notice);
27753
28611
  continue;
27754
28612
  }
28613
+ if (attachment.kind === "binary") {
28614
+ parts.push({
28615
+ type: "text",
28616
+ 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.`
28617
+ });
28618
+ continue;
28619
+ }
27755
28620
  const data = toBase64(await this.#adapter.storage.readBinary(path));
27756
28621
  if (attachment.kind === "image") {
27757
28622
  parts.push({
@@ -27775,7 +28640,7 @@ var ChatEngine = class {
27775
28640
  };
27776
28641
  }
27777
28642
  /**
27778
- * 文本类附件进上下文的预算(`loop.maxDocumentTextBytes`,默认 500 KB)。
28643
+ * 文本类附件进上下文的预算(`loop.maxDocumentTextBytes`,默认 5000 KB)。
27779
28644
  * 与 agentLoop 的 `#documentTooLarge` 同一个值、同一条口径。
27780
28645
  */
27781
28646
  #textBudget() {
@@ -27801,11 +28666,38 @@ var ChatEngine = class {
27801
28666
  * 给了 id 就是教模型去调一个不存在的工具。
27802
28667
  */
27803
28668
  #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;
28669
+ if (!this.#hasDocumentReader(attachment)) return void 0;
27806
28670
  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
28671
  }
27808
28672
  /**
28673
+ * 这份附件的格式有没有注册文档读取器,即 `read_document` 能不能读它(0.22.0 分册 16 FR-16.1)。
28674
+ *
28675
+ * 取件号与「超限改走分批」共用这一个判据:判反了不是少一句说明,
28676
+ * 而是给出一个调不到的工具(没 reader 却给 id),或者白白丢掉全文(有 reader 却当没有)。
28677
+ */
28678
+ #hasDocumentReader(attachment) {
28679
+ const mime = attachment.contentType.split(";")[0]?.trim().toLowerCase();
28680
+ 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;
28681
+ }
28682
+ /**
28683
+ * 文本类附件的正文。`text` 存的就是文本;`document-text` 存的是**原件**,文本在这里现抽。
28684
+ *
28685
+ * 抽取推迟到发送,是为了让 `read_document` 还能拿到原件字节去读图表与扫描页——
28686
+ * 上传时就抽掉的话,那些东西在磁盘上根本不存在了(0.22.0 分册 18)。
28687
+ *
28688
+ * 返回 `undefined` = 本环境抽不出文本。不抛错:附件是上传时被收下的,
28689
+ * 此刻让整条消息发不出去等于拿用户刚打的字陪绑,而原件还在盘上,模型仍可按 id 逐块读。
28690
+ */
28691
+ async #attachmentText(attachment, path) {
28692
+ if (attachment.kind === "text") return await this.#adapter.storage.readText(path);
28693
+ const bytes = await this.#adapter.storage.readBinary(path);
28694
+ if (!(bytes[0] === 80 && bytes[1] === 75)) return new TextDecoder().decode(bytes);
28695
+ const format = extractedDocumentFormat(attachment.contentType, attachment.name);
28696
+ const extract = format === "xlsx" ? this.#adapter.xlsxExtractor : format === "pptx" ? this.#adapter.pptxExtractor : this.#adapter.docxExtractor;
28697
+ if (extract === void 0) return void 0;
28698
+ return await extract(bytes);
28699
+ }
28700
+ /**
27809
28701
  * 文本类附件(纯文本 / docx / xlsx / 抽取出来的 PDF 文本)的发送形态。
27810
28702
  *
27811
28703
  * 预算走 `maxDocumentTextBytes` 而不是 `formatAttachmentText` 的 32 KiB 兜底:
@@ -27817,16 +28709,24 @@ var ChatEngine = class {
27817
28709
  const size = new TextEncoder().encode(body).length;
27818
28710
  if (size > max) {
27819
28711
  const locale = this.#liveConfig?.appearance.locale ?? "en";
28712
+ const params = {
28713
+ name: attachment.name,
28714
+ size: formatSize(size),
28715
+ limit: formatSize(max)
28716
+ };
28717
+ if (this.#hasDocumentReader(attachment)) return {
28718
+ part: {
28719
+ type: "text",
28720
+ 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.`
28721
+ },
28722
+ notice: translateText(locale, "attachment.pdfAsPagedRead", params)
28723
+ };
27820
28724
  return {
27821
28725
  part: {
27822
28726
  type: "text",
27823
28727
  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
28728
  },
27825
- notice: translateText(locale, "attachment.textTooLong", {
27826
- name: attachment.name,
27827
- size: formatSize(size),
27828
- limit: formatSize(max)
27829
- })
28729
+ notice: translateText(locale, "attachment.textTooLong", params)
27830
28730
  };
27831
28731
  }
27832
28732
  return { part: {
@@ -27841,9 +28741,11 @@ var ChatEngine = class {
27841
28741
  } };
27842
28742
  }
27843
28743
  /**
27844
- * 单个 PDF 附件的发送形态(FR-18.5)。阈值内直传原件——保住扫描件、版面与图表;
27845
- * 超阈值才退到抽取文本,抽不出就**拒绝并说明**,绝不截断:半份 PDF 是坏文件,
27846
- * 半份抽取文本会让模型以为自己读全了(FR-18.5a,与 `#documentTooLarge` 同口径)。
28744
+ * 单个 PDF 附件的发送形态(FR-18.5;0.22.0 分册 16 改写超阈值那一支)。
28745
+ *
28746
+ * 阈值内直传原件——保住扫描件、版面与图表。超阈值分两种走法:
28747
+ * 该格式**注册了读取器**时交给 `read_document` 分批读(FR-16.1),正文一个字都不进消息;
28748
+ * 没有读取器时才退到抽取文本,抽不出就**拒绝并说明**,绝不截断(FR-18.5a)。
27847
28749
  */
27848
28750
  async #routeDocument(attachment, data) {
27849
28751
  const max = this.#documentBudget();
@@ -27867,6 +28769,13 @@ var ChatEngine = class {
27867
28769
  },
27868
28770
  notice: translateText(locale, key, params)
27869
28771
  });
28772
+ if (this.#hasDocumentReader(attachment)) return {
28773
+ part: {
28774
+ type: "text",
28775
+ 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.`
28776
+ },
28777
+ notice: translateText(locale, "attachment.pdfAsPagedRead", params)
28778
+ };
27870
28779
  const extract = this.#adapter.pdfExtractor;
27871
28780
  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
28781
  let body;
@@ -28167,6 +29076,96 @@ var ChatEngine = class {
28167
29076
  };
28168
29077
  }
28169
29078
  /**
29079
+ * 会话级委派同意(0.23.0 分册 19 FR-19.10~19.14)。
29080
+ *
29081
+ * 记在**会话**上而不是引擎实例上:切到另一个会话是另一件任务,
29082
+ * 上一件任务里同意过并发委派不代表这一件也同意。
29083
+ */
29084
+ #delegationConsent = /* @__PURE__ */ new Map();
29085
+ /** 用户在确认卡里选定的并发数;没问过或被拒时为 1(拒绝后根本不会走到委派) */
29086
+ #delegationConcurrency() {
29087
+ const sessionId = this.#currentSessionId;
29088
+ if (sessionId === void 0) return 1;
29089
+ return this.#delegationConsent.get(sessionId)?.concurrency ?? 1;
29090
+ }
29091
+ /**
29092
+ * 首次委派前问一次,之后沿用本会话的答案(FR-19.12)。
29093
+ * `suggested` 是模型给的建议值,`hardLimit` 是宿主经 `policy` 设的天花板(FR-19.16)。
29094
+ */
29095
+ async #askDelegationConsent(suggested, hardLimit) {
29096
+ const sessionId = this.#currentSessionId ?? "";
29097
+ const remembered = this.#delegationConsent.get(sessionId);
29098
+ if (remembered !== void 0) return remembered;
29099
+ const max = Math.max(1, Math.floor(hardLimit));
29100
+ const preset = Math.min(Math.max(1, Math.floor(suggested)), max);
29101
+ this.#delegationAskSeq += 1;
29102
+ const response = await this.#uiBridge.request({
29103
+ type: "authorize",
29104
+ id: `delegation-consent-${this.#delegationAskSeq}`,
29105
+ capability: "confirm",
29106
+ message: `Run parts of this task as parallel sub-agents? What they do is not recorded in this conversation, so this conversation can no longer be saved as a new skill. At most ${preset} sub-agent(s) would run at the same time.`,
29107
+ details: {
29108
+ kind: "delegation",
29109
+ suggestedConcurrency: preset,
29110
+ maxConcurrency: max
29111
+ }
29112
+ });
29113
+ const decision = response.cancelled === true || response.value === false ? {
29114
+ approved: false,
29115
+ concurrency: 1
29116
+ } : {
29117
+ approved: true,
29118
+ concurrency: readChosenConcurrency(response.value, preset, max)
29119
+ };
29120
+ this.#delegationConsent.set(sessionId, decision);
29121
+ return decision;
29122
+ }
29123
+ #delegationAskSeq = 0;
29124
+ /**
29125
+ * 委派工具源 + 会话级确认闸门(0.23.0 分册 19)。
29126
+ *
29127
+ * 闸门套在**工具调用外层**而不是 `runner` 里:`runner` 已经在编排器的排队与
29128
+ * 预算发放之后,那时再问用户,被拒的那次已经占了一个名额与一份轮数额度。
29129
+ */
29130
+ #delegationToolSource(deps, loopConfig) {
29131
+ const source = createDelegationToolSource({
29132
+ runner: createChatbotSubAgentRunner({
29133
+ deps,
29134
+ loopConfig,
29135
+ uiBridge: this.#uiBridge
29136
+ }),
29137
+ parentBudget: () => this.#parentBudget(loopConfig),
29138
+ policy: this.#delegationPolicy(loopConfig),
29139
+ concurrencyLimit: () => Math.min(this.#delegationConcurrency(), DELEGATION_MAX_CONCURRENCY),
29140
+ todos: this.#todoStore
29141
+ });
29142
+ return {
29143
+ ...source,
29144
+ call: async (name, args) => {
29145
+ const suggested = args["suggestedConcurrency"];
29146
+ if (!(await this.#askDelegationConsent(typeof suggested === "number" && Number.isFinite(suggested) ? suggested : 1, DELEGATION_MAX_CONCURRENCY)).approved) return {
29147
+ ok: false,
29148
+ content: [],
29149
+ error: {
29150
+ code: "DELEGATION_DECLINED",
29151
+ message: "The user declined delegation for this conversation. Do this work yourself in this conversation and do not call delegate_task again."
29152
+ }
29153
+ };
29154
+ return source.call(name, args);
29155
+ }
29156
+ };
29157
+ }
29158
+ /**
29159
+ * 子 agent 的自身上限(FR-19.2/19.3)。**不再有内建的 8 轮**:
29160
+ * 缺省即不收紧,只受父级剩余预算约束;宿主在设置里调小 maxTurns 时才会收紧。
29161
+ */
29162
+ #delegationPolicy(loopConfig) {
29163
+ return {
29164
+ ...loopConfig.maxTurns !== void 0 ? { maxTurns: loopConfig.maxTurns } : {},
29165
+ ...loopConfig.totalTimeoutMs !== void 0 ? { timeoutMs: loopConfig.totalTimeoutMs } : {}
29166
+ };
29167
+ }
29168
+ /**
28170
29169
  * 浏览器内置模型自动进模型列表(FR-14.1,B4-01/FR-27.2 调整)。
28171
29170
  * **只要浏览器支持 Prompt API**(含「模型未就绪」model-not-ready)就加入条目;
28172
29171
  * API 完全不存在(api-missing / unsupported-browser)才不加——不可用只在列表里
@@ -28227,6 +29226,7 @@ var ChatEngine = class {
28227
29226
  ...this.#adapter.downloads.consent ? { consent: this.#adapter.downloads.consent } : {},
28228
29227
  ...this.#adapter.docxExtractor ? { docxExtractor: this.#adapter.docxExtractor } : {},
28229
29228
  ...this.#adapter.xlsxExtractor ? { xlsxExtractor: this.#adapter.xlsxExtractor } : {},
29229
+ ...this.#adapter.pptxExtractor ? { pptxExtractor: this.#adapter.pptxExtractor } : {},
28230
29230
  ...this.#adapter.pdfExtractor ? { pdfExtractor: this.#adapter.pdfExtractor } : {},
28231
29231
  maxDocumentBytes: () => this.#documentBudget(),
28232
29232
  ...this.#adapter.documentAudit ? { audit: this.#adapter.documentAudit } : {}
@@ -28235,23 +29235,78 @@ var ChatEngine = class {
28235
29235
  root: `${this.#chatRoot}/artifacts`,
28236
29236
  fs
28237
29237
  });
29238
+ const readDocumentBytes = async (id, context) => {
29239
+ const prefix = PREFIXED_TEMPLATE_IDS.find((p) => id.startsWith(p));
29240
+ if (prefix) throw new WebSkillError("TOOL_UNSUPPORTED", `The id "${id}" names a ${prefix === "skill:" ? "file bundled with a skill" : "document open in an online viewer"}. It can only be used as a template (inspect_template / fill_template), not read here.`);
29241
+ const attachment = this.#runAttachments.find((a) => a.id === id);
29242
+ if (attachment) {
29243
+ const bytes = await this.#adapter.storage.readBinary(`${this.attachmentsRoot}/${attachment.path}`);
29244
+ return {
29245
+ name: attachment.name,
29246
+ bytes
29247
+ };
29248
+ }
29249
+ const stored = context?.binaries?.get(id);
29250
+ if (stored) return {
29251
+ name: stored.name,
29252
+ bytes: stored.bytes
29253
+ };
29254
+ if (!downloadPolicy) return void 0;
29255
+ const downloaded = await downloadPolicy.readBytes(id);
29256
+ return {
29257
+ name: downloaded.entry.name,
29258
+ bytes: downloaded.bytes
29259
+ };
29260
+ };
29261
+ /**
29262
+ * 模板字节比 `read_document` 多认两条取件号:`weboffice:<handle>`(0.22.0 分册 42 FR-42.12)
29263
+ * 与 `skill:<技能名>/<路径>`(0.23.0 分册 23 FR-23.1)。
29264
+ *
29265
+ * **前缀先认,认出来就只走它自己那条路,取不到即抛、不回落**(FR-23.13)。
29266
+ * 顺序不能倒过来:`readDocumentBytes` 的末段对认不出的号是**抛**而不是返回
29267
+ * `undefined`,所以「先穷尽本地再看前缀」时前缀判断根本执行不到——0.22.0 的
29268
+ * `weboffice:` 在接了下载端口的宿主里就是这么失效的(设计 24 §1 实测)。
29269
+ *
29270
+ * 不回落同样是硬的:带前缀的号是指名的那一份,回落等于拿另一份文档冒充它。
29271
+ */
29272
+ const readTemplateBytes = async (id, context) => {
29273
+ if (id.startsWith("skill:")) return await this.#readSkillTemplate(id, fs);
29274
+ if (id.startsWith("weboffice:")) {
29275
+ const online = this.#adapter.webOfficeTemplateSource;
29276
+ if (!online) throw new WebSkillError("TOOL_UNSUPPORTED", `Cannot read "${id}": this conversation has no online document source attached.`);
29277
+ return await online.read(id.slice(10), context);
29278
+ }
29279
+ return await readDocumentBytes(id, context);
29280
+ };
29281
+ /**
29282
+ * 模板取件号里那些**宿主接了才存在**的说法(0.22.0 FR-42.12、0.23.0 FR-23.3)。
29283
+ *
29284
+ * 无条件写死的后果是模型反复尝试一个在这个宿主里永远读不到的号。技能那一句
29285
+ * 还多一道闸:一个带资源的技能都没装时不提(AC-23.8)。探测只在真有模板工具时做。
29286
+ */
29287
+ const hasTemplateEngine = Boolean(this.#adapter.xlsxTemplateEngine || this.#adapter.docxTemplateEngine);
29288
+ const templateIdHints = [...this.#adapter.webOfficeTemplateSource ? [`For a document open in an online WPS/WebOffice viewer, use "${WEB_OFFICE_TEMPLATE_PREFIX}<handle>" with the handle from list_weboffice_documents.`] : [], ...hasTemplateEngine && await this.#hasSkillResources(fs) ? [`For a file that ships inside an installed skill, use "${SKILL_FILE_PREFIX}<skill name>/<path inside the skill>", for example "${SKILL_FILE_PREFIX}quotation/assets/template.xlsx".`] : []];
28238
29289
  const baseTools = [
28239
29290
  ...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
- } }),
29291
+ createSpreadsheetToolSource({
29292
+ writeArtifact: (path, content, mimeType, context) => {
29293
+ if (!context?.runId) throw new WebSkillError("TOOL_EXECUTION_FAILED", "Writing a workbook requires an active run.");
29294
+ return artifactStore.createBinaryArtifact({
29295
+ runId: context.runId,
29296
+ path,
29297
+ content,
29298
+ mimeType
29299
+ });
29300
+ },
29301
+ resolveImage: (ref, context) => this.#spreadsheetImage(ref, context?.runId)
29302
+ }),
28249
29303
  ...(rc === void 0 ? this.#options.generativeUi === true : rc.agentCapabilities.generativeUi) ? [createUiCatalogToolSource(rc ? { presets: rc.agentCapabilities.uiPresets } : {})] : [],
28250
29304
  ...rc?.agentCapabilities.todo === false ? [] : [createTodoToolSource({ store: this.#todoStore })],
28251
29305
  ...rc?.agentCapabilities.skillGeneration === true ? [createSkillGenerationToolSource({
28252
29306
  ui: this.#uiBridge,
28253
29307
  ...this.#options.skillCandidates ? { sink: this.#options.skillCandidates } : {},
28254
29308
  sessionId: () => this.#currentSessionId,
29309
+ resolveAttachment: (id) => this.#skillAttachment(id),
28255
29310
  ...toolStepStore ? { steps: toolStepStore } : {},
28256
29311
  messages: { confirmSkill: (draft) => translateText(rc.appearance.locale, "skillGeneration.confirm", {
28257
29312
  name: draft.name,
@@ -28272,29 +29327,51 @@ var ChatEngine = class {
28272
29327
  }
28273
29328
  })] : [],
28274
29329
  ...this.#adapter.pageActions ? [createPageActionToolSource({ policy: this.#adapter.pageActions })] : [],
29330
+ ...this.#adapter.pageImage ? [createPageImageToolSource({ capture: (input, context) => this.#capturePageImage(this.#adapter.pageImage, artifactStore, input, context?.runId) })] : [],
29331
+ ...this.#adapter.remoteImage ? [createRemoteImageToolSource({ fetchImage: (input, context) => this.#fetchRemoteImage(this.#adapter.remoteImage, artifactStore, input, context?.runId) })] : [],
28275
29332
  ...this.#adapter.pageTabs ? [createTabWorksetToolSource({ policy: this.#adapter.pageTabs })] : [],
28276
29333
  ...downloadPolicy ? [createDownloadedFileToolSource({ policy: downloadPolicy })] : [],
28277
29334
  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
- } },
29335
+ files: { read: readDocumentBytes },
28294
29336
  ...this.#adapter.pdfDocumentReader ? { pdf: this.#adapter.pdfDocumentReader } : {},
28295
29337
  ...this.#adapter.docxDocumentReader ? { docx: this.#adapter.docxDocumentReader } : {},
28296
- ...this.#adapter.xlsxDocumentReader ? { xlsx: this.#adapter.xlsxDocumentReader } : {}
29338
+ ...this.#adapter.xlsxDocumentReader ? { xlsx: this.#adapter.xlsxDocumentReader } : {},
29339
+ ...this.#adapter.pptxDocumentReader ? { pptx: this.#adapter.pptxDocumentReader } : {}
28297
29340
  }),
29341
+ ...this.#adapter.xlsxTemplateEngine || this.#adapter.docxTemplateEngine ? [createTemplateToolSource({
29342
+ files: { read: readTemplateBytes },
29343
+ ...templateIdHints.length > 0 ? { idHint: templateIdHints.join(" ") } : {},
29344
+ ...this.#adapter.xlsxTemplateEngine ? { xlsx: this.#adapter.xlsxTemplateEngine } : {},
29345
+ ...this.#adapter.docxTemplateEngine ? { docx: this.#adapter.docxTemplateEngine } : {},
29346
+ writeArtifact: (path, content, mimeType, context) => {
29347
+ if (!context?.runId) throw new WebSkillError("TOOL_EXECUTION_FAILED", "Filling a template requires an active run.");
29348
+ return artifactStore.createBinaryArtifact({
29349
+ runId: context.runId,
29350
+ path,
29351
+ content,
29352
+ mimeType
29353
+ });
29354
+ },
29355
+ resolveImage: async (ref, context) => {
29356
+ const found = await this.#spreadsheetImage(ref, context?.runId);
29357
+ return {
29358
+ bytes: found.bytes,
29359
+ extension: found.contentType.replace(/^image\//, "") || "png",
29360
+ contentType: found.contentType
29361
+ };
29362
+ }
29363
+ })] : [],
29364
+ ...this.#adapter.dwg ? [createDwgToolSource({
29365
+ load: async (file, context) => {
29366
+ const found = await readDocumentBytes(file, context);
29367
+ 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.`);
29368
+ return {
29369
+ document: await this.#adapter.dwg.read(found.bytes, found.name),
29370
+ name: found.name
29371
+ };
29372
+ },
29373
+ project: (input) => this.#adapter.dwg.project(input)
29374
+ })] : [],
28298
29375
  ...this.#adapter.webOffice ? [createWebOfficeToolSource({ policy: new WebOfficePolicy({
28299
29376
  host: this.#adapter.webOffice.host,
28300
29377
  ui: this.#uiBridge,
@@ -28345,6 +29422,7 @@ var ChatEngine = class {
28345
29422
  ...this.#adapter.linkedDocuments ? { linkedDocuments: this.#adapter.linkedDocuments } : {},
28346
29423
  ...this.#adapter.docxExtractor ? { docxExtractor: this.#adapter.docxExtractor } : {},
28347
29424
  ...this.#adapter.xlsxExtractor ? { xlsxExtractor: this.#adapter.xlsxExtractor } : {},
29425
+ ...this.#adapter.pptxExtractor ? { pptxExtractor: this.#adapter.pptxExtractor } : {},
28348
29426
  ...this.#adapter.pdfExtractor ? { pdfExtractor: this.#adapter.pdfExtractor } : {},
28349
29427
  visionDelegate: this.#visionDelegate,
28350
29428
  ...this.#adapter.documentAudit ? { documentAudit: this.#adapter.documentAudit } : {},
@@ -28355,17 +29433,10 @@ var ChatEngine = class {
28355
29433
  };
28356
29434
  const runtime = new WebSkillRuntime({
28357
29435
  ...deps,
28358
- externalTools: [...baseTools, ...rc?.agentCapabilities.delegation === true ? [createDelegationToolSource({
28359
- runner: createChatbotSubAgentRunner({
28360
- deps,
28361
- loopConfig,
28362
- uiBridge: this.#uiBridge
28363
- }),
28364
- parentBudget: () => this.#parentBudget(loopConfig),
28365
- todos: this.#todoStore
28366
- })] : []]
29436
+ externalTools: [...baseTools, ...rc?.agentCapabilities.delegation === true ? [this.#delegationToolSource(deps, loopConfig)] : []]
28367
29437
  });
28368
29438
  runtime.events.on("*", (event) => {
29439
+ this.#runSessions.set(event.runId, event.sessionId);
28369
29440
  if (event.phase === "route") {
28370
29441
  this.#runSkills.set(event.runId, /* @__PURE__ */ new Set());
28371
29442
  this.#runPhases.delete(event.runId);
@@ -28541,6 +29612,7 @@ var ChatEngine = class {
28541
29612
  message: assistantMessage
28542
29613
  });
28543
29614
  this.#runSkills.delete(run.id);
29615
+ this.#runSessions.delete(run.id);
28544
29616
  this.#runTodos.delete(run.id);
28545
29617
  this.#runPerceptions.delete(run.id);
28546
29618
  this.#runCandidates.delete(run.id);
@@ -28758,7 +29830,7 @@ const useWebSkillThreadList = ({ sessions, currentId, isLoading, thread, onSwitc
28758
29830
  id: mainThreadId,
28759
29831
  createdAt: ""
28760
29832
  };
28761
- const items = useClientLookup((pending ? [...sessions, pending] : sessions).map((session) => withKey(session.id, ThreadListItemClient({
29833
+ const items = useClientLookup((pending ? [pending, ...sessions] : sessions).map((session) => withKey(session.id, ThreadListItemClient({
28762
29834
  session,
28763
29835
  onSwitchTo: () => session.id === "__webskill_new_thread__" ? onCreate() : onSwitchTo(session.id),
28764
29836
  onRename: (title) => onRename(session.id, title),
@@ -29498,14 +30570,6 @@ function AssistantUiSessionList({ className = "", onNavigate, onCollapse, hasEar
29498
30570
  className: "min-h-0 flex-1 overflow-y-auto px-2 pb-3",
29499
30571
  role: "list",
29500
30572
  children: [
29501
- hasEarlier && onLoadEarlier ? /* @__PURE__ */ jsx("button", {
29502
- type: "button",
29503
- "data-testid": "load-earlier-sessions",
29504
- disabled: loadingEarlier,
29505
- onClick: onLoadEarlier,
29506
- className: "mb-1 w-full rounded-md px-2.5 py-2 text-sm text-muted transition-colors hover:bg-subtle hover:text-ink disabled:opacity-60",
29507
- children: loadingEarlier ? t("session.loading") : t("session.loadEarlier")
29508
- }) : null,
29509
30573
  skipped.length > 0 ? /* @__PURE__ */ jsx("p", {
29510
30574
  role: "status",
29511
30575
  "data-testid": "session-skipped",
@@ -29549,7 +30613,15 @@ function AssistantUiSessionList({ className = "", onNavigate, onCollapse, hasEar
29549
30613
  onEndRename: () => setRenamingId(void 0)
29550
30614
  });
29551
30615
  }
29552
- })
30616
+ }),
30617
+ hasEarlier && onLoadEarlier ? /* @__PURE__ */ jsx("button", {
30618
+ type: "button",
30619
+ "data-testid": "load-earlier-sessions",
30620
+ disabled: loadingEarlier,
30621
+ onClick: onLoadEarlier,
30622
+ className: "mt-1 w-full rounded-md px-2.5 py-2 text-sm text-muted transition-colors hover:bg-subtle hover:text-ink disabled:opacity-60",
30623
+ children: loadingEarlier ? t("session.loading") : t("session.loadEarlier")
30624
+ }) : null
29553
30625
  ]
29554
30626
  })
29555
30627
  ]
@@ -29793,6 +30865,25 @@ function webOfficeDetails(details) {
29793
30865
  };
29794
30866
  }
29795
30867
  /**
30868
+ * 并发委派确认卡的载荷(0.23.0 分册 19 FR-19.10/19.14)。
30869
+ *
30870
+ * 与其他授权卡不同,这张卡带一个**旋钮**:用户不只是同不同意,还要定下同时跑几个。
30871
+ * `max` 是宿主硬上限,`suggested` 是模型的建议值(已被引擎截到 `max` 以内)——
30872
+ * 面板里出现一个选不了的数字,比不出现更糟。
30873
+ */
30874
+ function delegationDetails(details) {
30875
+ if (typeof details !== "object" || details === null) return void 0;
30876
+ const record = details;
30877
+ if (record["kind"] !== "delegation") return void 0;
30878
+ const suggested = record["suggestedConcurrency"];
30879
+ const max = record["maxConcurrency"];
30880
+ if (typeof max !== "number" || !Number.isFinite(max) || max < 1) return void 0;
30881
+ return {
30882
+ suggested: Math.min(Math.max(1, typeof suggested === "number" && Number.isFinite(suggested) ? Math.floor(suggested) : 1), Math.floor(max)),
30883
+ max: Math.floor(max)
30884
+ };
30885
+ }
30886
+ /**
29796
30887
  * 视觉委派告知卡的载荷(0.21.0 分册 17)。
29797
30888
  *
29798
30889
  * 端点的 `label` 与 `host` 都要:前者是用户在设置里给它起的名字,后者是它**真实的去处**。
@@ -29821,6 +30912,44 @@ function visionDelegationDetails(details) {
29821
30912
  ...typeof rememberLabel === "string" ? { rememberLabel } : {}
29822
30913
  };
29823
30914
  }
30915
+ /**
30916
+ * 图片来源授权卡的载荷(0.22.0 分册 11 §3.3 / 分册 12)。
30917
+ *
30918
+ * `kind` 决定风险说的是哪一件事:取页面上的图、还是去互联网上下载一张。
30919
+ * 两者共用一个能力名,所以**没有 `kind` 就不能渲染**——缺省回退成另一句话
30920
+ * 等于让用户在看不见来源的情况下点同意。
30921
+ */
30922
+ function imageSourceDetails(details) {
30923
+ if (typeof details !== "object" || details === null) return void 0;
30924
+ const record = details;
30925
+ const kind = record["kind"];
30926
+ if (kind !== "page-image" && kind !== "remote-image") return void 0;
30927
+ const note = record["note"];
30928
+ const url = record["url"];
30929
+ const rememberLabelKey = record["rememberLabelKey"];
30930
+ return {
30931
+ kind,
30932
+ ...typeof note === "string" && note !== "" ? { note } : {},
30933
+ ...typeof url === "string" && url !== "" ? { url } : {},
30934
+ ...record["rememberable"] === true ? { rememberable: true } : {},
30935
+ ...record["rememberScope"] === "session" ? { rememberScope: "session" } : {},
30936
+ ...typeof rememberLabelKey === "string" ? { rememberLabelKey } : {}
30937
+ };
30938
+ }
30939
+ /**
30940
+ * 这个地址是不是明文 http。
30941
+ *
30942
+ * 只认真正的 `http:` 协议,不做子串匹配——`https://a/?next=http://b` 里的那截
30943
+ * 不是本次要发的请求,据此吓唬用户会让这条提示很快被当成噪音忽略掉。
30944
+ */
30945
+ function isInsecureUrl(url) {
30946
+ if (url === void 0) return false;
30947
+ try {
30948
+ return new URL(url).protocol === "http:";
30949
+ } catch {
30950
+ return false;
30951
+ }
30952
+ }
29824
30953
  /** 五个沙箱能力闸门的共用载荷(`gate()` 统一注入 `skill`,其余按能力各带一项) */ function sandboxDetails(details) {
29825
30954
  if (typeof details !== "object" || details === null) return void 0;
29826
30955
  const record = details;
@@ -29858,6 +30987,7 @@ function localizedAuthorizeMessage(request, t) {
29858
30987
  size: formatBytes(details.file.size)
29859
30988
  })}`;
29860
30989
  }
30990
+ if (request.capability === "confirm" && delegationDetails(request.details) !== void 0) return t("interaction.delegation.body");
29861
30991
  if (request.capability === "readReference" || request.capability === "readAsset" || request.capability === "writeArtifact" || request.capability === "fetchData" || request.capability === "confirm") {
29862
30992
  const details = sandboxDetails(request.details);
29863
30993
  if (details === void 0) return void 0;
@@ -29910,6 +31040,14 @@ function localizedAuthorizeMessage(request, t) {
29910
31040
  host: details.endpointHost
29911
31041
  });
29912
31042
  }
31043
+ if (request.capability === "readImageSource") {
31044
+ const details = imageSourceDetails(request.details);
31045
+ if (details === void 0) return void 0;
31046
+ 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"}`);
31047
+ const insecure = isInsecureUrl(details.url) ? ` ${t("interaction.imageSource.insecure")}` : "";
31048
+ if (details.note === void 0) return `${head}${insecure}`;
31049
+ return `${head}${insecure} ${t("interaction.imageSource.note", { note: details.note })}`;
31050
+ }
29913
31051
  }
29914
31052
 
29915
31053
  //#endregion
@@ -29976,7 +31114,7 @@ function InteractionWaitBar({ bridge, label, elsewhere = false, panel, onLocate
29976
31114
  id: INTERACTION_WAIT_ID,
29977
31115
  "data-testid": "interaction-wait",
29978
31116
  role: "status",
29979
- className: "webskill-chat-prose mx-auto flex w-full items-center gap-2 px-4 pt-2 text-xs text-muted",
31117
+ className: "webskill-chat-prose mx-auto flex w-full shrink-0 items-center gap-2 px-4 pt-2 text-xs text-muted",
29980
31118
  children: [
29981
31119
  /* @__PURE__ */ jsx(Hourglass, {
29982
31120
  className: "size-3.5 shrink-0",
@@ -42189,6 +43327,8 @@ function attachmentAccept(options) {
42189
43327
  if (options.files) parts.push(...FILE_MIME_TYPES);
42190
43328
  if (options.documents) parts.push(DOCX_MIME, ".docx");
42191
43329
  if (options.spreadsheets) parts.push(XLSX_MIME, ".xlsx");
43330
+ if (options.presentations) parts.push(PPTX_MIME, ".pptx");
43331
+ if (options.drawings) parts.push(...DWG_MIME_TYPES, ...BINARY_EXTENSIONS.map((ext) => `.${ext}`));
42192
43332
  return parts.join(",");
42193
43333
  }
42194
43334
 
@@ -42207,11 +43347,14 @@ function safeName(name) {
42207
43347
  const cleaned = (name.split(/[\\/]/).pop() ?? "attachment").replace(/[^\w.\- ]+/g, "_").replace(/^\.+/, "");
42208
43348
  return cleaned === "" ? "attachment" : cleaned.slice(0, 80);
42209
43349
  }
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");
43350
+ /** 三种待抽取格式共用 `document-text` 这一种 kind,选抽取器时要按格式再分一次 */
43351
+ const EXTRACTED_LABEL = {
43352
+ docx: "Word documents",
43353
+ xlsx: "Excel workbooks",
43354
+ pptx: "PowerPoint presentations"
43355
+ };
43356
+ function formatOf(file) {
43357
+ return extractedDocumentFormat(file.type, file.name);
42215
43358
  }
42216
43359
  /**
42217
43360
  * 附件落盘到 `<attachmentsRoot>/<sessionId>/`,`ChatEngine.send` 再按路径读回构造 `LlmContentPart[]`。
@@ -42230,7 +43373,9 @@ var WebSkillAttachmentAdapter = class {
42230
43373
  images: this.#imagesAllowed(),
42231
43374
  files: true,
42232
43375
  documents: this.#options.docxExtractor !== void 0,
42233
- spreadsheets: this.#options.xlsxExtractor !== void 0
43376
+ spreadsheets: this.#options.xlsxExtractor !== void 0,
43377
+ presentations: this.#options.pptxExtractor !== void 0,
43378
+ drawings: this.#options.drawingsSupported?.() ?? false
42234
43379
  });
42235
43380
  }
42236
43381
  #multimodal() {
@@ -42240,7 +43385,10 @@ var WebSkillAttachmentAdapter = class {
42240
43385
  return this.#multimodal().imageAttachments && (this.#options.imageCapable?.() ?? false);
42241
43386
  }
42242
43387
  #extractorFor(file) {
42243
- return isSpreadsheet(file) ? this.#options.xlsxExtractor : this.#options.docxExtractor;
43388
+ const format = formatOf(file);
43389
+ if (format === "xlsx") return this.#options.xlsxExtractor;
43390
+ if (format === "pptx") return this.#options.pptxExtractor;
43391
+ return this.#options.docxExtractor;
42244
43392
  }
42245
43393
  async add({ file }) {
42246
43394
  const kind = classifyAttachment(file.type, file.name);
@@ -42248,11 +43396,12 @@ var WebSkillAttachmentAdapter = class {
42248
43396
  if (kind === "image") {
42249
43397
  if (!this.#multimodal().imageAttachments) throw new WebSkillError("ATTACHMENT_TYPE_REJECTED", "Image attachments are turned off. Enable them in settings before sending images.");
42250
43398
  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.`);
43399
+ } 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.`);
43400
+ 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
43401
  const id = `att-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
42253
43402
  const relative = `${this.#options.sessionId() ?? "pending"}/${id}-${safeName(file.name)}`;
42254
43403
  const path = `${this.#options.root}/${relative}`;
42255
- const contentType = file.type === "" ? "text/plain" : file.type;
43404
+ const contentType = file.type !== "" ? file.type : kind === "binary" ? "application/octet-stream" : "text/plain";
42256
43405
  try {
42257
43406
  this.#stored.set(id, await this.#store({
42258
43407
  id,
@@ -42296,9 +43445,8 @@ var WebSkillAttachmentAdapter = class {
42296
43445
  };
42297
43446
  }
42298
43447
  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())));
43448
+ if (this.#extractorFor(file) === void 0) throw new WebSkillError("ATTACHMENT_TYPE_REJECTED", "No document text extractor is wired up in this environment.");
43449
+ await this.#writeStreaming(path, file);
42302
43450
  return {
42303
43451
  ...base,
42304
43452
  size: file.size
@@ -42311,6 +43459,13 @@ var WebSkillAttachmentAdapter = class {
42311
43459
  size: file.size
42312
43460
  };
42313
43461
  }
43462
+ if (kind === "binary") {
43463
+ await this.#writeStreaming(path, file);
43464
+ return {
43465
+ ...base,
43466
+ size: file.size
43467
+ };
43468
+ }
42314
43469
  const compressed = await compressImageToBudget(file, this.#multimodal().maxImageBytes);
42315
43470
  await this.#options.storage.writeBinary(path, new Uint8Array(await compressed.blob.arrayBuffer()));
42316
43471
  const recoded = compressed.compressedBytes !== compressed.originalBytes;
@@ -42832,8 +43987,11 @@ function ActiveInteractionCard({ request, bridge }) {
42832
43987
  const downloadedFile = isAuthorize && request.capability === "readDownloadedFile" ? downloadedFileDetails(request.details) : void 0;
42833
43988
  const webOffice = isAuthorize && request.capability === "readWebOfficeDocument" ? webOfficeDetails(request.details) : void 0;
42834
43989
  const visionDelegation = isAuthorize && request.capability === "delegateVision" ? visionDelegationDetails(request.details) : void 0;
43990
+ const imageSource = isAuthorize && request.capability === "readImageSource" ? imageSourceDetails(request.details) : void 0;
43991
+ const delegation = isAuthorize && request.capability === "confirm" ? delegationDetails(request.details) : void 0;
43992
+ const [concurrency, setConcurrency] = useState(void 0);
42835
43993
  const traceEvidence = isAuthorize ? traceEvidenceDetails(request.details) : void 0;
42836
- const localized = pageAction !== void 0 || downloadedFile !== void 0 || webOffice !== void 0 || visionDelegation !== void 0;
43994
+ const localized = pageAction !== void 0 || downloadedFile !== void 0 || webOffice !== void 0 || visionDelegation !== void 0 || imageSource !== void 0 || delegation !== void 0;
42837
43995
  const spec = interactionToUiSpec(request.type === "authorize" && localized ? {
42838
43996
  ...request,
42839
43997
  message: ""
@@ -42859,6 +44017,13 @@ function ActiveInteractionCard({ request, bridge }) {
42859
44017
  return;
42860
44018
  }
42861
44019
  const value = shapeInteractionValue(interactionToFormModel(request), event.value ?? {});
44020
+ if (delegation !== void 0) {
44021
+ bridge.resolve({
44022
+ id: request.id,
44023
+ value: { concurrency: concurrency ?? delegation.suggested }
44024
+ });
44025
+ return;
44026
+ }
42862
44027
  bridge.resolve({
42863
44028
  id: request.id,
42864
44029
  value,
@@ -43036,6 +44201,72 @@ function ActiveInteractionCard({ request, bridge }) {
43036
44201
  }), visionDelegation.rememberLabel ?? t(visionDelegation.rememberScope === "endpoint" ? "interaction.visionDelegation.rememberFallback" : "interaction.visionDelegation.rememberSession")]
43037
44202
  }) : null
43038
44203
  ] }) : null,
44204
+ imageSource ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
44205
+ /* @__PURE__ */ jsx("p", {
44206
+ "data-testid": "interaction-image-source-title",
44207
+ className: "text-sm font-medium text-ink",
44208
+ children: t("interaction.imageSource.title")
44209
+ }),
44210
+ /* @__PURE__ */ jsx("p", {
44211
+ "data-testid": "interaction-image-source-body",
44212
+ className: "text-sm text-ink",
44213
+ children: imageSource.kind === "remote-image" ? imageSource.url !== void 0 ? t("interaction.imageSource.remoteImageBody", { url: imageSource.url }) : t("interaction.imageSource.remoteImageAnyBody") : t("interaction.imageSource.pageImageBody")
44214
+ }),
44215
+ isInsecureUrl(imageSource.url) ? /* @__PURE__ */ jsx("p", {
44216
+ "data-testid": "interaction-image-source-insecure",
44217
+ className: "text-sm text-destructive",
44218
+ children: t("interaction.imageSource.insecure")
44219
+ }) : null,
44220
+ imageSource.note !== void 0 ? /* @__PURE__ */ jsx("p", {
44221
+ "data-testid": "interaction-image-source-note",
44222
+ className: "text-sm text-muted",
44223
+ children: t("interaction.imageSource.note", { note: imageSource.note })
44224
+ }) : null,
44225
+ imageSource.rememberable === true ? /* @__PURE__ */ jsxs("label", {
44226
+ className: "flex items-center gap-2 text-sm text-ink",
44227
+ children: [/* @__PURE__ */ jsx("input", {
44228
+ type: "checkbox",
44229
+ "data-testid": "interaction-image-source-remember",
44230
+ checked: remember,
44231
+ onChange: (event) => setRemember(event.currentTarget.checked)
44232
+ }), t(imageSource.rememberLabelKey ?? "interaction.imageSource.rememberFallback")]
44233
+ }) : null
44234
+ ] }) : null,
44235
+ delegation ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
44236
+ /* @__PURE__ */ jsx("p", {
44237
+ "data-testid": "interaction-delegation-title",
44238
+ className: "text-sm font-medium text-ink",
44239
+ children: t("interaction.delegation.title")
44240
+ }),
44241
+ /* @__PURE__ */ jsx("p", {
44242
+ "data-testid": "interaction-delegation-body",
44243
+ className: "text-sm text-ink",
44244
+ children: t("interaction.delegation.body")
44245
+ }),
44246
+ /* @__PURE__ */ jsx("p", {
44247
+ "data-testid": "interaction-delegation-decline",
44248
+ className: "text-sm text-muted",
44249
+ children: t("interaction.delegation.declineNote")
44250
+ }),
44251
+ /* @__PURE__ */ jsxs("label", {
44252
+ className: "flex flex-wrap items-center gap-2 text-sm text-ink",
44253
+ children: [t("interaction.delegation.concurrency"), /* @__PURE__ */ jsx("select", {
44254
+ "data-testid": "interaction-delegation-concurrency",
44255
+ className: "rounded-md border border-border bg-canvas px-2 py-1 text-sm text-ink",
44256
+ value: String(concurrency ?? delegation.suggested),
44257
+ onChange: (event) => setConcurrency(Number(event.currentTarget.value)),
44258
+ children: Array.from({ length: delegation.max }, (_, index) => index + 1).map((n) => /* @__PURE__ */ jsx("option", {
44259
+ value: String(n),
44260
+ children: n
44261
+ }, n))
44262
+ })]
44263
+ }),
44264
+ /* @__PURE__ */ jsx("p", {
44265
+ "data-testid": "interaction-delegation-suggested",
44266
+ className: "text-xs text-muted",
44267
+ children: t("interaction.delegation.suggestedNote", { count: String(delegation.suggested) })
44268
+ })
44269
+ ] }) : null,
43039
44270
  traceEvidence ? /* @__PURE__ */ jsxs("div", {
43040
44271
  "data-testid": "interaction-trace-evidence",
43041
44272
  children: [/* @__PURE__ */ jsx("div", {
@@ -43694,6 +44925,7 @@ function Chatbot({ adapter, config, locale: localeProp, theme: themeProp, render
43694
44925
  multimodal: () => multimodalRef.current,
43695
44926
  imageCapable: () => imageCapableRef.current,
43696
44927
  notifyStorageError: (fileName) => storageToastRef.current?.(fileName),
44928
+ drawingsSupported: () => adapter.dwg !== void 0,
43697
44929
  ...adapter.docxExtractor ? { docxExtractor: adapter.docxExtractor } : {},
43698
44930
  ...adapter.xlsxExtractor ? { xlsxExtractor: adapter.xlsxExtractor } : {}
43699
44931
  }), [adapter, engine]);
@@ -43957,6 +45189,19 @@ function Chatbot({ adapter, config, locale: localeProp, theme: themeProp, render
43957
45189
  const bytes = await adapter.storage.readBinary(`${engine.attachmentsRoot}/${attachment.path}`);
43958
45190
  return URL.createObjectURL(new Blob([bytes], { type: attachment.contentType }));
43959
45191
  }, [adapter, engine]);
45192
+ const messagesRef = useRef(messages);
45193
+ messagesRef.current = messages;
45194
+ /** `![](upload:{id})` → 附件字节的 blob URL。模型输出不了 base64,正文贴附件图只能靠引用 */
45195
+ const resolveImageRef = useCallback(async (ref) => {
45196
+ const separator = ref.indexOf(":");
45197
+ if (separator === -1) return void 0;
45198
+ const kind = ref.slice(0, separator);
45199
+ const rest = ref.slice(separator + 1);
45200
+ if (kind !== "upload") return void 0;
45201
+ const attachment = findAttachmentByRef([...messagesRef.current].reverse().flatMap((message) => message.attachments ?? []), rest);
45202
+ if (attachment === void 0) return void 0;
45203
+ return resolveAttachmentUrl(attachment);
45204
+ }, [resolveAttachmentUrl]);
43960
45205
  const handleRetry = useCallback((messageId) => {
43961
45206
  flushPendingDeletes().then(() => engine.retry(messageId)).catch((e) => setError({ message: toErrorMessage(e) }));
43962
45207
  }, [engine, flushPendingDeletes]);
@@ -44032,7 +45277,7 @@ function Chatbot({ adapter, config, locale: localeProp, theme: themeProp, render
44032
45277
  if (!sessionsCursor || loadingEarlierSessions) return;
44033
45278
  setLoadingEarlierSessions(true);
44034
45279
  engine.listSessions({ cursor: sessionsCursor }).then((page) => {
44035
- setSessions((prev) => [...page.items, ...prev]);
45280
+ setSessions((prev) => [...prev, ...page.items]);
44036
45281
  setSessionsCursor(page.nextCursor);
44037
45282
  }).catch((e) => setError({ message: toErrorMessage(e) })).finally(() => setLoadingEarlierSessions(false));
44038
45283
  }, [
@@ -44123,187 +45368,190 @@ function Chatbot({ adapter, config, locale: localeProp, theme: themeProp, render
44123
45368
  onArchiveSession: handleArchiveSession,
44124
45369
  onUnarchiveSession: handleUnarchiveSession,
44125
45370
  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, {
45371
+ children: /* @__PURE__ */ jsx(MarkdownImageResolverProvider, {
45372
+ resolve: resolveImageRef,
45373
+ children: /* @__PURE__ */ jsx(ThemeScope, {
45374
+ ref: shellRef,
45375
+ theme,
45376
+ "data-layout": resolvedLayout,
45377
+ "data-session-list": resolvedSessionList,
45378
+ "data-session-narrow": narrow ? "true" : "false",
45379
+ className: "webskill-aui-shell relative flex h-full min-h-0 flex-row overflow-hidden font-sans text-ink",
45380
+ children: /* @__PURE__ */ jsxs(LocalizedUndoToastProvider, { children: [/* @__PURE__ */ jsxs(Fragment$1, { children: [
45381
+ /* @__PURE__ */ jsx(AttachmentStorageToastBridge, { sink: storageToastRef }),
45382
+ narrow && sessionsExpanded ? /* @__PURE__ */ jsx("div", {
45383
+ "data-testid": "session-list-overlay",
45384
+ "aria-hidden": true,
45385
+ onClick: () => setSessionsOpen(false),
45386
+ className: "absolute inset-0 z-[var(--webskill-z-raised)] bg-scrim"
45387
+ }) : null,
45388
+ /* @__PURE__ */ jsx(AssistantUiSessionList, {
45389
+ className: "webskill-aui-session-list",
45390
+ onCollapse: () => setSessionsOpen(false),
45391
+ ...narrow ? { onNavigate: () => setSessionsOpen(false) } : {},
45392
+ hasEarlier: sessionsCursor !== void 0,
45393
+ loadingEarlier: loadingEarlierSessions,
45394
+ onLoadEarlier: handleLoadEarlierSessions,
45395
+ source: sessionsSource,
45396
+ skipped: skippedSessions
45397
+ })
45398
+ ] }), /* @__PURE__ */ jsxs("div", {
45399
+ className: "webskill-chat-column flex min-h-0 min-w-0 flex-1 flex-col",
45400
+ children: [
45401
+ /* @__PURE__ */ jsx(AssistantUiThreadToolbar, {
45402
+ ...config?.title ? { title: config.title } : {},
45403
+ onOpenSessions: () => setSessionsOpen(true),
45404
+ onCreateSession: handleCreate,
45405
+ ...onOpenSettings ? { onOpenSettings: () => onOpenSettings() } : {},
45406
+ ...docked !== void 0 ? { docked } : {},
45407
+ ...onDock ? { onDock } : {},
45408
+ ...onClose ? { onClose } : {}
45409
+ }),
45410
+ /* @__PURE__ */ jsx(InterruptedBanner, {
45411
+ engine,
45412
+ ...currentSessionId ? { sessionId: currentSessionId } : {}
45413
+ }),
45414
+ /* @__PURE__ */ jsxs("div", {
45415
+ className: `flex min-h-0 flex-1 flex-col ${composerCentered ? "justify-center" : ""}`,
45416
+ children: [
45417
+ /* @__PURE__ */ jsx(AssistantUiViewport, {
45418
+ grow: !composerCentered,
45419
+ children: /* @__PURE__ */ jsxs("div", {
45420
+ className: "webskill-chat-prose mx-auto flex w-full min-w-0 flex-col gap-4 px-4 py-6",
45421
+ children: [
45422
+ messages.length === 0 && !sending ? /* @__PURE__ */ jsx(AssistantUiEmptyState, {
45423
+ ...config?.title ? { title: config.title } : {},
45424
+ ...staticQuickPrompts ? { prompts: staticQuickPrompts } : {},
45425
+ ...config?.dynamicQuickPrompts ? { dynamicPrompts: config.dynamicQuickPrompts } : {},
45426
+ limit: quickPromptLimit,
45427
+ disabled: sending,
45428
+ onPrompt: handleSend
45429
+ }) : null,
45430
+ /* @__PURE__ */ jsx(MessageDeleteOutlet, {
45431
+ queue: pendingDeletes,
44189
45432
  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
45433
+ ...currentSessionId ? { sessionId: currentSessionId } : {},
45434
+ onRemove: (messageId) => setMessages((prev) => prev.filter((m) => m.id !== messageId)),
45435
+ children: (handleDeleteMessage) => /* @__PURE__ */ jsx(AssistantUiMessageList, {
45436
+ messages,
45437
+ ...messagesCursor !== void 0 ? { earlier: {
45438
+ loading: loadingEarlierMessages,
45439
+ onLoad: handleLoadEarlierMessages
45440
+ } } : {},
45441
+ renderMessage: (message) => /* @__PURE__ */ jsx(AssistantUiMessage, {
45442
+ message,
45443
+ onRetry: handleRetry,
45444
+ onDelete: handleDeleteMessage,
45445
+ onDownload: handleDownload,
45446
+ modelLabels,
45447
+ resolveAttachmentUrl,
45448
+ ...surfaceRegistry ? { surfaceRegistry } : {},
45449
+ ...engine.documentSurface ? { documentSurface: engine.documentSurface } : {},
45450
+ renderer,
45451
+ bridge: engine.bridge,
45452
+ ...adapter.navigation.openConsoleTrace ? { onViewTrace: adapter.navigation.openConsoleTrace } : {},
45453
+ ...adapter.navigation.openConsoleCandidate ? { onOpenCandidate: adapter.navigation.openConsoleCandidate } : {}
45454
+ })
44219
45455
  })
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)
45456
+ }),
45457
+ (() => {
45458
+ const interactionVisible = interactionSessionId === void 0 || interactionSessionId === currentSessionId;
45459
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [renderer !== "native" ? /* @__PURE__ */ jsx("div", {
45460
+ "data-testid": "spec-interaction-slot",
45461
+ ...interactionVisible ? { "data-webskill-interaction-anchor": "" } : {},
45462
+ className: interactionVisible ? void 0 : "hidden",
45463
+ children: /* @__PURE__ */ jsx(SpecInteraction, {
45464
+ channel: engine.interactionChannel,
45465
+ renderer
45466
+ })
45467
+ }) : null, interactionVisible ? /* @__PURE__ */ jsx(InteractionCard, { bridge: engine.bridge }) : null] });
45468
+ })(),
45469
+ /* @__PURE__ */ jsx(ResultBlocksPro, {
45470
+ bridge: engine.bridge,
45471
+ activeRunId: currentRunId,
45472
+ onDownload: handleDownload
45473
+ }),
45474
+ error ? /* @__PURE__ */ jsx(ErrorCard, {
45475
+ message: error.message,
45476
+ ...error.code ? { code: error.code } : {},
45477
+ loopLimits,
45478
+ ...onOpenSettings ? { onOpenSettings: () => onOpenSettings("settings.runtime") } : {},
45479
+ onDismiss: () => setError(void 0)
45480
+ }) : null
45481
+ ]
44255
45482
  })
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
- })] })
45483
+ }),
45484
+ /* @__PURE__ */ jsx(InteractionWaitBar, {
45485
+ bridge: engine.bridge,
45486
+ ...interactionPending && interactionTarget !== void 0 ? { label: interactionTarget } : {},
45487
+ ...interactionElsewhere ? {
45488
+ elsewhere: true,
45489
+ ...interactionPanel !== void 0 ? { panel: interactionPanel } : {}
45490
+ } : {},
45491
+ onLocate: handleLocateInteraction
45492
+ }),
45493
+ (() => {
45494
+ const items = visiblePinnedTodos(pinnedTodos);
45495
+ return items ? /* @__PURE__ */ jsx("div", {
45496
+ "data-testid": "pinned-todo-panel",
45497
+ className: "webskill-chat-prose mx-auto w-full shrink-0 px-3",
45498
+ children: /* @__PURE__ */ jsx(TodoPanel, {
45499
+ items,
45500
+ scrollable: true,
45501
+ onDismiss: () => setPinnedTodos(dismissPinnedTodos)
45502
+ })
45503
+ }) : null;
45504
+ })(),
45505
+ (() => {
45506
+ const window = selectedEntry?.contextWindow;
45507
+ const usage = formatContextUsage(contextTokens, window);
45508
+ if (!queuedSend && usage === void 0) return null;
45509
+ return /* @__PURE__ */ jsxs("div", {
45510
+ className: "webskill-chat-prose text-muted-foreground mx-auto flex w-full shrink-0 items-center justify-between gap-2 px-3 text-xs",
45511
+ children: [queuedSend ? /* @__PURE__ */ jsx("span", {
45512
+ "data-testid": "queued-send-hint",
45513
+ children: locale === "zh" ? "前一条还在处理,这条排在后面" : "Waiting for the previous message to finish"
45514
+ }) : /* @__PURE__ */ jsx("span", {}), usage !== void 0 ? /* @__PURE__ */ jsx("span", {
45515
+ "data-testid": "context-usage",
45516
+ children: locale === "zh" ? `上下文 ${usage}` : `Context ${usage}`
45517
+ }) : null]
45518
+ });
45519
+ })(),
45520
+ /* @__PURE__ */ jsx(AssistantUiComposer, {
45521
+ ...modelCapabilities ? { capabilities: {
45522
+ tools: modelCapabilities.tools,
45523
+ image: modelCapabilities.image
45524
+ } } : {},
45525
+ running: busy,
45526
+ disabled: interactionPending,
45527
+ ...interactionTarget !== void 0 ? { waitTarget: interactionTarget } : {},
45528
+ centered: composerCentered,
45529
+ attachments: attachmentsReady,
45530
+ attachmentInfo: (id) => attachmentAdapter.describe(id),
45531
+ attachmentImages: {
45532
+ enabled: multimodal.imageAttachments,
45533
+ imageCapable: modelTakesImages
45534
+ },
45535
+ ...dictationLang !== "" ? { dictationLang } : {},
45536
+ ...onDictationPermissionDenied ? { onDictationPermissionDenied } : {},
45537
+ ...onCameraPermissionDenied ? { onCameraPermissionDenied } : {},
45538
+ ...composerModels && !noUsableModel ? { models: composerModels } : {},
45539
+ ...noUsableModel ? { noModel: { onConfigure: () => setNoModelGuideOpen(true) } } : {},
45540
+ ...config?.placeholder ? { placeholder: config.placeholder } : {}
45541
+ }),
45542
+ /* @__PURE__ */ jsx(NoModelGuideDialog, {
45543
+ open: noModelGuideOpen,
45544
+ onClose: () => setNoModelGuideOpen(false),
45545
+ ...onOpenSettings ? { onConfigure: () => {
45546
+ setNoModelGuideOpen(false);
45547
+ onOpenSettings("connections.models");
45548
+ } } : {}
45549
+ })
45550
+ ]
45551
+ })
45552
+ ]
45553
+ })] })
45554
+ })
44307
45555
  })
44308
45556
  }) })
44309
45557
  });
@@ -44364,7 +45612,7 @@ function VercelPayloadPreview({ bridge }) {
44364
45612
  * Version of the published `@webskill/chatbot` package, injected at build time.
44365
45613
  * @stable
44366
45614
  */
44367
- const CHATBOT_VERSION = "0.18.0";
45615
+ const CHATBOT_VERSION = "0.20.0";
44368
45616
 
44369
45617
  //#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 };
45618
+ 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 };