@webskill/chatbot 0.16.0 → 0.18.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/chatbot.css CHANGED
@@ -388,6 +388,9 @@
388
388
  .ms-auto {
389
389
  margin-inline-start: auto;
390
390
  }
391
+ .me-1 {
392
+ margin-inline-end: var(--spacing);
393
+ }
391
394
  .mt-0\.5 {
392
395
  margin-top: calc(var(--spacing) * 0.5);
393
396
  }
@@ -708,6 +711,9 @@
708
711
  .cursor-default {
709
712
  cursor: default;
710
713
  }
714
+ .cursor-grab {
715
+ cursor: grab;
716
+ }
711
717
  .cursor-not-allowed {
712
718
  cursor: not-allowed;
713
719
  }
@@ -1032,6 +1038,12 @@
1032
1038
  .bg-muted-foreground {
1033
1039
  background-color: var(--muted-foreground);
1034
1040
  }
1041
+ .bg-muted\/40 {
1042
+ background-color: var(--muted-foreground);
1043
+ @supports (color: color-mix(in lab, red, red)) {
1044
+ background-color: color-mix(in oklab, var(--muted-foreground) 40%, transparent);
1045
+ }
1046
+ }
1035
1047
  .bg-popover {
1036
1048
  background-color: var(--popover);
1037
1049
  }
@@ -1391,6 +1403,10 @@
1391
1403
  outline-style: var(--tw-outline-style);
1392
1404
  outline-width: 1px;
1393
1405
  }
1406
+ .sepia {
1407
+ --tw-sepia: sepia(100%);
1408
+ filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
1409
+ }
1394
1410
  .filter {
1395
1411
  filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
1396
1412
  }
@@ -1703,6 +1719,9 @@
1703
1719
  --tw-outline-style: none;
1704
1720
  outline-style: none;
1705
1721
  }
