@icen.ai/ui 0.5.0 → 0.8.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.
Files changed (138) hide show
  1. package/AGENTS.md +256 -0
  2. package/README.md +47 -13
  3. package/dist/base.css +31 -4
  4. package/dist/behaviors/accordion.d.mts +4 -0
  5. package/dist/behaviors/accordion.mjs +4 -3
  6. package/dist/behaviors/ai-chat.d.mts +57 -0
  7. package/dist/behaviors/ai-chat.mjs +557 -0
  8. package/dist/behaviors/ai-composer.d.mts +112 -0
  9. package/dist/behaviors/ai-composer.mjs +2032 -0
  10. package/dist/behaviors/ai-core.d.mts +151 -0
  11. package/dist/behaviors/ai-core.mjs +491 -0
  12. package/dist/behaviors/ai-diff.d.mts +53 -0
  13. package/dist/behaviors/ai-diff.mjs +282 -0
  14. package/dist/behaviors/ai-panel.d.mts +98 -0
  15. package/dist/behaviors/ai-panel.mjs +1012 -0
  16. package/dist/behaviors/ai-provider.d.mts +198 -0
  17. package/dist/behaviors/ai-provider.mjs +1055 -0
  18. package/dist/behaviors/ai-tool.d.mts +18 -0
  19. package/dist/behaviors/ai-tool.mjs +2125 -0
  20. package/dist/behaviors/ai-tools.d.mts +81 -0
  21. package/dist/behaviors/ai-tools.mjs +1658 -0
  22. package/dist/behaviors/back-top.d.mts +20 -0
  23. package/dist/behaviors/back-top.mjs +49 -32
  24. package/dist/behaviors/carousel.d.mts +4 -0
  25. package/dist/behaviors/carousel.mjs +34 -16
  26. package/dist/behaviors/charts.d.mts +250 -0
  27. package/dist/behaviors/charts.mjs +1301 -448
  28. package/dist/behaviors/command-palette.d.mts +8 -0
  29. package/dist/behaviors/command-palette.mjs +24 -12
  30. package/dist/behaviors/context-menu.d.mts +36 -0
  31. package/dist/behaviors/context-menu.mjs +6 -2
  32. package/dist/behaviors/copy.d.mts +4 -0
  33. package/dist/behaviors/copy.mjs +7 -6
  34. package/dist/behaviors/datatable.d.mts +78 -0
  35. package/dist/behaviors/datatable.mjs +54 -11
  36. package/dist/behaviors/date-picker.d.mts +4 -0
  37. package/dist/behaviors/date-picker.mjs +104 -36
  38. package/dist/behaviors/dropdown.d.mts +4 -0
  39. package/dist/behaviors/dropdown.mjs +7 -3
  40. package/dist/behaviors/input.d.mts +4 -0
  41. package/dist/behaviors/input.mjs +29 -22
  42. package/dist/behaviors/modal.d.mts +11 -0
  43. package/dist/behaviors/modal.mjs +11 -6
  44. package/dist/behaviors/nav.d.mts +7 -0
  45. package/dist/behaviors/nav.mjs +25 -9
  46. package/dist/behaviors/notification.d.mts +172 -0
  47. package/dist/behaviors/notification.mjs +243 -89
  48. package/dist/behaviors/popover.d.mts +46 -0
  49. package/dist/behaviors/popover.mjs +2 -1
  50. package/dist/behaviors/select.d.mts +4 -0
  51. package/dist/behaviors/select.mjs +131 -26
  52. package/dist/behaviors/sidebar.d.mts +4 -0
  53. package/dist/behaviors/sidebar.mjs +4 -3
  54. package/dist/behaviors/slider.d.mts +4 -0
  55. package/dist/behaviors/slider.mjs +4 -3
  56. package/dist/behaviors/split-pane.d.mts +4 -0
  57. package/dist/behaviors/split-pane.mjs +13 -13
  58. package/dist/behaviors/tabs.d.mts +8 -0
  59. package/dist/behaviors/tabs.mjs +44 -19
  60. package/dist/behaviors/tag-input.d.mts +4 -0
  61. package/dist/behaviors/tag-input.mjs +12 -3
  62. package/dist/behaviors/theme.d.mts +25 -0
  63. package/dist/behaviors/theme.mjs +3 -1
  64. package/dist/behaviors/toast.d.mts +26 -0
  65. package/dist/behaviors/toast.mjs +24 -18
  66. package/dist/behaviors/tree.d.mts +4 -0
  67. package/dist/behaviors/tree.mjs +4 -3
  68. package/dist/behaviors/upload.d.mts +4 -0
  69. package/dist/behaviors/upload.mjs +63 -26
  70. package/dist/components/ai-chat.css +1037 -0
  71. package/dist/components/ai-chat.mjs +3 -0
  72. package/dist/components/ai-composer.mjs +3 -0
  73. package/dist/components/ai-context.mjs +2 -0
  74. package/dist/components/ai-diff.css +311 -0
  75. package/dist/components/ai-diff.mjs +3 -0
  76. package/dist/components/ai-files.mjs +3 -0
  77. package/dist/components/ai-message.mjs +3 -0
  78. package/dist/components/ai-panel.css +682 -0
  79. package/dist/components/ai-reasoning.mjs +3 -0
  80. package/dist/components/ai-subagent.mjs +2 -0
  81. package/dist/components/ai-todo.mjs +2 -0
  82. package/dist/components/ai-tool-call.mjs +2 -0
  83. package/dist/components/ai-tool.css +532 -0
  84. package/dist/components/ai-usage.mjs +3 -0
  85. package/dist/components/badge.css +4 -8
  86. package/dist/components/breadcrumb.css +1 -5
  87. package/dist/components/btn.css +4 -7
  88. package/dist/components/card.css +3 -6
  89. package/dist/components/chart-bar.css +10 -0
  90. package/dist/components/chart-calendar.mjs +5 -0
  91. package/dist/components/chart-gauge.css +1 -1
  92. package/dist/components/chart-heatmap.css +40 -0
  93. package/dist/components/chart-scatter.css +24 -0
  94. package/dist/components/chart-scatter.mjs +4 -0
  95. package/dist/components/charts.css +86 -1
  96. package/dist/components/charts.mjs +1 -0
  97. package/dist/components/command-palette.css +2 -2
  98. package/dist/components/content.css +8 -9
  99. package/dist/components/copy.css +8 -8
  100. package/dist/components/datatable.css +4 -8
  101. package/dist/components/date-picker.css +3 -7
  102. package/dist/components/empty.css +0 -4
  103. package/dist/components/feedback.css +10 -17
  104. package/dist/components/form.css +13 -14
  105. package/dist/components/input.css +2 -2
  106. package/dist/components/layout.css +36 -36
  107. package/dist/components/media.css +12 -8
  108. package/dist/components/menu.css +2 -2
  109. package/dist/components/modal.css +1 -1
  110. package/dist/components/nav.css +11 -6
  111. package/dist/components/notification.css +28 -28
  112. package/dist/components/pagination.css +1 -1
  113. package/dist/components/panel.css +16 -16
  114. package/dist/components/pill.css +15 -15
  115. package/dist/components/segmented.css +5 -5
  116. package/dist/components/segmented.mjs +1 -0
  117. package/dist/components/select.css +12 -20
  118. package/dist/components/sidebar.css +1 -1
  119. package/dist/components/split-pane.css +0 -1
  120. package/dist/components/stat.css +14 -18
  121. package/dist/components/steps.css +2 -2
  122. package/dist/components/switch.css +8 -12
  123. package/dist/components/table.css +46 -47
  124. package/dist/components/tabs.css +35 -46
  125. package/dist/components/tag-input.css +4 -4
  126. package/dist/components/toast.css +16 -13
  127. package/dist/components/tooltip.css +2 -2
  128. package/dist/index.d.mts +37 -0
  129. package/dist/index.mjs +7890 -1199
  130. package/dist/registry.json +113 -4
  131. package/dist/retro-effects.css +21 -17
  132. package/dist/tokens/colors.css +4 -1
  133. package/dist/tokens/retro-effects.css +21 -17
  134. package/dist/tokens/style-profiles.css +21 -0
  135. package/dist/tokens.css +25 -1
  136. package/dist/ui.css +3168 -433
  137. package/docs/spec/ai-native.md +659 -0
  138. package/package.json +15 -5
