@webskill/chatbot 0.24.2 → 0.25.1

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/chatbot.css CHANGED
@@ -1204,6 +1204,9 @@
1204
1204
  .pb-1 {
1205
1205
  padding-bottom: var(--spacing);
1206
1206
  }
1207
+ .pb-2 {
1208
+ padding-bottom: calc(var(--spacing) * 2);
1209
+ }
1207
1210
  .pb-3 {
1208
1211
  padding-bottom: calc(var(--spacing) * 3);
1209
1212
  }
@@ -1867,6 +1870,11 @@
1867
1870
  color: var(--sidebar-accent-foreground);
1868
1871
  }
1869
1872
  }
1873
+ @media (prefers-reduced-motion: reduce) {
1874
+ .motion-reduce\:animate-none {
1875
+ animation: none;
1876
+ }
1877
+ }
1870
1878
  @media (width < 40rem) {
1871
1879
  .max-sm\:top-0 {
1872
1880
  top: 0px;
package/dist/config.d.ts CHANGED
@@ -58,15 +58,22 @@ interface ApplyConfigDefaultsInput {
58
58
  /** 上次应用过的缺省值快照,可缺席 */
59
59
  readonly lastApplied?: WebSkillConfigDefaults;
60
60
  }
61
- /** 合并结果:可直接使用的配置,以及要持久化的本次缺省值快照 @experimental */
61
+ /** 合并结果:可直接使用的配置,以及要持久化的本次快照 @experimental */
62
62
  interface AppliedConfigDefaults {
63
63
  readonly config: RuntimeConfig;
64
- /** 本次应用的缺省值;调用方负责持久化,下次作为 `lastApplied` 传回 */
64
+ /**
65
+ * 本次应用的快照;调用方负责持久化,下次作为 `lastApplied` 传回。
66
+ * 有烘焙模型时它还带一个内部同步状态(0.27.1),**必须整对象存取**——
67
+ * 只挑 `WebSkillConfigDefaults` 的已知键会把状态丢掉。
68
+ */
65
69
  readonly snapshot: WebSkillConfigDefaults;
66
70
  }
67
71
  /**
68
72
  * `lastApplied` 缺席时每条分支都退化成「stored 存在就取 stored」,
69
73
  * 即老实现的 `deepMerge(defaults, stored)`——修订跟随是纯增量,不改既有语义。
74
+ *
75
+ * 快照里的内部同步状态只在有烘焙模型时出现;它不是 `RuntimeConfig` 字段,
76
+ * 宿主照旧整存整取快照即可。
70
77
  * @experimental
71
78
  */
72
79
  declare function applyConfigDefaults(input: ApplyConfigDefaultsInput): AppliedConfigDefaults;
@@ -97,7 +104,13 @@ declare const BAKED_KEY_SENTINEL = "\0webskill:baked-api-key";
97
104
  * @experimental
98
105
  */
99
106
  type BakedSecretLoader = () => Promise<Readonly<Record<string, string>>>;
100
- /** 上次生效的缺省值快照仓;存哪儿由宿主决定 @experimental */
107
+ /**
108
+ * 上次生效的缺省值快照仓;存哪儿由宿主决定。
109
+ *
110
+ * 快照除了 `WebSkillConfigDefaults` 还可能带内部同步状态(0.27.1),
111
+ * 因此宿主必须**整对象存取**,不能只挑已知键。
112
+ * @experimental
113
+ */
101
114
  interface ConfigDefaultsSnapshotStore {
102
115
  read(): Promise<WebSkillConfigDefaults | undefined> | WebSkillConfigDefaults | undefined;
103
116
  write(snapshot: WebSkillConfigDefaults): Promise<void> | void;
package/dist/config.js CHANGED
@@ -57,6 +57,7 @@ function isAllowedSite(url, allowedHosts) {
57
57
 
58
58
  //#endregion
59
59
  //#region src/config/merge.ts
60
+ const BAKED_SYNC_KEY = "__webskillBakedSync";
60
61
  function isPlainObject(value) {
61
62
  return typeof value === "object" && value !== null && !Array.isArray(value);
62
63
  }
@@ -91,28 +92,125 @@ function mergeLeaf(stored, defaultValue, lastApplied) {
91
92
  if (lastApplied !== void 0 && sameValue(stored, lastApplied)) return defaultValue;
92
93
  return stored;
93
94
  }
95
+ /** 模型列表内容的摘要;只用于判断「与上次记录相比有没有变」,不是加密承诺 */
96
+ function fingerprintEntries(value) {
97
+ let text;
98
+ try {
99
+ text = JSON.stringify(value ?? []) ?? "null";
100
+ } catch {
101
+ return "unserializable";
102
+ }
103
+ let h1 = 2166136261;
104
+ let h2 = 16777619;
105
+ for (let i = 0; i < text.length; i += 1) {
106
+ const code = text.charCodeAt(i);
107
+ h1 = Math.imul(h1 ^ code, 16777619) >>> 0;
108
+ h2 = Math.imul(h2 ^ code, 2246822507) >>> 0;
109
+ }
110
+ return `${text.length}:${h1.toString(16)}:${h2.toString(16)}`;
111
+ }
112
+ /** 读快照里的内部状态;形状非法时当作没有(旧快照 / 手改过的存储) */
113
+ function readBakedSync(snapshot) {
114
+ if (snapshot === void 0) return void 0;
115
+ const value = snapshot[BAKED_SYNC_KEY];
116
+ if (!isPlainObject(value)) return void 0;
117
+ const mode = value["mode"];
118
+ const fingerprint = value["fingerprint"];
119
+ if ((mode === "baked" || mode === "repair" || mode === "user") && typeof fingerprint === "string") return {
120
+ mode,
121
+ fingerprint
122
+ };
123
+ }
124
+ /**
125
+ * 旧快照的一次性修复:只补烘焙条目里存储缺失的键,不覆盖已有值。
126
+ *
127
+ * 旧快照分不出「上一次下发留下的旧条目」与「用户主动清空过一个字段」,
128
+ * 所以只能补缺失:`contextWindow`、`capabilities.image` 这类新字段能回来,
129
+ * 用户改过的 `baseUrl` / `apiKey` / `image: false` 一个字不动。
130
+ */
131
+ function fillMissingValue(current, fallback) {
132
+ if (current === void 0) return fallback;
133
+ if (isPlainObject(current) && isPlainObject(fallback)) {
134
+ const out = { ...current };
135
+ for (const key of Object.keys(fallback)) {
136
+ if (key === "apiKey") continue;
137
+ const value = fillMissingValue(current[key], fallback[key]);
138
+ if (value !== void 0) out[key] = value;
139
+ }
140
+ return out;
141
+ }
142
+ return current;
143
+ }
144
+ function repairBakedEntries(entries, baked) {
145
+ const byId = new Map(baked.map((entry) => [entry.id, entry]));
146
+ return entries.map((entry) => {
147
+ const source = byId.get(entry.id);
148
+ return source === void 0 ? entry : fillMissingValue(entry, source);
149
+ });
150
+ }
94
151
  /**
95
- * 烘焙模型按 id 强制同步(0.21.0 分册 24 · FR-24.1)。
152
+ * 烘焙模型按 id 强制同步(0.21.0 分册 24 · FR-24.1),并记住「下发/修复之后
153
+ * 用户存储有没有变」(0.27.1 分册 10)。
96
154
  *
97
155
  * 与 `mergeLeaf` 的「缺省值」语义不同,这里是**下发**:整片 `entries` 是一片叶子,
98
- * 用户只要动过列表任意一处,此后所有重新打包的模型配置都到不了他那里。
156
+ * 不做字段级合并,以免造出「一半新一半旧」的端点。
99
157
  * 所以烘焙条目按 id 整条覆盖、按 defaults 的顺序在前,用户自建的按原相对序在末尾。
100
158
  *
101
- * 但下发只在**运维真的重新打包过模型配置时**发生(FR-24.1d):烘焙列表与快照逐字
159
+ * 下发只在**运维真的重新打包过模型配置时**发生(FR-24.1d):烘焙列表与快照逐字
102
160
  * 相同,说明这一版没人改过 `config.json`,此时用户在 Console 里的改动与排序全部作数。
103
161
  * 快照缺席(全新安装或重装扩展)按「变了」处理——那正是「恢复初始化配置」的时刻。
104
162
  *
105
163
  * 读的是 `stored` 原始值而不是合并后的那份:合并后的 `entries` 已经被叶子规则
106
164
  * 选过边,从它数不准「用户自建了哪些」。
165
+ *
166
+ * 0.27.1 之前,下发结果没有落进 `stored`,快照却在第一次加载后变成了新烘焙列表;
167
+ * 第二次加载因此误判「用户没动过」的分支为「用户胜出」,把刚下发的字段丢掉。
168
+ * 内部状态记录了下发那一刻 `stored` 的指纹:指纹没变就继续下发/修复,
169
+ * 变了才是用户真的动过。
107
170
  */
108
- function syncBakedEntries(stored, baked, previous, retired) {
171
+ function syncBakedEntries(stored, baked, previous, lastApplied, retired) {
172
+ const raw = Array.isArray(stored) ? stored : [];
173
+ const fingerprint = fingerprintEntries(raw);
174
+ const state = readBakedSync(lastApplied);
175
+ const bakedChanged = previous === void 0 || !sameValue(previous, baked);
176
+ const kept = raw.filter((entry) => isPlainObject(entry) && typeof entry["id"] === "string" && !retired.has(entry["id"]));
109
177
  const bakedIds = new Set(baked.map((entry) => entry.id));
110
- const kept = Array.isArray(stored) ? stored.filter((entry) => isPlainObject(entry) && typeof entry["id"] === "string" && !retired.has(entry["id"])) : [];
111
- if (previous !== void 0 && sameValue(previous, baked)) {
112
- const storedIds = new Set(kept.map((entry) => entry.id));
113
- return [...kept, ...baked.filter((entry) => !storedIds.has(entry.id))];
114
- }
115
- return [...baked, ...kept.filter((entry) => !bakedIds.has(entry.id))];
178
+ const keptIds = new Set(kept.map((entry) => entry.id));
179
+ const bakedFirst = [...baked, ...kept.filter((entry) => !bakedIds.has(entry.id))];
180
+ const storedFirst = [...kept, ...baked.filter((entry) => !keptIds.has(entry.id))];
181
+ if (bakedChanged) return {
182
+ entries: bakedFirst,
183
+ state: {
184
+ mode: "baked",
185
+ fingerprint
186
+ }
187
+ };
188
+ if (state === void 0) return {
189
+ entries: repairBakedEntries(storedFirst, baked),
190
+ state: {
191
+ mode: "repair",
192
+ fingerprint
193
+ }
194
+ };
195
+ if (state.fingerprint !== fingerprint) return {
196
+ entries: storedFirst,
197
+ state: {
198
+ mode: "user",
199
+ fingerprint
200
+ }
201
+ };
202
+ if (state.mode === "baked") return {
203
+ entries: bakedFirst,
204
+ state
205
+ };
206
+ if (state.mode === "repair") return {
207
+ entries: repairBakedEntries(storedFirst, baked),
208
+ state
209
+ };
210
+ return {
211
+ entries: storedFirst,
212
+ state
213
+ };
116
214
  }
117
215
  /**
118
216
  * 上一版烘过、这一版不再烘的 id(FR-24.5)。
@@ -133,30 +231,40 @@ function resolveDefaultId(entries, current) {
133
231
  /** `llm.entries` 之外的一切仍走 0.20.0 的逐叶子规则 */
134
232
  function applyBakedModels(merged, stored, defaults, lastApplied) {
135
233
  const baked = defaults.llm?.entries;
136
- if (baked === void 0 || !isPlainObject(merged)) return merged;
234
+ if (baked === void 0 || !isPlainObject(merged)) return { merged };
137
235
  const storedLlm = isPlainObject(stored) ? stored["llm"] : void 0;
138
- const entries = syncBakedEntries(isPlainObject(storedLlm) ? storedLlm["entries"] : void 0, baked, lastApplied?.llm?.entries, retiredBakedIds(lastApplied, baked));
236
+ const sync = syncBakedEntries(isPlainObject(storedLlm) ? storedLlm["entries"] : void 0, baked, lastApplied?.llm?.entries, lastApplied, retiredBakedIds(lastApplied, baked));
139
237
  const llm = isPlainObject(merged["llm"]) ? merged["llm"] : {};
140
- const defaultId = resolveDefaultId(entries, llm["defaultId"]);
238
+ const defaultId = resolveDefaultId(sync.entries, llm["defaultId"]);
141
239
  return {
142
- ...merged,
143
- llm: {
144
- ...llm,
145
- entries,
146
- ...defaultId === void 0 ? {} : { defaultId }
147
- }
240
+ merged: {
241
+ ...merged,
242
+ llm: {
243
+ ...llm,
244
+ entries: sync.entries,
245
+ ...defaultId === void 0 ? {} : { defaultId }
246
+ }
247
+ },
248
+ state: sync.state
148
249
  };
149
250
  }
150
251
  /**
151
252
  * `lastApplied` 缺席时每条分支都退化成「stored 存在就取 stored」,
152
253
  * 即老实现的 `deepMerge(defaults, stored)`——修订跟随是纯增量,不改既有语义。
254
+ *
255
+ * 快照里的内部同步状态只在有烘焙模型时出现;它不是 `RuntimeConfig` 字段,
256
+ * 宿主照旧整存整取快照即可。
153
257
  * @experimental
154
258
  */
155
259
  function applyConfigDefaults(input) {
156
260
  const { stored, defaults, lastApplied } = input;
261
+ const baked = applyBakedModels(stored === void 0 ? defaults : mergeLeaf(stored, defaults, lastApplied), stored, defaults, lastApplied);
157
262
  return {
158
- config: mergeRuntimeConfigDefaults(applyBakedModels(stored === void 0 ? defaults : mergeLeaf(stored, defaults, lastApplied), stored, defaults, lastApplied)),
159
- snapshot: defaults
263
+ config: mergeRuntimeConfigDefaults(baked.merged),
264
+ snapshot: baked.state === void 0 ? defaults : {
265
+ ...defaults,
266
+ [BAKED_SYNC_KEY]: baked.state
267
+ }
160
268
  };
161
269
  }
162
270
 
package/dist/index.d.ts CHANGED
@@ -340,7 +340,7 @@ interface ChatbotHostAdapter {
340
340
  /** xlsx → 行流(`createXlsxBlockReader`);不注入即 `read_document` 读不了 Excel @experimental */
341
341
  xlsxDocumentReader?: XlsxDocumentReader;
342
342
  /**
343
- * xlsx → 单元格网格(`createXlsxTableReader`)。注入后,估算超过阈值的 xlsx 附件只以画像进消息,
343
+ * xlsx → 单元格网格或可重复扫描的行流(`createXlsxTableReader`)。注入后,估算超过阈值的 xlsx 附件只以画像进消息,
344
344
  * 统计交给 `query_table` 在本机对全表计算(整张表不进上下文);不注入则两者都不发生。
345
345
  * @experimental
346
346
  */
@@ -1632,6 +1632,7 @@ declare const chatbotDictionary: {
1632
1632
  'composer.inputLabel': string;
1633
1633
  'composer.send': string;
1634
1634
  'composer.stop': string;
1635
+ 'composer.processingAttachments': string;
1635
1636
  'composer.attachments': string;
1636
1637
  'composer.addAttachment': string;
1637
1638
  'composer.removeAttachment': string;
@@ -2092,6 +2093,7 @@ declare const chatbotDictionary: {
2092
2093
  'composer.inputLabel': string;
2093
2094
  'composer.send': string;
2094
2095
  'composer.stop': string;
2096
+ 'composer.processingAttachments': string;
2095
2097
  'composer.attachments': string;
2096
2098
  'composer.addAttachment': string;
2097
2099
  'composer.removeAttachment': string;
@@ -2555,6 +2557,7 @@ declare const useT: () => TranslateFn<{
2555
2557
  'composer.inputLabel': string;
2556
2558
  'composer.send': string;
2557
2559
  'composer.stop': string;
2560
+ 'composer.processingAttachments': string;
2558
2561
  'composer.attachments': string;
2559
2562
  'composer.addAttachment': string;
2560
2563
  'composer.removeAttachment': string;
@@ -3015,6 +3018,7 @@ declare const useT: () => TranslateFn<{
3015
3018
  'composer.inputLabel': string;
3016
3019
  'composer.send': string;
3017
3020
  'composer.stop': string;
3021
+ 'composer.processingAttachments': string;
3018
3022
  'composer.attachments': string;
3019
3023
  'composer.addAttachment': string;
3020
3024
  'composer.removeAttachment': string;
@@ -3494,6 +3498,6 @@ declare function ErrorCard({ code, message, loopLimits, onOpenSettings, onDismis
3494
3498
  * Version of the published `@webskill/chatbot` package, injected at build time.
3495
3499
  * @stable
3496
3500
  */
3497
- declare const CHATBOT_VERSION = "0.24.2";
3501
+ declare const CHATBOT_VERSION = "0.25.1";
3498
3502
  //#endregion
3499
3503
  export { A2uiSurfaceHost, type A2uiSurfaceHostProps, A2uiSurfaceSnapshotHost, type A2uiSurfaceSnapshotHostProps, type AppearanceChange, CHATBOT_VERSION, type ChatAttachmentInput, type ChatAttachmentMeta, type ChatComponent, type ChatComponentCallRecord, type ChatComponentConfirmation, type ChatComponentCredentialField, type ChatComponentInvokeContext, type ChatComponentOutcome, type ChatComponentStore, type ChatComponentText, type ChatContentPart, ChatEngine, type ChatEngineOptions, type ChatEvent, type ChatInteractionRecord, type ChatLayout, type ChatMessage, type ChatRunPhase, type ChatSessionListResult, type ChatSessionMeta, type ChatTheme, type ChatThinking, type ChatTokenUsage, type ChatToolCall, Chatbot, type ChatbotConfig, type ChatbotGovernancePorts, type ChatbotHostAdapter, type ChatbotProps, ChatbotVisionDelegate, type ChatbotVisionDelegateOptions, CompositeUiBridge, type CompositeUiBridgeDeps, DEFAULT_RENDERER_CAPABILITIES, DEFAULT_RUNTIME_CONFIG_STORAGE_KEY, type DownloadableFile, type DwgHost, ErrorCard, type ErrorCardProps, type HttpMessageComponentOptions, InteractionCard, type InteractionCardProps, InterruptedBanner, type InterruptedBannerProps, type Locale, type LocalizedText, OpenUiSurfaceHost, type OpenUiSurfaceHostProps, OpenUiSurfaceSnapshotHost, type OpenUiSurfaceSnapshotHostProps, type QuickPrompt, type QuickPromptIconName, type RendererCapability, type RendererKind, type ResolvedChatLayout, ResultBlockList, type ResultBlockListProps, ResultBlocksPro, type ResultBlocksProProps, type RunSnapshot, type RuntimeAppearanceConfig, type RuntimeConfig, type RuntimeConfigStore, type RuntimeQuickPrompt, type RuntimeRendererId, type SandboxExecutorDeps, type SettingsSectionId, SkillBadges, SpecInteraction, type SpecInteractionProps, VercelPayloadPreview, type VercelPayloadPreviewProps, VercelSurfaceHost, type VercelSurfaceHostProps, VercelSurfaceSnapshotHost, type VercelSurfaceSnapshotHostProps, type VisionDelegationConsent, type VisionDelegationEndpoint, WEB_OFFICE_TEMPLATE_PREFIX, chatbotDictionary, configureA2uiMarkdown, createHttpMessageComponent, createLocalStorageRuntimeConfigStore, createMemoryRuntimeConfigStore, isLlmEntryUsable, pickUsableLlmEntry, probeA2uiAvailability, probeOpenUiAvailability, sandboxExecutorDeps, useT };
package/dist/index.js CHANGED
@@ -23,7 +23,7 @@ import { ActionBarPrimitive, AttachmentPrimitive, AuiProvider, ComposerPrimitive
23
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, SCROLL_DIRECTIONS, TodoStore, createDelegationToolSource, createDocumentToolSource, createDownloadedFileToolSource, createDwgToolSource, createPageActionToolSource, createPageImageToolSource, createPagePerceptionToolSource, createRemoteImageToolSource, createSkillGenerationToolSource, createSpreadsheetToolSource, createTabWorksetToolSource, createTableQueryToolSource, createTemplateToolSource, createTodoToolSource, profileTable, sniffDocumentFormat, withDelegationOrigin } from "@webskill/sdk/agent";
26
+ import { DownloadedFilePolicy, PAGE_ACTION_KINDS, SCROLL_DIRECTIONS, TodoStore, createDelegationToolSource, createDocumentToolSource, createDownloadedFileToolSource, createDwgToolSource, createPageActionToolSource, createPageImageToolSource, createPagePerceptionToolSource, createRemoteImageToolSource, createSkillGenerationToolSource, createSpreadsheetToolSource, createTabWorksetToolSource, createTableQueryToolSource, createTemplateToolSource, createTodoToolSource, isStreamingTableWorkbook, profileStreamingTable, profileTable, sniffDocumentFormat, summarizeStreamingWorkbook, 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";
@@ -25916,6 +25916,7 @@ const chatbotDictionary = defineDictionary({
25916
25916
  "composer.inputLabel": "Message input",
25917
25917
  "composer.send": "Send",
25918
25918
  "composer.stop": "Stop",
25919
+ "composer.processingAttachments": "Processing attachments on this device. Large files may take a while; please keep this chat open…",
25919
25920
  "composer.attachments": "Attachments",
25920
25921
  "composer.addAttachment": "Attach a file",
25921
25922
  "composer.removeAttachment": "Remove attachment: {name}",
@@ -26376,6 +26377,7 @@ const chatbotDictionary = defineDictionary({
26376
26377
  "composer.inputLabel": "消息输入框",
26377
26378
  "composer.send": "发送",
26378
26379
  "composer.stop": "停止",
26380
+ "composer.processingAttachments": "正在本机处理附件,大文件可能需要一些时间,请保持对话打开…",
26379
26381
  "composer.attachments": "附件",
26380
26382
  "composer.addAttachment": "添加附件",
26381
26383
  "composer.removeAttachment": "移除附件:{name}",
@@ -27918,7 +27920,7 @@ function smallTableHint(id) {
27918
27920
  * 已解析的表按「会话 + 来源」缓存(FR-10.17)。
27919
27921
  *
27920
27922
  * 缓存的是进行中的 promise:分流时解析一次,紧接着的查询复用同一份;失败不留。
27921
- * 容量 2、最近使用淘汰——一张 2 万 × 12 的网格在堆里是十余 MB,
27923
+ * 容量 2、最近使用淘汰——网格工作簿会占堆内存,流式工作簿只缓存元数据与取件出口,
27922
27924
  * 留两格是为「同一会话里先后传两份表做对比」。
27923
27925
  */
27924
27926
  var TableCache = class {
@@ -29474,33 +29476,56 @@ var ChatEngine = class {
29474
29476
  if (extract === void 0) return void 0;
29475
29477
  return await extract(bytes);
29476
29478
  }
29479
+ async #readTable(reader, path) {
29480
+ if (reader.readStreaming !== void 0) {
29481
+ const storage = this.#adapter.storage;
29482
+ if (storage.createReadStream !== void 0) return await reader.readStreaming(() => storage.createReadStream(path));
29483
+ return await reader.readStreaming(await storage.readBinary(path));
29484
+ }
29485
+ return await reader.read(await this.#adapter.storage.readBinary(path));
29486
+ }
29477
29487
  /**
29478
29488
  * xlsx 附件的发送形态分流(0.26.0 分册 10 · FR-10.1 / FR-10.5)。
29479
29489
  *
29480
29490
  * 返回 `undefined` = 不适用(没接端口、不是 xlsx、解析失败),调用方走 0.25.0 的原路径——
29481
29491
  * 解析失败时同一份坏文件会在原路径里以既有的形态报错,这里不新增失败形态。
29482
- * 解析在这里先做一次、网格进缓存,随后的 `query_table` 复用同一份。
29492
+ * 解析在这里先做一次、工作簿进缓存,随后的 `query_table` 复用同一份。
29483
29493
  */
29484
29494
  async #tableRouting(attachment, path, sessionId) {
29485
29495
  const reader = this.#adapter.xlsxTableReader;
29486
29496
  if (reader === void 0 || attachment.kind !== "document-text") return void 0;
29487
29497
  if (extractedDocumentFormat(attachment.contentType, attachment.name) !== "xlsx") return void 0;
29488
- const workbook = await this.#tables.get(sessionId, `att:${attachment.path}`, async () => reader.read(await this.#adapter.storage.readBinary(path))).catch(() => void 0);
29498
+ const workbook = await this.#tables.get(sessionId, `att:${attachment.path}`, async () => this.#readTable(reader, path)).catch(() => void 0);
29489
29499
  if (workbook === void 0) return void 0;
29490
- const tokens = estimateTableTokens(workbook);
29500
+ let tokens;
29501
+ let rows;
29502
+ let profile;
29503
+ if (isStreamingTableWorkbook(workbook)) {
29504
+ const summary = await summarizeStreamingWorkbook(workbook);
29505
+ tokens = summary.tokens;
29506
+ rows = summary.dataRows;
29507
+ profile = () => profileStreamingTable({
29508
+ name: attachment.name,
29509
+ id: attachment.id,
29510
+ workbook
29511
+ });
29512
+ } else {
29513
+ tokens = estimateTableTokens(workbook);
29514
+ rows = countDataRows(workbook);
29515
+ profile = () => Promise.resolve(profileTable({
29516
+ name: attachment.name,
29517
+ id: attachment.id,
29518
+ workbook
29519
+ }));
29520
+ }
29491
29521
  if (tokens <= 24e3) return {
29492
29522
  kind: "inline",
29493
29523
  hint: smallTableHint(attachment.id)
29494
29524
  };
29495
- const rows = countDataRows(workbook);
29496
29525
  const locale = this.#liveConfig?.appearance.locale ?? "en";
29497
29526
  return {
29498
29527
  kind: "profile",
29499
- profile: profileTable({
29500
- name: attachment.name,
29501
- id: attachment.id,
29502
- workbook
29503
- }),
29528
+ profile: await profile(),
29504
29529
  steering: tableQuerySteering({
29505
29530
  id: attachment.id,
29506
29531
  tokens,
@@ -29523,8 +29548,22 @@ var ChatEngine = class {
29523
29548
  async #downloadSheetRouting(input) {
29524
29549
  const reader = this.#adapter.xlsxTableReader;
29525
29550
  if (reader === void 0) return void 0;
29526
- const workbook = await this.#tables.get(this.#currentSessionId ?? "", `dl:${input.entry.id}`, () => reader.read(input.bytes)).catch(() => void 0);
29551
+ const workbook = await this.#tables.get(this.#currentSessionId ?? "", `dl:${input.entry.id}`, () => reader.readStreaming !== void 0 ? reader.readStreaming(input.bytes) : reader.read(input.bytes)).catch(() => void 0);
29527
29552
  if (workbook === void 0) return void 0;
29553
+ if (isStreamingTableWorkbook(workbook)) {
29554
+ const summary = await summarizeStreamingWorkbook(workbook);
29555
+ if (summary.tokens <= 24e3 && summary.textChars <= ATTACHMENT_TEXT_LIMIT) return void 0;
29556
+ return `${await profileStreamingTable({
29557
+ name: input.entry.name,
29558
+ id: input.entry.id,
29559
+ workbook
29560
+ })}\n\n` + tableQuerySteering({
29561
+ id: input.entry.id,
29562
+ tokens: summary.tokens,
29563
+ rows: summary.dataRows,
29564
+ download: true
29565
+ });
29566
+ }
29528
29567
  const tokens = estimateTableTokens(workbook);
29529
29568
  if (tokens <= 24e3 && tableTextChars(workbook) <= ATTACHMENT_TEXT_LIMIT) return void 0;
29530
29569
  return `${profileTable({
@@ -29554,7 +29593,7 @@ var ChatEngine = class {
29554
29593
  const notXlsx = (name) => new WebSkillError("TOOL_UNSUPPORTED", `"${name}" is not an Excel (.xlsx) workbook; query_table reads .xlsx only.`);
29555
29594
  const fromAttachment = async (a) => {
29556
29595
  if (a.kind !== "document-text" || extractedDocumentFormat(a.contentType, a.name) !== "xlsx") throw notXlsx(a.name);
29557
- const workbook = await this.#tables.get(sessionId, `att:${a.path}`, async () => reader.read(await this.#adapter.storage.readBinary(`${this.attachmentsRoot}/${a.path}`)));
29596
+ const workbook = await this.#tables.get(sessionId, `att:${a.path}`, async () => this.#readTable(reader, `${this.attachmentsRoot}/${a.path}`));
29558
29597
  return {
29559
29598
  name: a.name,
29560
29599
  workbook
@@ -29575,7 +29614,7 @@ var ChatEngine = class {
29575
29614
  if (sniffDocumentFormat(bytes, name) !== "xlsx") throw notXlsx(name);
29576
29615
  return {
29577
29616
  name,
29578
- workbook: await this.#tables.get(sessionId, source, () => reader.read(bytes))
29617
+ workbook: await this.#tables.get(sessionId, source, () => reader.readStreaming !== void 0 ? reader.readStreaming(bytes) : reader.read(bytes))
29579
29618
  };
29580
29619
  };
29581
29620
  const stored = context?.binaries?.get(id);
@@ -30180,6 +30219,45 @@ var ChatEngine = class {
30180
30219
  */
30181
30220
  const hasTemplateEngine = Boolean(this.#adapter.xlsxTemplateEngine || this.#adapter.docxTemplateEngine);
30182
30221
  const templateIdHints = [...this.#adapter.webOfficeTemplateSource ? [`For a document open in an online office viewer on the page, 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".`] : []];
30222
+ /**
30223
+ * 页面取像预算(0.27.1 分册 11)。
30224
+ *
30225
+ * 普通读页(`requested: false`)只认主模型自身能看图;显式 `images: true`
30226
+ * 才允许用可用的视觉委派兜底。两条路径共享同一份配置读取,避免数值漂移。
30227
+ */
30228
+ const pageImageBudget = async (requested) => {
30229
+ const live = await this.#runtimeConfigStore()?.load();
30230
+ const maxImageBytes = live?.multimodal.maxImageBytes ?? 0;
30231
+ const maxImages = live?.multimodal.maxImagesPerMessage ?? 0;
30232
+ const minImageArea = live?.multimodal.minImageArea ?? 0;
30233
+ if (!(live?.multimodal.pageImageCapture ?? false)) return {
30234
+ enabled: false,
30235
+ maxImageBytes,
30236
+ maxImages,
30237
+ minImageArea,
30238
+ unavailableReason: "Page image capture is turned off in the host settings."
30239
+ };
30240
+ if (entryCapabilities(pickLlmEntry(live, this.#selectedModelId)).image) return {
30241
+ enabled: true,
30242
+ maxImageBytes,
30243
+ maxImages,
30244
+ minImageArea
30245
+ };
30246
+ if (!requested) return {
30247
+ enabled: false,
30248
+ maxImageBytes,
30249
+ maxImages,
30250
+ minImageArea
30251
+ };
30252
+ const delegated = await this.#visionDelegate?.applies() === true;
30253
+ return {
30254
+ enabled: delegated,
30255
+ maxImageBytes,
30256
+ maxImages,
30257
+ minImageArea,
30258
+ ...delegated ? {} : { unavailableReason: "No image-capable model is configured. Select a multimodal model or add one in Console." }
30259
+ };
30260
+ };
30183
30261
  const baseTools = [
30184
30262
  ...pageSkillSource ? [pageSkillSource] : [],
30185
30263
  createSpreadsheetToolSource({
@@ -30210,15 +30288,8 @@ var ChatEngine = class {
30210
30288
  ...pagePerception ? [createPagePerceptionToolSource({
30211
30289
  policy: pagePerception,
30212
30290
  ...this.#adapter.pagePerceptionPaging === void 0 ? {} : { paging: () => this.#adapter.pagePerceptionPaging },
30213
- imageCapture: async () => {
30214
- const live = await this.#runtimeConfigStore()?.load();
30215
- return {
30216
- enabled: (live?.multimodal.pageImageCapture ?? false) && entryCapabilities(pickLlmEntry(live, this.#selectedModelId)).image,
30217
- maxImageBytes: live?.multimodal.maxImageBytes ?? 0,
30218
- maxImages: live?.multimodal.maxImagesPerMessage ?? 0,
30219
- minImageArea: live?.multimodal.minImageArea ?? 0
30220
- };
30221
- }
30291
+ imageCapture: () => pageImageBudget(false),
30292
+ imageCaptureForRequest: () => pageImageBudget(true)
30222
30293
  })] : [],
30223
30294
  ...this.#adapter.pageActions ? [createPageActionToolSource({ policy: this.#adapter.pageActions })] : [],
30224
30295
  ...this.#adapter.pageImage ? [createPageImageToolSource({ capture: (input, context) => this.#capturePageImage(this.#adapter.pageImage, artifactStore, input, context?.runId) })] : [],
@@ -30234,7 +30305,16 @@ var ChatEngine = class {
30234
30305
  }),
30235
30306
  ...this.#adapter.xlsxTableReader ? [createTableQueryToolSource({
30236
30307
  tables: { load: (id, context) => this.#loadTable(id, context, downloadPolicy) },
30237
- downloadedFiles: () => downloadPolicy?.enabled === true
30308
+ downloadedFiles: () => downloadPolicy?.enabled === true,
30309
+ writeArtifact: (path, content, mimeType, context) => {
30310
+ if (!context?.runId) throw new WebSkillError("TOOL_EXECUTION_FAILED", "Writing a query result requires an active run.");
30311
+ return artifactStore.createBinaryArtifact({
30312
+ runId: context.runId,
30313
+ path,
30314
+ content,
30315
+ mimeType
30316
+ });
30317
+ }
30238
30318
  })] : [],
30239
30319
  ...this.#adapter.xlsxTemplateEngine || this.#adapter.docxTemplateEngine ? [createTemplateToolSource({
30240
30320
  files: { read: readTemplateBytes },
@@ -32410,7 +32490,7 @@ function ModelMenuButton({ models }) {
32410
32490
  }, option.id))
32411
32491
  })] });
32412
32492
  }
32413
- function AssistantUiComposer({ running, disabled = false, waitTarget, placeholder, attachments = false, attachmentInfo, attachmentImages, centered = false, models, noModel, dictationLang, onDictationPermissionDenied, onCameraPermissionDenied, capabilities }) {
32493
+ function AssistantUiComposer({ running, disabled = false, processingAttachments = false, waitTarget, placeholder, attachments = false, attachmentInfo, attachmentImages, centered = false, models, noModel, dictationLang, onDictationPermissionDenied, onCameraPermissionDenied, capabilities }) {
32414
32494
  const t = useT();
32415
32495
  const locale = useLocale();
32416
32496
  const dictation = useComposerDictation(dictationLang ?? DEFAULT_DICTATION_LANG[locale], onDictationPermissionDenied);
@@ -32427,11 +32507,20 @@ function AssistantUiComposer({ running, disabled = false, waitTarget, placeholde
32427
32507
  const cameraReason = !attachments ? t("composer.camera.noStorage") : attachmentReason ?? (camera.availability.available ? void 0 : t(`composer.camera.${camera.availability.reason}`));
32428
32508
  const cameraFailure = camera.failure === void 0 ? void 0 : camera.failure === "denied" && onCameraPermissionDenied !== void 0 ? t("composer.camera.denied.handoff") : t(`composer.camera.${camera.failure}`);
32429
32509
  const plainChatHintId = useId();
32430
- return /* @__PURE__ */ jsx(ComposerPrimitive.Root, {
32510
+ return /* @__PURE__ */ jsxs(ComposerPrimitive.Root, {
32431
32511
  "data-testid": "assistant-ui-composer",
32432
32512
  "data-centered": centered,
32433
32513
  className: "webskill-aui-composer shrink-0 px-4 py-4",
32434
- children: /* @__PURE__ */ jsxs(ComposerPrimitive.AttachmentDropzone, {
32514
+ children: [processingAttachments ? /* @__PURE__ */ jsxs("div", {
32515
+ "data-testid": "attachment-processing",
32516
+ role: "status",
32517
+ "aria-live": "polite",
32518
+ className: "webskill-chat-prose mx-auto flex w-full items-center gap-2 px-3 pb-2 text-xs text-muted",
32519
+ children: [/* @__PURE__ */ jsx(LoaderCircle, {
32520
+ className: "size-3.5 shrink-0 animate-spin motion-reduce:animate-none",
32521
+ "aria-hidden": true
32522
+ }), /* @__PURE__ */ jsx("span", { children: t("composer.processingAttachments") })]
32523
+ }) : null, /* @__PURE__ */ jsxs(ComposerPrimitive.AttachmentDropzone, {
32435
32524
  disabled: !attachments,
32436
32525
  className: "webskill-aui-composer-shell webskill-chat-prose mx-auto flex w-full flex-col gap-2 px-3 py-2 transition-colors data-[dragging=true]:border-ring",
32437
32526
  children: [
@@ -32657,7 +32746,7 @@ function AssistantUiComposer({ running, disabled = false, waitTarget, placeholde
32657
32746
  onCancel: camera.cancel
32658
32747
  })
32659
32748
  ]
32660
- })
32749
+ })]
32661
32750
  });
32662
32751
  }
32663
32752
 
@@ -42948,6 +43037,22 @@ function fallbackReason(error) {
42948
43037
  const code = error?.code;
42949
43038
  return typeof code === "string" && code !== "" ? code : "a2ui-render-failed";
42950
43039
  }
43040
+ function formControlValues(root) {
43041
+ const values = {};
43042
+ const visit = (node) => {
43043
+ for (const child of node.children) {
43044
+ if (child instanceof HTMLInputElement) {
43045
+ if (child.id) values[child.id] = child.type === "checkbox" ? child.checked : child.type === "number" ? Number(child.value) : child.value;
43046
+ } else if (child instanceof HTMLTextAreaElement || child instanceof HTMLSelectElement) {
43047
+ if (child.id) values[child.id] = child instanceof HTMLSelectElement && child.multiple ? [...child.selectedOptions].map((option) => option.value) : child.value;
43048
+ }
43049
+ if (child.shadowRoot) visit(child.shadowRoot);
43050
+ else visit(child);
43051
+ }
43052
+ };
43053
+ if (root) visit(root.shadowRoot ?? root);
43054
+ return values;
43055
+ }
42951
43056
  /**
42952
43057
  * `a2ui` 档:catalog 声明树经 BYOC catalog 的 Lit 元素渲染。
42953
43058
  * 元素与协议里公布的 `catalog_definition` 派生自同一份 shape,因此这里
@@ -42958,6 +43063,7 @@ function fallbackReason(error) {
42958
43063
  */
42959
43064
  function A2uiSpecSurface({ spec, surfaceId, actions, onAction, registry, runId, draft, onDraftChange }) {
42960
43065
  const mountRef = useRef(null);
43066
+ const formValuesRef = useRef({});
42961
43067
  const [fallback, setFallback] = useState();
42962
43068
  const emit = useEffectEvent((actionId, value) => {
42963
43069
  const declared = (actions ?? []).find((action) => action.id === actionId);
@@ -42987,8 +43093,11 @@ function A2uiSpecSurface({ spec, surfaceId, actions, onAction, registry, runId,
42987
43093
  const processor = new MessageProcessor([catalog, lit.basicCatalog], (action) => {
42988
43094
  const decoded = fromA2uiSpecAction(action);
42989
43095
  if (!decoded) return;
42990
- const values = processor.getClientDataModel("v0.9.1")?.surfaces?.[a2uiSurfaceId]?.[A2UI_SPEC_FORM_PATH.slice(1)];
42991
- emit(decoded.actionId, values ?? {});
43096
+ const values = {
43097
+ ...processor.getClientDataModel("v0.9.1")?.surfaces?.[a2uiSurfaceId]?.[A2UI_SPEC_FORM_PATH.slice(1)],
43098
+ ...formControlValues(surfaceElement)
43099
+ };
43100
+ emit(decoded.actionId, values);
42992
43101
  }, { version: "v0.9.1" });
42993
43102
  processor.onSurfaceCreated((surface) => {
42994
43103
  if (disposed) return;
@@ -42997,6 +43106,21 @@ function A2uiSpecSurface({ spec, surfaceId, actions, onAction, registry, runId,
42997
43106
  mount.replaceChildren(surfaceElement);
42998
43107
  });
42999
43108
  processor.processMessages(toA2uiSpecMessages(uiCatalog, spec, { surfaceId: a2uiSurfaceId }));
43109
+ const previousValues = formValuesRef.current;
43110
+ if (Object.keys(previousValues).length > 0) {
43111
+ const defaults = processor.getClientDataModel("v0.9.1")?.surfaces?.[a2uiSurfaceId]?.[A2UI_SPEC_FORM_PATH.slice(1)];
43112
+ processor.processMessages([{
43113
+ version: "v0.9.1",
43114
+ updateDataModel: {
43115
+ surfaceId: a2uiSurfaceId,
43116
+ path: A2UI_SPEC_FORM_PATH,
43117
+ value: {
43118
+ ...defaults,
43119
+ ...previousValues
43120
+ }
43121
+ }
43122
+ }]);
43123
+ }
43000
43124
  } catch (error) {
43001
43125
  reportA2uiFallback(surfaceId, "spec", error);
43002
43126
  if (!disposed) setFallback(fallbackReason(error));
@@ -43004,6 +43128,8 @@ function A2uiSpecSurface({ spec, surfaceId, actions, onAction, registry, runId,
43004
43128
  })();
43005
43129
  return () => {
43006
43130
  disposed = true;
43131
+ const values = surfaceElement?.surface?.dataModel.get(A2UI_SPEC_FORM_PATH);
43132
+ if (typeof values === "object" && values !== null && !Array.isArray(values)) formValuesRef.current = values;
43007
43133
  surfaceElement?.remove();
43008
43134
  };
43009
43135
  }, [spec, surfaceId]);
@@ -46021,6 +46147,7 @@ function Chatbot({ adapter, config, locale: localeProp, theme: themeProp, render
46021
46147
  const [loadingEarlierMessages, setLoadingEarlierMessages] = useState(false);
46022
46148
  const [currentSessionId, setCurrentSessionId] = useState();
46023
46149
  const [sending, setSending] = useState(false);
46150
+ const [processingAttachments, setProcessingAttachments] = useState(false);
46024
46151
  /** 这一条排在同会话的上一条后面(FR-20.3):排队本身保护历史顺序,要改的是它不可见 */
46025
46152
  const [queuedSend, setQueuedSend] = useState(false);
46026
46153
  /**
@@ -46291,6 +46418,7 @@ function Chatbot({ adapter, config, locale: localeProp, theme: themeProp, render
46291
46418
  setError(void 0);
46292
46419
  break;
46293
46420
  case "message-complete":
46421
+ if (event.message.role === "user") setProcessingAttachments(false);
46294
46422
  setMessages((prev) => prev.some((message) => message.id === event.message.id) ? prev : [...prev, event.message]);
46295
46423
  break;
46296
46424
  case "interaction-requested":
@@ -46364,7 +46492,13 @@ function Chatbot({ adapter, config, locale: localeProp, theme: themeProp, render
46364
46492
  setNoModelGuideOpen(true);
46365
46493
  return;
46366
46494
  }
46495
+ const hasAttachments = attachments.length > 0;
46496
+ if (hasAttachments) {
46497
+ setProcessingAttachments(true);
46498
+ setSending(true);
46499
+ }
46367
46500
  (async () => {
46501
+ if (hasAttachments) await new Promise((resolve) => setTimeout(resolve, 0));
46368
46502
  await flushPendingDeletes();
46369
46503
  let sessionId = currentSessionId;
46370
46504
  if (!sessionId) {
@@ -46376,7 +46510,13 @@ function Chatbot({ adapter, config, locale: localeProp, theme: themeProp, render
46376
46510
  const needsTitle = sessionsRef.current.find((session) => session.id === sessionId)?.title === void 0;
46377
46511
  await engine.send(text, attachments);
46378
46512
  if (needsTitle) await refreshSessions();
46379
- })().catch((e) => setError({ message: toErrorMessage(e) }));
46513
+ })().catch((e) => {
46514
+ if (hasAttachments) {
46515
+ setProcessingAttachments(false);
46516
+ setSending(false);
46517
+ }
46518
+ setError({ message: toErrorMessage(e) });
46519
+ });
46380
46520
  }, [
46381
46521
  engine,
46382
46522
  currentSessionId,
@@ -46732,6 +46872,7 @@ function Chatbot({ adapter, config, locale: localeProp, theme: themeProp, render
46732
46872
  } } : {},
46733
46873
  running: busy,
46734
46874
  disabled: interactionPending,
46875
+ processingAttachments,
46735
46876
  ...interactionTarget !== void 0 ? { waitTarget: interactionTarget } : {},
46736
46877
  centered: composerCentered,
46737
46878
  attachments: attachmentsReady,
@@ -46820,7 +46961,7 @@ function VercelPayloadPreview({ bridge }) {
46820
46961
  * Version of the published `@webskill/chatbot` package, injected at build time.
46821
46962
  * @stable
46822
46963
  */
46823
- const CHATBOT_VERSION = "0.24.2";
46964
+ const CHATBOT_VERSION = "0.25.1";
46824
46965
 
46825
46966
  //#endregion
46826
46967
  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, createHttpMessageComponent, createLocalStorageRuntimeConfigStore, createMemoryRuntimeConfigStore, isLlmEntryUsable, pickUsableLlmEntry, probeA2uiAvailability, probeOpenUiAvailability, sandboxExecutorDeps, useT };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webskill/chatbot",
3
- "version": "0.24.2",
3
+ "version": "0.25.1",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -38,7 +38,7 @@
38
38
  "@a2ui/web_core": "^0.10.5",
39
39
  "@lit/context": "^1.1.0",
40
40
  "@openuidev/react-lang": "0.2.8",
41
- "@webskill/sdk": "^0.27.0",
41
+ "@webskill/sdk": "^0.28.1",
42
42
  "mermaid": "^12.0.0",
43
43
  "react": ">=19.0.0",
44
44
  "react-dom": ">=19.0.0",