1722
+ .active\:cursor-grabbing:active {
1723
+ cursor: grabbing;
1724
+ }
1706
1725
  .active\:bg-primary\/80:active {
1707
1726
  background-color: var(--primary);
1708
1727
  @supports (color: color-mix(in lab, red, red)) {
@@ -0,0 +1,107 @@
1
+ import { n as RuntimeConfig, r as RuntimeConfigStore } from "./index-Nnkn73aD.js";
2
+ import { a as parseWebSkillConfig, c as ConfigFieldSpec, i as WebSkillConfigDefaults, l as ConfigLeafKind, n as ParseWebSkillConfigOptions, o as CONFIGURED_RUNTIME_PATHS, r as ParsedWebSkillConfig, s as CONFIG_FIELDS, t as CONFIG_TOP_LEVEL_SECTIONS, u as NON_CONFIGURABLE_RUNTIME_PATHS } from "./parse-B8D0fl8I.js";
3
+ //#region src/config/errors.d.ts
4
+ /** 稳定错误码,供宿主可编程处理;`message` 一律英文 @experimental */
5
+ type WebSkillConfigErrorCode = 'CONFIG_UNKNOWN_KEY' | 'CONFIG_TYPE' | 'CONFIG_RANGE' | 'CONFIG_ENUM' | 'CONFIG_SHAPE' | 'CONFIG_REFERENCE';
6
+ /**
7
+ * 配置文件校验失败。
8
+ *
9
+ * 校验一律**硬失败**:拼错一个字段名而解析器静默忽略,得到的是「看起来生效了其实没生效」
10
+ * 的构建产物,症状出现在运行时且完全无从反推。
11
+ * @experimental
12
+ */
13
+ declare class WebSkillConfigError extends Error {
14
+ readonly code: WebSkillConfigErrorCode;
15
+ /** 出问题的位置在配置文件里的路径,如 `agentRuntime.loop.maxTurns` */
16
+ readonly path: string;
17
+ constructor(code: WebSkillConfigErrorCode, path: string, message: string);
18
+ }
19
+ //#endregion
20
+ //#region src/config/merge.d.ts
21
+ /**
22
+ * 三方合并:宿主缺省值 · 用户存储 · 上次应用过的缺省值快照。
23
+ *
24
+ * 语义是**缺省值**而不是强制覆盖:用户在 Console 里改过的项永远优先。
25
+ * 合并按层递归而非顶层 spread——SDK 后续新增字段时,老的存储里没有那个字段,
26
+ * 逐层合并才能让缺省值填进去;顶层 spread 会因为 `sandbox` 整体存在而整段落空。
27
+ *
28
+ * 数组是**替换**语义:`entries: []` 是「用户把模型删光了」,
29
+ * 与「从没配过」(键缺席)必须分得开,否则删掉的条目会自己长回来。
30
+ *
31
+ * 唯一的例外是 `llm.entries` 里的**烘焙条目**:它们是随扩展一起发的运维配置,
32
+ * 不是用户资产,走 `syncBakedEntries` 的下发语义(0.21.0 分册 24)。
33
+ */
34
+ /** 三方合并的输入 @experimental */
35
+ interface ApplyConfigDefaultsInput {
36
+ /** 用户存储里的原始值(**未**合并默认值),可缺席 */
37
+ readonly stored?: unknown;
38
+ /** `parseWebSkillConfig(...).defaults` */
39
+ readonly defaults: WebSkillConfigDefaults;
40
+ /** 上次应用过的缺省值快照,可缺席 */
41
+ readonly lastApplied?: WebSkillConfigDefaults;
42
+ }
43
+ /** 合并结果:可直接使用的配置,以及要持久化的本次缺省值快照 @experimental */
44
+ interface AppliedConfigDefaults {
45
+ readonly config: RuntimeConfig;
46
+ /** 本次应用的缺省值;调用方负责持久化,下次作为 `lastApplied` 传回 */
47
+ readonly snapshot: WebSkillConfigDefaults;
48
+ }
49
+ /**
50
+ * `lastApplied` 缺席时每条分支都退化成「stored 存在就取 stored」,
51
+ * 即老实现的 `deepMerge(defaults, stored)`——修订跟随是纯增量,不改既有语义。
52
+ * @experimental
53
+ */
54
+ declare function applyConfigDefaults(input: ApplyConfigDefaultsInput): AppliedConfigDefaults;
55
+ //#endregion
56
+ //#region src/config/store.d.ts
57
+ /**
58
+ * 把「宿主缺省值」接进宿主已有的 `RuntimeConfigStore`。
59
+ *
60
+ * 不改 `RuntimeConfigStore` 接口——那是既有公开契约。包装器额外要一条读**原始**
61
+ * 存储的通道(`readRaw`):`load()` 返回的是补齐默认值之后的完整配置,
62
+ * 从它分不出「用户显式设成 deny-all」和「用户没配、SDK 默认就是 deny-all」。
63
+ */
64
+ /**
65
+ * 存储里代替明文 apiKey 的占位串。
66
+ *
67
+ * 它让烘焙的密钥不落盘(F12 看 localStorage 看不到),但**到此为止**:
68
+ * 密钥运行时必然在内存里以明文存在,也必然明文发给模型提供方。
69
+ * 这不是安全边界,只是把可被顺手读到的面缩小一点。
70
+ * @experimental
71
+ */
72
+ declare const BAKED_KEY_SENTINEL = "\0webskill:baked-api-key";
73
+ /**
74
+ * entryId → 明文 apiKey。
75
+ *
76
+ * 解密代码由 `@webskill/chatbot/vite` 在 `secrets: 'bake'` 时**生成**,
77
+ * 因此本模块里一行 WebCrypto 都没有——`'omit'` 构建的产物里也就扫不到 `crypto.subtle`。
78
+ * 宿主直接把虚拟模块的 `loadWebSkillSecrets` 传进来即可。
79
+ * @experimental
80
+ */
81
+ type BakedSecretLoader = () => Promise<Readonly<Record<string, string>>>;
82
+ /** 上次生效的缺省值快照仓;存哪儿由宿主决定 @experimental */
83
+ interface ConfigDefaultsSnapshotStore {
84
+ read(): Promise<WebSkillConfigDefaults | undefined> | WebSkillConfigDefaults | undefined;
85
+ write(snapshot: WebSkillConfigDefaults): Promise<void> | void;
86
+ clear(): Promise<void> | void;
87
+ }
88
+ /** 包装选项 @experimental */
89
+ interface ConfigDefaultsStoreOptions {
90
+ /** `parseWebSkillConfig(...).defaults` */
91
+ readonly defaults: WebSkillConfigDefaults;
92
+ /** 读原始存储;返回 `undefined` 表示这台机器上从没存过配置 */
93
+ readRaw(): Promise<unknown> | unknown;
94
+ /** 上次应用过的缺省值快照的读写。不给就退化成「每次都按首次应用处理」 */
95
+ readonly snapshot?: ConfigDefaultsSnapshotStore;
96
+ /** 烘焙凭据的读取器,通常是 `virtual:webskill-config` 的 `loadWebSkillSecrets` */
97
+ readonly secrets?: BakedSecretLoader;
98
+ }
99
+ /**
100
+ * 把一个 `RuntimeConfigStore` 包成「读时叠缺省值、写时摘凭据」的 store。
101
+ *
102
+ * 用户改过的字段永远优先;传了 `snapshot` 时,没改过的字段会跟随重新打包后的新缺省值。
103
+ * @experimental
104
+ */
105
+ declare function createConfigDefaultsStore(store: RuntimeConfigStore, options: ConfigDefaultsStoreOptions): RuntimeConfigStore;
106
+ //#endregion
107
+ export { type AppliedConfigDefaults, type ApplyConfigDefaultsInput, BAKED_KEY_SENTINEL, type BakedSecretLoader, CONFIGURED_RUNTIME_PATHS, CONFIG_FIELDS, CONFIG_TOP_LEVEL_SECTIONS, type ConfigDefaultsSnapshotStore, type ConfigDefaultsStoreOptions, type ConfigFieldSpec, type ConfigLeafKind, NON_CONFIGURABLE_RUNTIME_PATHS, type ParseWebSkillConfigOptions, type ParsedWebSkillConfig, type WebSkillConfigDefaults, WebSkillConfigError, type WebSkillConfigErrorCode, applyConfigDefaults, createConfigDefaultsStore, parseWebSkillConfig };
package/dist/config.js ADDED
@@ -0,0 +1,200 @@
1
+ import { r as mergeRuntimeConfigDefaults } from "./types-C4dzGOPp.js";
2
+ import { a as NON_CONFIGURABLE_RUNTIME_PATHS, i as CONFIG_FIELDS, n as parseWebSkillConfig, o as WebSkillConfigError, r as CONFIGURED_RUNTIME_PATHS, t as CONFIG_TOP_LEVEL_SECTIONS } from "./parse-Bo68XTn_.js";
3
+
4
+ //#region src/config/merge.ts
5
+ function isPlainObject(value) {
6
+ return typeof value === "object" && value !== null && !Array.isArray(value);
7
+ }
8
+ /** 叶子相等判定。数组与对象都按结构比,因为叶子可以是数组(模型列表、提示词) */
9
+ function sameValue(a, b) {
10
+ if (Object.is(a, b)) return true;
11
+ if (Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((item, index) => sameValue(item, b[index]));
12
+ if (isPlainObject(a) && isPlainObject(b)) {
13
+ const keys = Object.keys(a);
14
+ if (keys.length !== Object.keys(b).length) return false;
15
+ return keys.every((key) => key in b && sameValue(a[key], b[key]));
16
+ }
17
+ return false;
18
+ }
19
+ /**
20
+ * 逐叶子决定归属。
21
+ *
22
+ * 中间那个分支是本版的**全部新增语义**,也是最容易被后人当成冗余删掉的一条:
23
+ * 存储里的值与上次应用过的缺省值逐字相等,说明用户从没碰过这一项,
24
+ * 于是它应当跟随宿主新一版的缺省值。没有这一条,宿主改了默认值,
25
+ * 所有装过旧版的用户永远停在旧默认值上,而他们自己完全不知道有这么一项。
26
+ */
27
+ function mergeLeaf(stored, defaultValue, lastApplied) {
28
+ if (defaultValue === void 0) return stored;
29
+ if (stored === void 0) return defaultValue;
30
+ if (isPlainObject(stored) && isPlainObject(defaultValue)) {
31
+ const last = isPlainObject(lastApplied) ? lastApplied : void 0;
32
+ const out = { ...stored };
33
+ for (const key of Object.keys(defaultValue)) out[key] = mergeLeaf(stored[key], defaultValue[key], last?.[key]);
34
+ return out;
35
+ }
36
+ if (lastApplied !== void 0 && sameValue(stored, lastApplied)) return defaultValue;
37
+ return stored;
38
+ }
39
+ /**
40
+ * 烘焙模型按 id 强制同步(0.21.0 分册 24 · FR-24.1)。
41
+ *
42
+ * 与 `mergeLeaf` 的「缺省值」语义不同,这里是**下发**:整片 `entries` 是一片叶子,
43
+ * 用户只要动过列表任意一处,此后所有重新打包的模型配置都到不了他那里。
44
+ * 所以烘焙条目按 id 整条覆盖、按 defaults 的顺序在前,用户自建的按原相对序在末尾。
45
+ *
46
+ * 但下发只在**运维真的重新打包过模型配置时**发生(FR-24.1d):烘焙列表与快照逐字
47
+ * 相同,说明这一版没人改过 `config.json`,此时用户在 Console 里的改动与排序全部作数。
48
+ * 快照缺席(全新安装或重装扩展)按「变了」处理——那正是「恢复初始化配置」的时刻。
49
+ *
50
+ * 读的是 `stored` 原始值而不是合并后的那份:合并后的 `entries` 已经被叶子规则
51
+ * 选过边,从它数不准「用户自建了哪些」。
52
+ */
53
+ function syncBakedEntries(stored, baked, previous, retired) {
54
+ const bakedIds = new Set(baked.map((entry) => entry.id));
55
+ const kept = Array.isArray(stored) ? stored.filter((entry) => isPlainObject(entry) && typeof entry["id"] === "string" && !retired.has(entry["id"])) : [];
56
+ if (previous !== void 0 && sameValue(previous, baked)) {
57
+ const storedIds = new Set(kept.map((entry) => entry.id));
58
+ return [...kept, ...baked.filter((entry) => !storedIds.has(entry.id))];
59
+ }
60
+ return [...baked, ...kept.filter((entry) => !bakedIds.has(entry.id))];
61
+ }
62
+ /**
63
+ * 上一版烘过、这一版不再烘的 id(FR-24.5)。
64
+ *
65
+ * 不认它们的话,运维从 `config.json` 里删掉的模型会以「用户自建」的身份
66
+ * 永久留在每台机器上。快照缺席时分辨不出来,一律保留。
67
+ */
68
+ function retiredBakedIds(lastApplied, baked) {
69
+ const previous = lastApplied?.llm?.entries ?? [];
70
+ const bakedIds = new Set(baked.map((entry) => entry.id));
71
+ return new Set(previous.map((entry) => entry.id).filter((id) => !bakedIds.has(id)));
72
+ }
73
+ /** 烘焙条目在新一版里被删掉时,用户的默认项会变成空引用——那条空引用正是本册造出来的(FR-24.4) */
74
+ function resolveDefaultId(entries, current) {
75
+ if (typeof current === "string" && entries.some((entry) => entry.id === current)) return current;
76
+ return entries[0]?.id;
77
+ }
78
+ /** `llm.entries` 之外的一切仍走 0.20.0 的逐叶子规则 */
79
+ function applyBakedModels(merged, stored, defaults, lastApplied) {
80
+ const baked = defaults.llm?.entries;
81
+ if (baked === void 0 || !isPlainObject(merged)) return merged;
82
+ const storedLlm = isPlainObject(stored) ? stored["llm"] : void 0;
83
+ const entries = syncBakedEntries(isPlainObject(storedLlm) ? storedLlm["entries"] : void 0, baked, lastApplied?.llm?.entries, retiredBakedIds(lastApplied, baked));
84
+ const llm = isPlainObject(merged["llm"]) ? merged["llm"] : {};
85
+ const defaultId = resolveDefaultId(entries, llm["defaultId"]);
86
+ return {
87
+ ...merged,
88
+ llm: {
89
+ ...llm,
90
+ entries,
91
+ ...defaultId === void 0 ? {} : { defaultId }
92
+ }
93
+ };
94
+ }
95
+ /**
96
+ * `lastApplied` 缺席时每条分支都退化成「stored 存在就取 stored」,
97
+ * 即老实现的 `deepMerge(defaults, stored)`——修订跟随是纯增量,不改既有语义。
98
+ * @experimental
99
+ */
100
+ function applyConfigDefaults(input) {
101
+ const { stored, defaults, lastApplied } = input;
102
+ return {
103
+ config: mergeRuntimeConfigDefaults(applyBakedModels(stored === void 0 ? defaults : mergeLeaf(stored, defaults, lastApplied), stored, defaults, lastApplied)),
104
+ snapshot: defaults
105
+ };
106
+ }
107
+
108
+ //#endregion
109
+ //#region src/config/store.ts
110
+ /**
111
+ * 把「宿主缺省值」接进宿主已有的 `RuntimeConfigStore`。
112
+ *
113
+ * 不改 `RuntimeConfigStore` 接口——那是既有公开契约。包装器额外要一条读**原始**
114
+ * 存储的通道(`readRaw`):`load()` 返回的是补齐默认值之后的完整配置,
115
+ * 从它分不出「用户显式设成 deny-all」和「用户没配、SDK 默认就是 deny-all」。
116
+ */
117
+ /**
118
+ * 存储里代替明文 apiKey 的占位串。
119
+ *
120
+ * 它让烘焙的密钥不落盘(F12 看 localStorage 看不到),但**到此为止**:
121
+ * 密钥运行时必然在内存里以明文存在,也必然明文发给模型提供方。
122
+ * 这不是安全边界,只是把可被顺手读到的面缩小一点。
123
+ * @experimental
124
+ */
125
+ const BAKED_KEY_SENTINEL = "\0webskill:baked-api-key";
126
+ function mapEntries(config, map) {
127
+ return {
128
+ ...config,
129
+ llm: {
130
+ ...config.llm,
131
+ entries: config.llm.entries.map(map)
132
+ }
133
+ };
134
+ }
135
+ function createSecretVault(loader) {
136
+ let cache;
137
+ async function read() {
138
+ if (cache === void 0) cache = loader === void 0 ? {} : await loader();
139
+ return cache;
140
+ }
141
+ return {
142
+ /**
143
+ * 只在 `apiKey` 缺席或等于占位串时注入:空串是用户**显式清空**,
144
+ * 再注入回去等于用户删不掉这个密钥。
145
+ */
146
+ async restore(config) {
147
+ const secrets = await read();
148
+ if (Object.keys(secrets).length === 0) return config;
149
+ return mapEntries(config, (entry) => {
150
+ const baked = secrets[entry.id];
151
+ if (baked === void 0) return entry;
152
+ if (entry.apiKey !== void 0 && entry.apiKey !== "\0webskill:baked-api-key") return entry;
153
+ return {
154
+ ...entry,
155
+ apiKey: baked
156
+ };
157
+ });
158
+ },
159
+ /** 落盘前把烘焙密钥换回占位串,明文因此不进存储 */
160
+ async strip(config) {
161
+ const secrets = await read();
162
+ if (Object.keys(secrets).length === 0) return config;
163
+ return mapEntries(config, (entry) => entry.apiKey !== void 0 && entry.apiKey === secrets[entry.id] ? {
164
+ ...entry,
165
+ apiKey: BAKED_KEY_SENTINEL
166
+ } : entry);
167
+ }
168
+ };
169
+ }
170
+ /**
171
+ * 把一个 `RuntimeConfigStore` 包成「读时叠缺省值、写时摘凭据」的 store。
172
+ *
173
+ * 用户改过的字段永远优先;传了 `snapshot` 时,没改过的字段会跟随重新打包后的新缺省值。
174
+ * @experimental
175
+ */
176
+ function createConfigDefaultsStore(store, options) {
177
+ const vault = createSecretVault(options.secrets);
178
+ return {
179
+ load: async () => {
180
+ const stored = await options.readRaw();
181
+ const lastApplied = await options.snapshot?.read();
182
+ const { config, snapshot } = applyConfigDefaults({
183
+ stored: stored ?? void 0,
184
+ defaults: options.defaults,
185
+ lastApplied
186
+ });
187
+ await options.snapshot?.write(snapshot);
188
+ return vault.restore(config);
189
+ },
190
+ save: async (config) => store.save(await vault.strip(config)),
191
+ reset: async () => {
192
+ await store.reset();
193
+ await options.snapshot?.clear();
194
+ },
195
+ ...store.subscribe ? { subscribe: (listener) => store.subscribe(listener) } : {}
196
+ };
197
+ }
198
+
199
+ //#endregion
200
+ export { BAKED_KEY_SENTINEL, CONFIGURED_RUNTIME_PATHS, CONFIG_FIELDS, CONFIG_TOP_LEVEL_SECTIONS, NON_CONFIGURABLE_RUNTIME_PATHS, WebSkillConfigError, applyConfigDefaults, createConfigDefaultsStore, parseWebSkillConfig };