@@ -0,0 +1,1055 @@
1
+ // src/behaviors/ai-core.ts
2
+ var svg = (body) => `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${body}</svg>`;
3
+ var ICON_TERMINAL = svg('<path d="m4 17 6-6-6-6"/><path d="M12 19h8"/>');
4
+ var ICON_FILE = svg(
5
+ '<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/>'
6
+ );
7
+ var ICON_FILE_PEN = svg(
8
+ '<path d="M12.5 22H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8l5 5v3.5"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M19 6.5 16.5 4"/><path d="M21.378 16.626a1 1 0 0 0-3.004-3.004l-4.01 4.012a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506z"/>'
9
+ );
10
+ var ICON_FILE_PLUS = svg(
11
+ '<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M9 15h6"/><path d="M12 12v6"/>'
12
+ );
13
+ var ICON_TRASH = svg(
14
+ '<path d="M3 6h18"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><path d="M10 11v6"/><path d="M14 11v6"/>'
15
+ );
16
+ var ICON_SEARCH = svg('<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>');
17
+ var ICON_FOLDER_SEARCH = svg(
18
+ '<path d="M11 20H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H20a2 2 0 0 1 2 2v4"/><circle cx="17" cy="17" r="3"/><path d="m21 21-1.9-1.9"/>'
19
+ );
20
+ var ICON_SCAN_SEARCH = svg(
21
+ '<path d="M3 7V5a2 2 0 0 1 2-2h2"/><path d="M17 3h2a2 2 0 0 1 2 2v2"/><path d="M21 17v2a2 2 0 0 1-2 2h-2"/><path d="M7 21H5a2 2 0 0 1-2-2v-2"/><circle cx="12" cy="12" r="3"/><path d="m16 16-1.9-1.9"/>'
22
+ );
23
+ var ICON_GLOBE = svg(
24
+ '<circle cx="12" cy="12" r="10"/><path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"/><path d="M2 12h20"/>'
25
+ );
26
+ var ICON_DOWNLOAD = svg(
27
+ '<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="m7 10 5 5 5-5"/><path d="M12 15V3"/>'
28
+ );
29
+ var ICON_BLOCKS = svg(
30
+ '<rect width="7" height="7" x="4" y="4" rx="1"/><rect width="7" height="7" x="13" y="4" rx="1"/><rect width="7" height="7" x="4" y="13" rx="1"/><rect width="7" height="7" x="13" y="13" rx="1"/>'
31
+ );
32
+ var ICON_ZAP = svg(
33
+ '<path d="M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z"/>'
34
+ );
35
+ var ICON_LIST = svg(
36
+ '<path d="M3 12h.01"/><path d="M3 18h.01"/><path d="M3 6h.01"/><path d="M8 12h13"/><path d="M8 18h13"/><path d="M8 6h13"/>'
37
+ );
38
+ var ICON_LIST_CHECKS = svg(
39
+ '<path d="m3 17 2 2 4-4"/><path d="m3 7 2 2 4-4"/><path d="M13 6h8"/><path d="M13 12h8"/><path d="M13 18h8"/>'
40
+ );
41
+ var ICON_BOT = svg(
42
+ '<path d="M12 8V4H8"/><rect width="16" height="12" x="4" y="8" rx="2"/><path d="M2 14h2"/><path d="M20 14h2"/><path d="M15 13v2"/><path d="M9 13v2"/>'
43
+ );
44
+ var ICON_CHART = svg(
45
+ '<path d="M3 3v18h18"/><path d="M7 15l4-6 3 4 5-8"/>'
46
+ );
47
+ var ICON_FILE_TEXT = svg(
48
+ '<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M10 9H8"/><path d="M16 13H8"/><path d="M16 17H8"/>'
49
+ );
50
+ function isObj(v) {
51
+ return typeof v === "object" && v !== null;
52
+ }
53
+ function pickStr(input, keys) {
54
+ if (!isObj(input)) return void 0;
55
+ for (const k of keys) {
56
+ const v = input[k];
57
+ if (typeof v === "string" && v) return v;
58
+ }
59
+ return void 0;
60
+ }
61
+ function pickNum(src, keys) {
62
+ if (!isObj(src)) return void 0;
63
+ for (const k of keys) {
64
+ const v = src[k];
65
+ if (typeof v === "number" && Number.isFinite(v)) return v;
66
+ }
67
+ return void 0;
68
+ }
69
+ function firstLine(s, max = 80) {
70
+ const line = s.split("\n", 1)[0]?.trim() ?? "";
71
+ return line.length > max ? `${line.slice(0, max)}\u2026` : line;
72
+ }
73
+ function filePathOf(input) {
74
+ return pickStr(input, ["file_path", "path", "file", "filename"]);
75
+ }
76
+ var summarizePath = (input) => filePathOf(input);
77
+ function summarizeRead(input) {
78
+ const p = filePathOf(input);
79
+ if (!p) return void 0;
80
+ const offset = pickNum(input, ["offset", "start_line"]);
81
+ if (offset == null) return p;
82
+ const limit = pickNum(input, ["limit", "end_line"]);
83
+ return limit != null ? `${p}:${offset}-${offset + limit}` : `${p}:${offset}`;
84
+ }
85
+ function summarizeShell(input) {
86
+ const cmd = typeof input === "string" ? input : pickStr(input, ["command", "cmd", "script"]);
87
+ return cmd ? firstLine(cmd) : void 0;
88
+ }
89
+ function summarizeGrep(input) {
90
+ const pattern = pickStr(input, ["pattern", "regex", "query"]);
91
+ const p = filePathOf(input);
92
+ if (pattern) return p ? `${firstLine(pattern, 40)} \xB7 ${p}` : firstLine(pattern);
93
+ return p;
94
+ }
95
+ var sum = (fn) => {
96
+ return (input) => fn(input) ?? "";
97
+ };
98
+ var BUILTIN_KINDS = {
99
+ shell: { label: "\u547D\u4EE4", icon: ICON_TERMINAL, tint: "accent", summarize: sum(summarizeShell) },
100
+ read: { label: "\u8BFB\u53D6", icon: ICON_FILE, tint: "info", summarize: sum(summarizeRead) },
101
+ edit: { label: "\u7F16\u8F91", icon: ICON_FILE_PEN, tint: "warning", summarize: sum(summarizePath) },
102
+ write: { label: "\u5199\u5165", icon: ICON_FILE_PLUS, tint: "warning", summarize: sum(summarizePath) },
103
+ rm: { label: "\u5220\u9664", icon: ICON_TRASH, tint: "error", summarize: sum(summarizePath) },
104
+ grep: { label: "\u641C\u7D22", icon: ICON_SEARCH, tint: "info", summarize: sum(summarizeGrep) },
105
+ glob: { label: "\u5339\u914D", icon: ICON_FOLDER_SEARCH, tint: "info", summarize: sum((i) => pickStr(i, ["pattern", "glob", "path"])) },
106
+ browser: { label: "\u6D4F\u89C8", icon: ICON_GLOBE, tint: "accent", summarize: sum((i) => pickStr(i, ["url", "uri", "target"])) },
107
+ search: { label: "\u68C0\u7D22", icon: ICON_SCAN_SEARCH, tint: "accent", summarize: sum((i) => pickStr(i, ["query", "q", "pattern"])) },
108
+ fetch: { label: "\u8BF7\u6C42", icon: ICON_DOWNLOAD, tint: "info", summarize: sum((i) => pickStr(i, ["url", "uri", "endpoint"])) },
109
+ mcp: { label: "MCP", icon: ICON_BLOCKS, tint: "muted", summarize: sum((i) => pickStr(i, ["tool", "method", "name"])) },
110
+ skill: { label: "\u6280\u80FD", icon: ICON_ZAP, tint: "accent", summarize: sum((i) => pickStr(i, ["skill", "name", "command"])) },
111
+ todo: {
112
+ label: "\u5F85\u529E",
113
+ icon: ICON_LIST,
114
+ tint: "info",
115
+ summarize: sum((input) => {
116
+ const items = input && typeof input === "object" && Array.isArray(input.items) ? input.items : Array.isArray(input) ? input : [];
117
+ const done = items.filter((it) => it?.status === "done").length;
118
+ return items.length > 0 ? `${done}/${items.length} \u5B8C\u6210` : "";
119
+ })
120
+ },
121
+ plan: { label: "\u8BA1\u5212", icon: ICON_LIST_CHECKS, tint: "info" },
122
+ subagent: { label: "\u5B50\u4EE3\u7406", icon: ICON_BOT, tint: "accent", summarize: sum((i) => pickStr(i, ["task", "description", "prompt", "message", "subagent_type"])) },
123
+ note: { label: "\u7B14\u8BB0", icon: ICON_FILE_TEXT, tint: "muted" },
124
+ chart: { label: "\u56FE\u8868", icon: ICON_CHART, tint: "info", summarize: sum((i) => pickStr(i, ["title", "name"])) }
125
+ };
126
+ var kindRegistry = new Map(Object.entries(BUILTIN_KINDS));
127
+ function str(v) {
128
+ return typeof v === "string" && v ? v : void 0;
129
+ }
130
+ function normalizeUsage(raw) {
131
+ if (!isObj(raw)) return {};
132
+ const usage = {};
133
+ const input = pickNum(raw, ["input"]) ?? pickNum(raw, ["prompt_tokens"]) ?? pickNum(raw, ["promptTokenCount"]) ?? pickNum(raw, ["input_tokens"]);
134
+ const output = pickNum(raw, ["output"]) ?? pickNum(raw, ["completion_tokens"]) ?? pickNum(raw, ["candidatesTokenCount"]) ?? pickNum(raw, ["output_tokens"]);
135
+ const cacheRead = pickNum(raw, ["cacheRead", "cache_read"]) ?? pickNum(raw, ["cache_read_input_tokens"]) ?? pickNum(raw, ["prompt_cache_hit_tokens"]) ?? pickNum(raw, ["cached_tokens"]) ?? pickNum(raw, ["cachedContentTokenCount"]) ?? (isObj(raw.prompt_tokens_details) ? pickNum(raw.prompt_tokens_details, ["cached_tokens"]) : void 0);
136
+ const cacheWrite = pickNum(raw, ["cacheWrite", "cache_write"]) ?? pickNum(raw, ["cache_creation_input_tokens"]) ?? pickNum(raw, ["cacheCreationTokenCount"]) ?? (isObj(raw.prompt_tokens_details) ? pickNum(raw.prompt_tokens_details, ["cache_write_tokens"]) : void 0);
137
+ const reasoning = pickNum(raw, ["reasoning"]) ?? pickNum(raw, ["thoughtsTokenCount"]) ?? (isObj(raw.completion_tokens_details) ? pickNum(raw.completion_tokens_details, ["reasoning_tokens"]) : void 0);
138
+ const total = pickNum(raw, ["total"]) ?? pickNum(raw, ["total_tokens"]) ?? pickNum(raw, ["totalTokenCount"]) ?? (input != null || output != null || cacheRead != null || cacheWrite != null || reasoning != null ? (input ?? 0) + (output ?? 0) + (cacheRead ?? 0) + (cacheWrite ?? 0) + (reasoning ?? 0) : void 0);
139
+ if (input != null) usage.input = input;
140
+ if (output != null) usage.output = output;
141
+ if (cacheRead != null) usage.cacheRead = cacheRead;
142
+ if (cacheWrite != null) usage.cacheWrite = cacheWrite;
143
+ if (reasoning != null) usage.reasoning = reasoning;
144
+ if (total != null) usage.total = total;
145
+ return usage;
146
+ }
147
+ var PART_TYPES = /* @__PURE__ */ new Set(["text", "image", "audio", "video", "file", "resource-link"]);
148
+ function isAiContentPart(v) {
149
+ if (!isObj(v)) return false;
150
+ const t = v.type;
151
+ if (typeof t !== "string" || !PART_TYPES.has(t)) return false;
152
+ if (t === "text") return typeof v.text === "string";
153
+ if (t === "resource-link") return typeof v.uri === "string";
154
+ return typeof v.url === "string" || typeof v.data === "string" && typeof v.mimeType === "string";
155
+ }
156
+ function aiContentUrl(part) {
157
+ if (part.type === "text" || part.type === "resource-link") return void 0;
158
+ if (typeof part.url === "string" && part.url) return part.url;
159
+ if (typeof part.data === "string" && part.data && typeof part.mimeType === "string") {
160
+ return `data:${part.mimeType};base64,${part.data}`;
161
+ }
162
+ return void 0;
163
+ }
164
+ function splitDataUri(uri) {
165
+ const m = /^data:([^;,]+);base64,(.*)$/s.exec(uri);
166
+ return m ? { mimeType: m[1] ?? "", data: m[2] ?? "" } : void 0;
167
+ }
168
+ function partFromOpenAi(item) {
169
+ const type = str(item.type) ?? "";
170
+ if (type === "text" && typeof item.text === "string") return { type: "text", text: item.text };
171
+ if (type === "image_url") {
172
+ const iu = isObj(item.image_url) ? item.image_url : void 0;
173
+ const url = iu ? str(iu.url) : void 0;
174
+ if (!url) return void 0;
175
+ const inline = splitDataUri(url);
176
+ return inline ? { type: "image", data: inline.data, mimeType: inline.mimeType } : { type: "image", url };
177
+ }
178
+ if (type === "input_audio") {
179
+ const ia = isObj(item.input_audio) ? item.input_audio : void 0;
180
+ const data = ia ? str(ia.data) : void 0;
181
+ const fmt = ia ? str(ia.format) : void 0;
182
+ if (!data) return void 0;
183
+ return { type: "audio", data, mimeType: fmt ? `audio/${fmt}` : "audio/wav" };
184
+ }
185
+ if (type === "file") {
186
+ const f = isObj(item.file) ? item.file : void 0;
187
+ const fileId = f ? str(f.file_id) ?? str(f.filename) : void 0;
188
+ if (!fileId) return void 0;
189
+ return { type: "file", url: fileId, filename: str(f?.filename) };
190
+ }
191
+ return void 0;
192
+ }
193
+ function partFromAnthropicSource(mediaType, src, fallbackMime) {
194
+ if (!isObj(src)) return void 0;
195
+ if (str(src.type) === "url") {
196
+ const url = str(src.url);
197
+ return url ? { type: mediaType, url } : void 0;
198
+ }
199
+ const data = str(src.data);
200
+ const mimeType = str(src.media_type) ?? fallbackMime;
201
+ return data ? { type: mediaType, data, mimeType } : void 0;
202
+ }
203
+ function partFromAnthropic(block) {
204
+ const type = str(block.type) ?? "";
205
+ if (type === "text" && typeof block.text === "string") return { type: "text", text: block.text };
206
+ if (type === "image") return partFromAnthropicSource("image", block.source, "image/png");
207
+ if (type === "audio") return partFromAnthropicSource("audio", block.source, "audio/wav");
208
+ if (type === "document") return partFromAnthropicSource("file", block.source, "application/pdf");
209
+ return void 0;
210
+ }
211
+ function partFromMcp(item) {
212
+ const type = str(item.type) ?? "";
213
+ if (type === "text" && typeof item.text === "string") return { type: "text", text: item.text };
214
+ if (type === "image" || type === "audio") {
215
+ const data = str(item.data);
216
+ const mimeType = str(item.mimeType) ?? (type === "image" ? "image/png" : "audio/wav");
217
+ return data ? { type, data, mimeType } : void 0;
218
+ }
219
+ if (type === "resource_link") {
220
+ const uri = str(item.uri) ?? str(item.url);
221
+ return uri ? { type: "resource-link", uri, name: str(item.name), mimeType: str(item.mimeType) } : void 0;
222
+ }
223
+ if (type === "resource") {
224
+ const res = isObj(item.resource) ? item.resource : void 0;
225
+ if (!res) return void 0;
226
+ const uri = str(res.uri);
227
+ const mimeType = str(res.mimeType) ?? "";
228
+ const text = str(res.text);
229
+ if (text) return { type: "text", text };
230
+ const blob = str(res.blob);
231
+ if (blob) {
232
+ if (mimeType.startsWith("image/")) return { type: "image", data: blob, mimeType };
233
+ if (mimeType.startsWith("audio/")) return { type: "audio", data: blob, mimeType };
234
+ if (mimeType.startsWith("video/")) return { type: "video", data: blob, mimeType };
235
+ return { type: "file", data: blob, mimeType: mimeType || "application/octet-stream", filename: uri?.split("/").pop() };
236
+ }
237
+ return uri ? { type: "resource-link", uri, mimeType: mimeType || void 0 } : void 0;
238
+ }
239
+ return void 0;
240
+ }
241
+ function normalizeContentParts(raw) {
242
+ if (typeof raw === "string") return raw ? [{ type: "text", text: raw }] : [];
243
+ if (!Array.isArray(raw)) return [];
244
+ const out = [];
245
+ for (const item of raw) {
246
+ if (isAiContentPart(item)) {
247
+ out.push(item);
248
+ continue;
249
+ }
250
+ if (!isObj(item)) continue;
251
+ const part = partFromOpenAi(item) ?? partFromAnthropic(item) ?? partFromMcp(item);
252
+ if (part) out.push(part);
253
+ }
254
+ return out;
255
+ }
256
+ function contentToText(content) {
257
+ if (typeof content === "string") return content;
258
+ return content.filter((p) => p.type === "text").map((p) => p.text).join("");
259
+ }
260
+
261
+ // src/behaviors/ai-provider.ts
262
+ function isObj2(v) {
263
+ return typeof v === "object" && v !== null;
264
+ }
265
+ function pickStr2(src, keys) {
266
+ if (!isObj2(src)) return void 0;
267
+ for (const k of keys) {
268
+ const v = src[k];
269
+ if (typeof v === "string" && v) return v;
270
+ }
271
+ return void 0;
272
+ }
273
+ function pickNum2(src, keys) {
274
+ if (!isObj2(src)) return void 0;
275
+ for (const k of keys) {
276
+ const v = src[k];
277
+ if (typeof v === "number" && Number.isFinite(v)) return v;
278
+ }
279
+ return void 0;
280
+ }
281
+ function compactNums(v) {
282
+ const out = {};
283
+ for (const [k, n] of Object.entries(v)) if (n != null) out[k] = n;
284
+ return out;
285
+ }
286
+ var idSeq = 0;
287
+ function genEntryId() {
288
+ idSeq = (idSeq + 1) % 46656;
289
+ return `ai-${Date.now().toString(36)}-${idSeq.toString(36).padStart(3, "0")}${Math.random().toString(36).slice(2, 5)}`;
290
+ }
291
+ function isAbortError(e) {
292
+ return typeof e === "object" && e !== null && e.name === "AbortError";
293
+ }
294
+ var letterBadge = (ch) => `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><circle cx="12" cy="12" r="9.4"/><text x="12" y="15.9" text-anchor="middle" font-size="10.5" font-weight="600" font-family="ui-sans-serif, system-ui, sans-serif" fill="currentColor" stroke="none">${ch}</text></svg>`;
295
+ var BUILTIN_PROVIDERS = [
296
+ {
297
+ id: "openai",
298
+ label: "OpenAI",
299
+ icon: letterBadge("O"),
300
+ baseURL: "https://api.openai.com/v1",
301
+ chatPath: "/chat/completions",
302
+ wire: "openai",
303
+ auth: "bearer",
304
+ browserDirect: "no",
305
+ // 官方禁止浏览器直连(CORS + 安全策略)
306
+ models: [
307
+ { id: "gpt-5", label: "GPT-5", context: 4e5, outputLimit: 128e3, pricing: { input: 1.25, output: 10, cacheRead: 0.125 } },
308
+ { id: "gpt-5-mini", label: "GPT-5 Mini", context: 4e5, outputLimit: 128e3, pricing: { input: 0.25, output: 2, cacheRead: 0.025 } },
309
+ { id: "gpt-5.1", label: "GPT-5.1", context: 4e5, outputLimit: 128e3, pricing: { input: 1.25, output: 10, cacheRead: 0.125 } },
310
+ { id: "gpt-5.5", label: "GPT-5.5", context: 1e6, outputLimit: 128e3, pricing: { input: 2, output: 12, cacheRead: 0.2 } }
311
+ ]
312
+ },
313
+ {
314
+ id: "claude",
315
+ label: "Claude",
316
+ icon: letterBadge("C"),
317
+ baseURL: "https://api.anthropic.com/v1",
318
+ chatPath: "/messages",
319
+ wire: "anthropic",
320
+ auth: "x-api-key",
321
+ extraHeaders: {
322
+ "anthropic-version": "2023-06-01",
323
+ "anthropic-dangerous-direct-browser-access": "true"
324
+ },
325
+ browserDirect: "yes",
326
+ // 官方支持浏览器直连(CORS 白名单)
327
+ models: [
328
+ { id: "claude-opus-5-5", label: "Claude Opus 5.5", context: 5e5, outputLimit: 64e3, pricing: { input: 4, output: 20, cacheRead: 0.4, cacheWrite: 5 } },
329
+ { id: "claude-sonnet-5-5", label: "Claude Sonnet 5.5", context: 5e5, outputLimit: 64e3, pricing: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 } },
330
+ { id: "claude-fable-5-1", label: "Claude Fable 5.1", context: 2e5, outputLimit: 64e3, pricing: { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 } },
331
+ { id: "claude-haiku-4-5", label: "Claude Haiku 4.5", context: 2e5, outputLimit: 64e3, pricing: { input: 1, output: 5, cacheRead: 0.1, cacheWrite: 1.25 } }
332
+ ]
333
+ },
334
+ {
335
+ id: "deepseek",
336
+ label: "DeepSeek",
337
+ icon: letterBadge("D"),
338
+ baseURL: "https://api.deepseek.com/v1",
339
+ chatPath: "/chat/completions",
340
+ wire: "openai",
341
+ auth: "bearer",
342
+ browserDirect: "unknown",
343
+ // 无承诺,建议代理
344
+ models: [
345
+ { id: "deepseek-flash", label: "DeepSeek Flash", context: 128e3, outputLimit: 8e3, pricing: { input: 0.14, output: 0.28, cacheRead: 0.014 } },
346
+ { id: "deepseek-v4-pro", label: "DeepSeek V4 Pro", context: 1e6, outputLimit: 128e3, pricing: { input: 0.55, output: 1.1, cacheRead: 0.055 } }
347
+ ]
348
+ },
349
+ {
350
+ id: "glm",
351
+ label: "GLM\uFF08\u667A\u8C31\uFF09",
352
+ icon: letterBadge("G"),
353
+ baseURL: "https://open.bigmodel.cn/api/paas/v4",
354
+ chatPath: "/chat/completions",
355
+ wire: "openai",
356
+ auth: "bearer",
357
+ // API key 直接当 Bearer;JWT 可选不做
358
+ browserDirect: "unknown",
359
+ models: [
360
+ { id: "glm-5.3", label: "GLM-5.3", context: 2e5, outputLimit: 128e3, pricing: { input: 0.55, output: 2.2, cacheRead: 0.055 } },
361
+ { id: "glm-5.3-flash", label: "GLM-5.3 Flash", context: 128e3, outputLimit: 64e3, pricing: { input: 0.14, output: 0.55, cacheRead: 0.014 } },
362
+ { id: "glm-5.2", label: "GLM-5.2", context: 128e3, outputLimit: 64e3, pricing: { input: 0.5, output: 3, cacheRead: 0.05 } },
363
+ { id: "glm-4.6", label: "GLM-4.6", context: 2e5, outputLimit: 128e3, pricing: { input: 0.6, output: 2.2, cacheRead: 0.06 } }
364
+ ]
365
+ },
366
+ {
367
+ id: "kimi",
368
+ label: "Kimi\uFF08\u6708\u4E4B\u6697\u9762\uFF09",
369
+ icon: letterBadge("K"),
370
+ baseURL: "https://api.moonshot.cn/v1",
371
+ chatPath: "/chat/completions",
372
+ wire: "openai",
373
+ auth: "bearer",
374
+ browserDirect: "unknown",
375
+ models: [
376
+ { id: "kimi-k3", label: "Kimi K3", context: 1e6, outputLimit: 128e3, pricing: { input: 3, output: 15, cacheRead: 0.3 } },
377
+ { id: "kimi-k2.7-code", label: "Kimi K2.7 Code", context: 256e3, outputLimit: 128e3, pricing: { input: 1.1, output: 3.3, cacheRead: 0.11 } },
378
+ { id: "kimi-k2.6", label: "Kimi K2.6", context: 256e3, outputLimit: 128e3, pricing: { input: 0.95, output: 3, cacheRead: 0.1 } }
379
+ ]
380
+ }
381
+ ];
382
+ var providerRegistry = new Map(BUILTIN_PROVIDERS.map((d) => [d.id, d]));
383
+ function registerAiProvider(def) {
384
+ if (def && typeof def.id === "string" && def.id) providerRegistry.set(def.id, def);
385
+ }
386
+ function getAiProvider(id) {
387
+ return providerRegistry.get(id);
388
+ }
389
+ function listAiProviders() {
390
+ return Array.from(providerRegistry.values());
391
+ }
392
+ function estimateCost(usage, provider, model) {
393
+ const p = getAiProvider(provider)?.models.find((m) => m.id === model)?.pricing;
394
+ if (!p) return void 0;
395
+ const u = normalizeUsage(usage);
396
+ const cost = (u.input ?? 0) / 1e6 * p.input + (u.output ?? 0) / 1e6 * p.output + (u.cacheRead ?? 0) / 1e6 * (p.cacheRead ?? p.input) + (u.cacheWrite ?? 0) / 1e6 * (p.cacheWrite ?? p.input * 1.25);
397
+ return Math.round(cost * 1e6) / 1e6;
398
+ }
399
+ var AiProviderError = class extends Error {
400
+ provider;
401
+ /** HTTP 状态码;网络层失败为 0 */
402
+ status;
403
+ /** 归一错误类型:provider 的 error.type/code,或 network / cancelled / parse / http_error */
404
+ type;
405
+ /** 原始错误体(HTTP body / 异常对象),排障用 */
406
+ raw;
407
+ constructor(init) {
408
+ super(init.message);
409
+ this.name = "AiProviderError";
410
+ this.provider = init.provider;
411
+ this.status = init.status;
412
+ this.type = init.type;
413
+ this.raw = init.raw;
414
+ }
415
+ };
416
+ function extractHttpError(raw, status) {
417
+ const e = isObj2(raw) && isObj2(raw.error) ? raw.error : isObj2(raw) ? raw : void 0;
418
+ const message = pickStr2(e, ["message", "msg", "error_description"]) ?? (typeof raw === "string" && raw ? raw : "") ?? `HTTP ${status}`;
419
+ const rawCode = e?.code;
420
+ const code = typeof rawCode === "string" && rawCode ? rawCode : typeof rawCode === "number" && Number.isFinite(rawCode) ? String(rawCode) : void 0;
421
+ const type = pickStr2(e, ["type"]) ?? code ?? "http_error";
422
+ return { type, message };
423
+ }
424
+ async function readHttpError(res, provider) {
425
+ const status = res.status;
426
+ let raw;
427
+ let text = "";
428
+ try {
429
+ text = await res.text();
430
+ } catch {
431
+ }
432
+ if (text) {
433
+ try {
434
+ raw = JSON.parse(text);
435
+ } catch {
436
+ raw = text;
437
+ }
438
+ }
439
+ const { type, message } = extractHttpError(raw, status);
440
+ return new AiProviderError({ provider, status, type, message, raw });
441
+ }
442
+ var USAGE_KEYS = ["input", "output", "cacheRead", "cacheWrite", "reasoning", "total"];
443
+ function addUsage(sum2, u) {
444
+ if (!u) return sum2;
445
+ for (const k of USAGE_KEYS) {
446
+ const v = u[k];
447
+ if (typeof v === "number" && Number.isFinite(v)) sum2[k] = (sum2[k] ?? 0) + v;
448
+ }
449
+ return sum2;
450
+ }
451
+ function isAuditEntry(v) {
452
+ return isObj2(v) && typeof v.id === "string" && typeof v.ts === "number" && typeof v.provider === "string" && typeof v.model === "string" && typeof v.baseURL === "string" && typeof v.stream === "boolean" && (v.status === "ok" || v.status === "error") && typeof v.durationMs === "number";
453
+ }
454
+ function createAiAuditor(opts) {
455
+ const max = Math.max(1, Math.floor(opts?.max ?? 100));
456
+ const persistKey = opts?.persist;
457
+ let entries = [];
458
+ const storage = () => {
459
+ try {
460
+ return typeof localStorage === "undefined" ? null : localStorage;
461
+ } catch {
462
+ return null;
463
+ }
464
+ };
465
+ if (persistKey) {
466
+ try {
467
+ const raw = storage()?.getItem(persistKey);
468
+ const parsed = raw ? JSON.parse(raw) : void 0;
469
+ if (Array.isArray(parsed)) entries = parsed.filter(isAuditEntry).slice(-max);
470
+ } catch {
471
+ }
472
+ }
473
+ const save = () => {
474
+ if (!persistKey) return;
475
+ try {
476
+ storage()?.setItem(persistKey, JSON.stringify(entries));
477
+ } catch {
478
+ }
479
+ };
480
+ return {
481
+ log(entry) {
482
+ entries.push(entry);
483
+ if (entries.length > max) entries.splice(0, entries.length - max);
484
+ save();
485
+ },
486
+ list() {
487
+ return entries.slice();
488
+ },
489
+ clear() {
490
+ entries = [];
491
+ save();
492
+ },
493
+ summary() {
494
+ const sum2 = {};
495
+ for (const e of entries) addUsage(sum2, e.usage);
496
+ return sum2;
497
+ },
498
+ totals() {
499
+ const usage = {};
500
+ let cost = 0;
501
+ let errors = 0;
502
+ let ttftSum = 0;
503
+ let ttftN = 0;
504
+ for (const e of entries) {
505
+ addUsage(usage, e.usage);
506
+ if (typeof e.cost === "number") cost += e.cost;
507
+ if (e.status === "error") errors++;
508
+ if (typeof e.ttftMs === "number") {
509
+ ttftSum += e.ttftMs;
510
+ ttftN++;
511
+ }
512
+ }
513
+ const totals = {
514
+ usage,
515
+ cost: Math.round(cost * 1e6) / 1e6,
516
+ requests: entries.length,
517
+ errors
518
+ };
519
+ if (ttftN > 0) totals.avgTtftMs = Math.round(ttftSum / ttftN);
520
+ return totals;
521
+ },
522
+ byModel() {
523
+ const out = {};
524
+ for (const e of entries) {
525
+ const key = `${e.provider}/${e.model}`;
526
+ addUsage(out[key] ??= {}, e.usage);
527
+ }
528
+ return out;
529
+ }
530
+ };
531
+ }
532
+ function parseSseFrame(raw) {
533
+ if (!raw.trim()) return void 0;
534
+ let event;
535
+ const dataLines = [];
536
+ for (const line of raw.split(/\r?\n/)) {
537
+ if (!line || line.startsWith(":")) continue;
538
+ if (line.startsWith("data:")) {
539
+ dataLines.push(line.slice(5).replace(/^ /, ""));
540
+ } else if (line.startsWith("event:")) {
541
+ event = line.slice(6).replace(/^ /, "") || void 0;
542
+ }
543
+ }
544
+ const data = dataLines.join("\n");
545
+ if (!data) return void 0;
546
+ return event ? { event, data } : { data };
547
+ }
548
+ async function* iterateSse(res, signal) {
549
+ const decoder = new TextDecoder();
550
+ let buf = "";
551
+ const drain = function* () {
552
+ for (; ; ) {
553
+ const m = /\r?\n\r?\n/.exec(buf);
554
+ if (!m) break;
555
+ const raw = buf.slice(0, m.index);
556
+ buf = buf.slice(m.index + m[0].length);
557
+ const frame = parseSseFrame(raw);
558
+ if (frame) yield frame;
559
+ }
560
+ };
561
+ if (res.body) {
562
+ const reader = res.body.getReader();
563
+ let onAbort;
564
+ const abortPromise = new Promise((_, reject) => {
565
+ onAbort = () => {
566
+ const e = new Error("Aborted");
567
+ e.name = "AbortError";
568
+ reject(e);
569
+ };
570
+ if (signal) {
571
+ if (signal.aborted) onAbort();
572
+ else signal.addEventListener("abort", onAbort, { once: true });
573
+ }
574
+ });
575
+ abortPromise.catch(() => {
576
+ });
577
+ try {
578
+ for (; ; ) {
579
+ const { done, value } = await Promise.race([reader.read(), abortPromise]);
580
+ if (done) break;
581
+ buf += decoder.decode(value, { stream: true });
582
+ yield* drain();
583
+ }
584
+ buf += decoder.decode();
585
+ } finally {
586
+ if (onAbort && signal) signal.removeEventListener("abort", onAbort);
587
+ try {
588
+ reader.releaseLock();
589
+ } catch {
590
+ }
591
+ }
592
+ } else {
593
+ buf += decoder.decode(await res.arrayBuffer());
594
+ }
595
+ const tail = parseSseFrame(buf);
596
+ if (tail) yield tail;
597
+ }
598
+ async function* parseOpenAiStream(res, signal) {
599
+ for await (const frame of iterateSse(res, signal)) {
600
+ if (frame.data === "[DONE]") return;
601
+ let j;
602
+ try {
603
+ j = JSON.parse(frame.data);
604
+ } catch {
605
+ continue;
606
+ }
607
+ if (!isObj2(j)) continue;
608
+ const choice = Array.isArray(j.choices) && isObj2(j.choices[0]) ? j.choices[0] : void 0;
609
+ if (choice) {
610
+ const delta = isObj2(choice.delta) ? choice.delta : void 0;
611
+ if (delta && typeof delta.content === "string" && delta.content) {
612
+ yield { kind: "text", delta: delta.content };
613
+ }
614
+ if (typeof choice.finish_reason === "string" && choice.finish_reason) {
615
+ yield { kind: "finish", reason: choice.finish_reason };
616
+ }
617
+ }
618
+ if (j.usage != null) yield { kind: "usage", usage: j.usage };
619
+ }
620
+ }
621
+ async function* parseAnthropicStream(res, signal) {
622
+ let usage = {};
623
+ for await (const frame of iterateSse(res, signal)) {
624
+ let j;
625
+ try {
626
+ j = JSON.parse(frame.data);
627
+ } catch {
628
+ continue;
629
+ }
630
+ if (!isObj2(j)) continue;
631
+ const type = typeof j.type === "string" && j.type || frame.event || "";
632
+ if (type === "message_start") {
633
+ const u = isObj2(j.message) && isObj2(j.message.usage) ? j.message.usage : void 0;
634
+ if (u) {
635
+ usage = compactNums({
636
+ input_tokens: pickNum2(u, ["input_tokens"]),
637
+ cache_read_input_tokens: pickNum2(u, ["cache_read_input_tokens"]),
638
+ cache_creation_input_tokens: pickNum2(u, ["cache_creation_input_tokens"])
639
+ });
640
+ if (Object.keys(usage).length) yield { kind: "usage", usage };
641
+ }
642
+ } else if (type === "content_block_delta") {
643
+ const d = isObj2(j.delta) ? j.delta : void 0;
644
+ if (d && d.type === "text_delta" && typeof d.text === "string" && d.text) {
645
+ yield { kind: "text", delta: d.text };
646
+ }
647
+ } else if (type === "message_delta") {
648
+ const d = isObj2(j.delta) ? j.delta : void 0;
649
+ if (d && typeof d.stop_reason === "string" && d.stop_reason) {
650
+ yield { kind: "finish", reason: d.stop_reason };
651
+ }
652
+ const u = isObj2(j.usage) ? j.usage : void 0;
653
+ const out = u ? pickNum2(u, ["output_tokens"]) : void 0;
654
+ if (out != null) {
655
+ usage = { ...usage, output_tokens: out };
656
+ yield { kind: "usage", usage };
657
+ }
658
+ } else if (type === "message_stop") {
659
+ return;
660
+ } else if (type === "error") {
661
+ const e = isObj2(j.error) ? j.error : void 0;
662
+ yield {
663
+ kind: "error",
664
+ type: pickStr2(e, ["type"]) ?? "stream_error",
665
+ message: pickStr2(e, ["message"]) ?? "\u6D41\u5F0F\u54CD\u5E94\u9519\u8BEF"
666
+ };
667
+ return;
668
+ }
669
+ }
670
+ }
671
+ function unsupported(provider, what) {
672
+ return new AiProviderError({
673
+ provider,
674
+ status: 0,
675
+ type: "unsupported",
676
+ message: `${provider} \u7EBF\u534F\u8BAE\u4E0D\u652F\u6301 ${what}`
677
+ });
678
+ }
679
+ function toOpenAiContent(provider, content) {
680
+ if (typeof content === "string") return content;
681
+ const out = [];
682
+ for (const part of content) {
683
+ if (part.type === "text") {
684
+ out.push({ type: "text", text: part.text });
685
+ } else if (part.type === "image") {
686
+ const url = aiContentUrl(part);
687
+ if (url) out.push({ type: "image_url", image_url: { url } });
688
+ } else if (part.type === "audio") {
689
+ if (!part.data) throw unsupported(provider, "\u4EC5 URL \u7684\u97F3\u9891\uFF08input_audio \u9700 base64\uFF0C\u8BF7\u5148\u53D6\u56DE\u5185\u8054\uFF09");
690
+ const format = (part.mimeType ?? "audio/wav").split("/")[1] ?? "wav";
691
+ out.push({ type: "input_audio", input_audio: { data: part.data, format } });
692
+ } else if (part.type === "video") {
693
+ throw unsupported(provider, "video \u8F93\u5165\uFF08\u65E0\u4E3B\u6D41 chat API \u652F\u6301\uFF09");
694
+ } else if (part.type === "file") {
695
+ if (!part.url) throw unsupported(provider, "base64 \u6587\u4EF6\uFF08file \u90E8\u4EF6\u9700 Files API file_id\uFF0C\u8BF7\u7F6E\u4E8E url \u69FD\uFF09");
696
+ out.push({ type: "file", file: { file_id: part.url } });
697
+ } else {
698
+ out.push({ type: "text", text: `[\u8D44\u6E90] ${part.name ?? ""} (${part.uri})`.trim() });
699
+ }
700
+ }
701
+ return out;
702
+ }
703
+ function toAnthropicBlocks(provider, content) {
704
+ if (typeof content === "string") return content ? [{ type: "text", text: content }] : [];
705
+ const out = [];
706
+ for (const part of content) {
707
+ if (part.type === "text") {
708
+ out.push({ type: "text", text: part.text });
709
+ } else if (part.type === "image") {
710
+ const source = part.url ? { type: "url", url: part.url } : { type: "base64", media_type: part.mimeType ?? "image/png", data: part.data ?? "" };
711
+ out.push({ type: "image", source });
712
+ } else if (part.type === "audio") {
713
+ if (!part.data) throw unsupported(provider, "\u4EC5 URL \u7684\u97F3\u9891\uFF08audio \u5757\u9700 base64 source\uFF09");
714
+ out.push({ type: "audio", source: { type: "base64", media_type: part.mimeType ?? "audio/wav", data: part.data } });
715
+ } else if (part.type === "video") {
716
+ throw unsupported(provider, "video \u8F93\u5165");
717
+ } else if (part.type === "file") {
718
+ const mime = part.mimeType ?? "application/pdf";
719
+ if (mime !== "application/pdf") throw unsupported(provider, `\u975E PDF \u6587\u4EF6\u5757\uFF08document \u5757\u4E3A application/pdf\uFF0C\u6536\u5230 ${mime}\uFF09`);
720
+ const source = part.url ? { type: "url", url: part.url } : { type: "base64", media_type: mime, data: part.data ?? "" };
721
+ out.push({ type: "document", source });
722
+ } else {
723
+ out.push({ type: "text", text: `[\u8D44\u6E90] ${part.name ?? ""} (${part.uri})`.trim() });
724
+ }
725
+ }
726
+ return out;
727
+ }
728
+ function buildOpenAiBody(provider, req, model, streaming) {
729
+ const messages = req.messages.map((m) => {
730
+ if (m.role === "tool") {
731
+ if (!m.toolCallId) throw unsupported(provider, "role 'tool' \u4E14\u7F3A toolCallId\uFF08openai \u65CF\u9700 tool_call_id\uFF09");
732
+ return { role: "tool", content: contentToText(m.content), tool_call_id: m.toolCallId };
733
+ }
734
+ return { role: m.role, content: toOpenAiContent(provider, m.content) };
735
+ });
736
+ const body = { model, messages, stream: streaming };
737
+ if (req.temperature != null) body.temperature = req.temperature;
738
+ if (req.maxTokens != null) body.max_tokens = req.maxTokens;
739
+ if (streaming) body.stream_options = { include_usage: true };
740
+ return body;
741
+ }
742
+ function buildAnthropicBody(provider, req, model, streaming) {
743
+ const systemMsgs = req.messages.filter((m) => m.role === "system");
744
+ const system = systemMsgs.map((m) => contentToText(m.content)).join("\n\n");
745
+ const messages = req.messages.filter((m) => m.role !== "system").map((m) => {
746
+ if (m.role === "tool") {
747
+ if (!m.toolCallId) throw unsupported(provider, "role 'tool' \u4E14\u7F3A toolCallId\uFF08anthropic \u65CF\u6620\u5C04 tool_result \u9700 tool_use_id\uFF09");
748
+ return { role: "user", content: [{ type: "tool_result", tool_use_id: m.toolCallId, content: toAnthropicBlocks(provider, m.content) }] };
749
+ }
750
+ const blocks = toAnthropicBlocks(provider, m.content);
751
+ if (m.cacheControl && blocks.length > 0) blocks[blocks.length - 1].cache_control = { type: "ephemeral" };
752
+ return { role: m.role, content: blocks };
753
+ });
754
+ const body = {
755
+ model,
756
+ messages,
757
+ max_tokens: req.maxTokens ?? 4096
758
+ };
759
+ if (system) {
760
+ body.system = systemMsgs.some((m) => m.cacheControl) ? [{ type: "text", text: system, cache_control: { type: "ephemeral" } }] : system;
761
+ }
762
+ if (req.temperature != null) body.temperature = req.temperature;
763
+ if (streaming) body.stream = true;
764
+ return body;
765
+ }
766
+ function buildHeaders(def, apiKey) {
767
+ const headers = { "content-type": "application/json" };
768
+ for (const [k, v] of Object.entries(def.extraHeaders ?? {})) headers[k] = v;
769
+ if (def.auth === "x-api-key") headers["x-api-key"] = apiKey;
770
+ else headers.authorization = `Bearer ${apiKey}`;
771
+ return headers;
772
+ }
773
+ function joinSignals(a, b) {
774
+ if (!b) return a;
775
+ const c = new AbortController();
776
+ if (a.aborted || b.aborted) {
777
+ c.abort();
778
+ return c.signal;
779
+ }
780
+ const fwd = () => {
781
+ if (!c.signal.aborted) c.abort();
782
+ };
783
+ a.addEventListener("abort", fwd, { once: true });
784
+ b.addEventListener("abort", fwd, { once: true });
785
+ return c.signal;
786
+ }
787
+ function dispatchUsageEvent(usage) {
788
+ if (typeof document === "undefined") return;
789
+ try {
790
+ document.dispatchEvent(new CustomEvent("icen:ai-usage", { detail: usage, bubbles: true }));
791
+ } catch {
792
+ }
793
+ }
794
+ function dispatchDoneEvent(detail) {
795
+ if (typeof document === "undefined") return;
796
+ try {
797
+ document.dispatchEvent(new CustomEvent("icen:ai-done", { detail, bubbles: true }));
798
+ } catch {
799
+ }
800
+ }
801
+ function createAiClient(options) {
802
+ const lookedUp = getAiProvider(options.provider);
803
+ if (!lookedUp) {
804
+ throw new Error(`[ai-provider] \u672A\u6CE8\u518C\u7684 provider: ${options.provider}\uFF08\u53EF\u7528\uFF1A${listAiProviders().map((d) => d.id).join(" / ")}\uFF09`);
805
+ }
806
+ const def = lookedUp;
807
+ const baseURL = (options.baseURL ?? def.baseURL).replace(/\/+$/, "");
808
+ const chatPath = def.chatPath.startsWith("/") ? def.chatPath : `/${def.chatPath}`;
809
+ const url = `${baseURL}${chatPath}`;
810
+ const defaultModel = options.model ?? def.models[0]?.id ?? "";
811
+ const doFetch = options.fetch ?? ((input, init) => fetch(input, init));
812
+ const normalizeThrown = (err) => {
813
+ if (err instanceof AiProviderError) return err;
814
+ if (isAbortError(err)) {
815
+ return new AiProviderError({ provider: def.id, status: 0, type: "cancelled", message: "\u8BF7\u6C42\u5DF2\u53D6\u6D88", raw: err });
816
+ }
817
+ const msg = err instanceof Error ? err.message : String(err);
818
+ const corsHint = err instanceof TypeError && def.browserDirect !== "yes" ? `\uFF1B\u6D4F\u89C8\u5668\u76F4\u8FDE ${def.label}\uFF08${def.baseURL}\uFF09\u5F88\u53EF\u80FD\u88AB CORS \u62E6\u622A\uFF0C\u5EFA\u8BAE\u7ECF\u4EE3\u7406/\u81EA\u5EFA\u7F51\u5173\u8F6C\u53D1` : "";
819
+ return new AiProviderError({
820
+ provider: def.id,
821
+ status: 0,
822
+ type: "network",
823
+ message: `\u7F51\u7EDC\u8BF7\u6C42\u5931\u8D25\uFF1A${msg}${corsHint}`,
824
+ raw: err
825
+ });
826
+ };
827
+ async function post(req, streaming, signal) {
828
+ const model = req.model ?? defaultModel;
829
+ const body = def.wire === "anthropic" ? buildAnthropicBody(def.id, req, model, streaming) : buildOpenAiBody(def.id, req, model, streaming);
830
+ let res;
831
+ try {
832
+ res = await doFetch(url, {
833
+ method: "POST",
834
+ headers: buildHeaders(def, options.apiKey),
835
+ body: JSON.stringify(body),
836
+ signal
837
+ });
838
+ } catch (err) {
839
+ throw normalizeThrown(err);
840
+ }
841
+ if (!res.ok) throw await readHttpError(res, def.id);
842
+ return res;
843
+ }
844
+ function audit(req, streaming, status, usage, durationMs, error, ttftMs) {
845
+ const model = req.model ?? defaultModel;
846
+ const cost = estimateCost(usage, def.id, model);
847
+ const entry = {
848
+ id: genEntryId(),
849
+ ts: Date.now(),
850
+ provider: def.id,
851
+ model,
852
+ baseURL,
853
+ stream: streaming,
854
+ status,
855
+ durationMs,
856
+ ttftMs,
857
+ cost,
858
+ usage: Object.keys(usage).length > 0 ? usage : void 0,
859
+ error
860
+ };
861
+ try {
862
+ options.onAudit?.(entry);
863
+ options.auditor?.log(entry);
864
+ } catch {
865
+ }
866
+ return { entry, cost };
867
+ }
868
+ const publishUsage = (usage) => {
869
+ if (Object.keys(usage).length > 0) dispatchUsageEvent(usage);
870
+ };
871
+ async function chat(req) {
872
+ const t0 = Date.now();
873
+ try {
874
+ const res = await post(req, false, req.signal);
875
+ const text = await res.text();
876
+ let j;
877
+ try {
878
+ j = text ? JSON.parse(text) : void 0;
879
+ } catch {
880
+ throw new AiProviderError({ provider: def.id, status: res.status, type: "parse", message: "\u54CD\u5E94\u4E0D\u662F\u5408\u6CD5 JSON", raw: text });
881
+ }
882
+ if (j === void 0) {
883
+ throw new AiProviderError({ provider: def.id, status: res.status, type: "parse", message: "\u54CD\u5E94\u4E3A\u7A7A", raw: text });
884
+ }
885
+ let outText = "";
886
+ let outParts;
887
+ let finishReason;
888
+ let usageRaw;
889
+ if (isObj2(j)) {
890
+ if (def.wire === "anthropic") {
891
+ const blocks = Array.isArray(j.content) ? j.content : [];
892
+ outParts = normalizeContentParts(blocks);
893
+ outText = contentToText(outParts);
894
+ finishReason = pickStr2(j, ["stop_reason"]);
895
+ } else {
896
+ const choice = Array.isArray(j.choices) && isObj2(j.choices[0]) ? j.choices[0] : void 0;
897
+ const message = choice && isObj2(choice.message) ? choice.message : void 0;
898
+ const content = message?.content;
899
+ if (typeof content === "string") {
900
+ outText = content;
901
+ } else if (Array.isArray(content)) {
902
+ outParts = normalizeContentParts(content);
903
+ outText = contentToText(outParts);
904
+ }
905
+ finishReason = choice ? pickStr2(choice, ["finish_reason"]) : void 0;
906
+ }
907
+ usageRaw = j.usage;
908
+ }
909
+ const usage = normalizeUsage(usageRaw);
910
+ publishUsage(usage);
911
+ const { cost } = audit(req, false, "ok", usage, Date.now() - t0);
912
+ dispatchDoneEvent({
913
+ status: "ok",
914
+ provider: def.id,
915
+ model: req.model ?? defaultModel,
916
+ stream: false,
917
+ usage,
918
+ cost,
919
+ durationMs: Date.now() - t0
920
+ });
921
+ const result = { text: outText, usage, cost, finishReason, raw: j };
922
+ if (outParts && outParts.length > 0) result.parts = outParts;
923
+ return result;
924
+ } catch (err) {
925
+ const e = normalizeThrown(err);
926
+ audit(req, false, "error", {}, Date.now() - t0, e.message);
927
+ dispatchDoneEvent({
928
+ status: e.type === "cancelled" ? "cancelled" : "error",
929
+ provider: def.id,
930
+ model: req.model ?? defaultModel,
931
+ stream: false,
932
+ error: e.message,
933
+ durationMs: Date.now() - t0
934
+ });
935
+ throw e;
936
+ }
937
+ }
938
+ function stream(req) {
939
+ const ac = new AbortController();
940
+ const signal = joinSignals(ac.signal, req.signal);
941
+ const chunks = [];
942
+ const waiters = [];
943
+ let finished = false;
944
+ let resolveDone;
945
+ const done = new Promise((res) => {
946
+ resolveDone = res;
947
+ });
948
+ const notify = () => {
949
+ const w = waiters.splice(0, waiters.length);
950
+ for (const fn of w) fn();
951
+ };
952
+ const push = (c) => {
953
+ chunks.push(c);
954
+ notify();
955
+ };
956
+ void (async () => {
957
+ const t0 = Date.now();
958
+ let text = "";
959
+ let finishReason;
960
+ let usageRaw;
961
+ let status = "ok";
962
+ let error;
963
+ let errorType;
964
+ let ttftMs;
965
+ try {
966
+ const res = await post(req, true, signal);
967
+ const events = def.wire === "anthropic" ? parseAnthropicStream(res, signal) : parseOpenAiStream(res, signal);
968
+ for await (const ev of events) {
969
+ if (ev.kind === "text") {
970
+ if (ttftMs == null) ttftMs = Date.now() - t0;
971
+ text += ev.delta;
972
+ push({ type: "text", delta: ev.delta });
973
+ } else if (ev.kind === "usage") {
974
+ usageRaw = ev.usage;
975
+ } else if (ev.kind === "finish") {
976
+ finishReason = ev.reason;
977
+ } else {
978
+ status = "error";
979
+ error = ev.message;
980
+ push({ type: "error", message: ev.message });
981
+ }
982
+ }
983
+ if (status === "ok") push({ type: "done" });
984
+ } catch (err) {
985
+ const e = normalizeThrown(err);
986
+ status = "error";
987
+ error = e.message;
988
+ errorType = e.type;
989
+ if (e.type !== "cancelled") push({ type: "error", message: e.message });
990
+ } finally {
991
+ const usage = normalizeUsage(usageRaw);
992
+ publishUsage(usage);
993
+ const { cost } = audit(
994
+ req,
995
+ true,
996
+ errorType === "cancelled" ? "error" : status,
997
+ usage,
998
+ Date.now() - t0,
999
+ errorType === "cancelled" ? "\u8BF7\u6C42\u5DF2\u53D6\u6D88" : error,
1000
+ ttftMs
1001
+ );
1002
+ dispatchDoneEvent({
1003
+ status: errorType === "cancelled" ? "cancelled" : status,
1004
+ provider: def.id,
1005
+ model: req.model ?? defaultModel,
1006
+ stream: true,
1007
+ usage,
1008
+ cost,
1009
+ error: errorType === "cancelled" ? void 0 : error,
1010
+ durationMs: Date.now() - t0,
1011
+ ttftMs
1012
+ });
1013
+ finished = true;
1014
+ notify();
1015
+ ac.abort();
1016
+ resolveDone({ text, usage, cost, finishReason, raw: usageRaw });
1017
+ }
1018
+ })();
1019
+ const iterator = {
1020
+ next() {
1021
+ if (chunks.length > 0) return Promise.resolve({ value: chunks.shift(), done: false });
1022
+ if (finished) return Promise.resolve({ value: void 0, done: true });
1023
+ return new Promise((resolve) => {
1024
+ waiters.push(() => {
1025
+ resolve(iterator.next());
1026
+ });
1027
+ });
1028
+ },
1029
+ return() {
1030
+ ac.abort();
1031
+ return Promise.resolve({ value: void 0, done: true });
1032
+ }
1033
+ };
1034
+ return {
1035
+ [Symbol.asyncIterator]() {
1036
+ return iterator;
1037
+ },
1038
+ cancel() {
1039
+ ac.abort();
1040
+ },
1041
+ done
1042
+ };
1043
+ }
1044
+ const config = Object.freeze({ provider: def.id, baseURL, model: defaultModel });
1045
+ return { chat, stream, config };
1046
+ }
1047
+ export {
1048
+ AiProviderError,
1049
+ createAiAuditor,
1050
+ createAiClient,
1051
+ estimateCost,
1052
+ getAiProvider,
1053
+ listAiProviders,
1054
+ registerAiProvider
1055
+ };