@kieksme/csp-plugin-chat 0.8.0 → 0.10.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/README.md CHANGED
@@ -23,3 +23,5 @@ Der System-Prompt begrenzt den Chat auf Support und Portalbedienung. Fachfremde
23
23
  Optional persisted conversations and real human support are available through the Teams plugin. The UI uses `chatSupport` from the public profile and identifies real support separately from the digital assistant. See [Teams support](../../docs/teams-support.md).
24
24
 
25
25
  For portals with an existing same-origin authenticated gateway, set `chatSupport.auth` to `session` to reuse the login without browser tokens. The API still requires a validated delegated access token supplied by the internal gateway. See [Teams support](../../docs/teams-support.md#vorhandene-portal-anmeldung-verwenden).
26
+
27
+ WebMCP tool: `chat_ask` streams one answer from `POST /api/v1/chat` and returns text, responder and sources. It changes no data but uses the chat rate limit and provider quota, so it is not marked read-only. See [WebMCP](../../docs/webmcp.md).
package/dist/browser.d.ts CHANGED
@@ -1,10 +1,12 @@
1
- import * as react from 'react';
1
+ import * as _kieksme_csp_sdk from '@kieksme/csp-sdk';
2
2
  import { BrowserContext } from '@kieksme/csp-sdk';
3
+ import * as react from 'react';
3
4
 
4
5
  declare function Chat(ctx: BrowserContext): react.JSX.Element;
5
6
  declare const _default: {
6
7
  id: string;
7
8
  sdkVersion: string;
9
+ tools: _kieksme_csp_sdk.WebMcpTool[];
8
10
  sections: {
9
11
  id: string;
10
12
  placement: "hero";
package/dist/browser.js CHANGED
@@ -1,9 +1,13 @@
1
1
  import {
2
2
  cspPlugin
3
- } from "./chunk-NHJR47VI.js";
3
+ } from "./chunk-MGUWXMOX.js";
4
4
 
5
- // src/browser.tsx
6
- import { useEffect as useEffect2, useLayoutEffect, useRef as useRef2, useState as useState2 } from "react";
5
+ // src/tools.ts
6
+ import {
7
+ UNTRUSTED_NOTE,
8
+ webMcpError,
9
+ webMcpJson
10
+ } from "@kieksme/csp-sdk";
7
11
 
8
12
  // src/answer.ts
9
13
  import { safeUrl } from "@kieksme/csp-sdk";
@@ -33,6 +37,138 @@ function answerParts(text, sources, streaming = false) {
33
37
  });
34
38
  }
35
39
 
40
+ // src/tools.ts
41
+ function parseMessages(value) {
42
+ if (!Array.isArray(value) || value.length < 1 || value.length > 20)
43
+ return "messages muss 1 bis 20 Eintr\xE4ge enthalten.";
44
+ let total = 0;
45
+ const messages = [];
46
+ for (const item of value) {
47
+ const { role, content } = item ?? {};
48
+ if (role !== "user" && role !== "assistant" || typeof content !== "string" || content.length < 1 || content.length > 4e3)
49
+ return "Jede Nachricht braucht role (user|assistant) und content (1 bis 4000 Zeichen).";
50
+ total += content.length;
51
+ messages.push({ role, content });
52
+ }
53
+ if (messages.at(-1).role !== "user")
54
+ return "Die letzte Nachricht muss von user stammen.";
55
+ if (total > 16e3)
56
+ return "Der Verlauf darf h\xF6chstens 16000 Zeichen enthalten.";
57
+ return messages;
58
+ }
59
+ async function askChat(ctx, messages, signal) {
60
+ const response = await (ctx.request ?? fetch)(
61
+ ctx.config.apiUrl + "/api/v1/chat",
62
+ {
63
+ method: "POST",
64
+ headers: { "Content-Type": "application/json" },
65
+ body: JSON.stringify({ messages }),
66
+ signal
67
+ }
68
+ );
69
+ if (!response.ok || !response.body)
70
+ throw new Error(
71
+ response.status === 429 ? "Zu viele Anfragen. Bitte eine Minute warten." : "Chat derzeit nicht erreichbar."
72
+ );
73
+ const reader = response.body.getReader();
74
+ const decoder = new TextDecoder();
75
+ let buffer = "";
76
+ let answer = "";
77
+ let complete = false;
78
+ let responder;
79
+ let sources = [];
80
+ try {
81
+ while (true) {
82
+ const { done, value } = await reader.read();
83
+ buffer += decoder.decode(value, { stream: !done });
84
+ let boundary;
85
+ while ((boundary = buffer.indexOf("\n\n")) >= 0) {
86
+ const block = buffer.slice(0, boundary);
87
+ buffer = buffer.slice(boundary + 2);
88
+ const type = block.match(/^event: (.+)$/m)?.[1];
89
+ const payload = block.match(/^data: (.+)$/m)?.[1];
90
+ if (!payload) continue;
91
+ const event = JSON.parse(payload);
92
+ if (type === "delta") answer += event.text;
93
+ if (type === "responder") responder = event;
94
+ if (type === "sources") sources = event;
95
+ if (type === "error") throw new Error(event.message);
96
+ if (type === "done") complete = true;
97
+ }
98
+ if (done) break;
99
+ }
100
+ } finally {
101
+ await reader.cancel().catch(() => void 0);
102
+ reader.releaseLock();
103
+ }
104
+ if (!complete)
105
+ throw new Error(
106
+ "Die Verbindung wurde unterbrochen. Antwort unvollst\xE4ndig."
107
+ );
108
+ return { answer, responder, sources };
109
+ }
110
+ var chatTools = [
111
+ {
112
+ name: "chat_ask",
113
+ description: "Fragt den digitalen Assistenten des Portals (kennt Hilfe-Inhalte, Team, Schichten und Status). Verbraucht Chat-Kontingent (Rate-Limit pro IP) und ruft einen KI-Anbieter auf; nur nutzen, wenn die anderen Tools nicht reichen. \xC4ndert keine Daten. Die letzte Nachricht muss von user stammen.",
114
+ inputSchema: {
115
+ type: "object",
116
+ properties: {
117
+ messages: {
118
+ type: "array",
119
+ minItems: 1,
120
+ maxItems: 20,
121
+ items: {
122
+ type: "object",
123
+ properties: {
124
+ role: { type: "string", enum: ["user", "assistant"] },
125
+ content: { type: "string", minLength: 1, maxLength: 4e3 }
126
+ },
127
+ required: ["role", "content"],
128
+ additionalProperties: false
129
+ }
130
+ }
131
+ },
132
+ required: ["messages"],
133
+ additionalProperties: false
134
+ },
135
+ annotations: { readOnlyHint: false },
136
+ execute: async (args, ctx) => {
137
+ if (ctx.config.chatSupport)
138
+ return webMcpError(
139
+ "Der Chat l\xE4uft im Supportmodus mit Anmeldung und ist per Tool nicht verf\xFCgbar. Bitte die Hotline nutzen."
140
+ );
141
+ const messages = parseMessages(args.messages);
142
+ if (typeof messages === "string") return webMcpError(messages);
143
+ try {
144
+ const { answer, responder, sources } = await askChat(
145
+ ctx,
146
+ messages,
147
+ AbortSignal.timeout(9e4)
148
+ );
149
+ return webMcpJson({
150
+ notice: UNTRUSTED_NOTE,
151
+ responder: responder?.role === "on-duty" ? responder.name : "Service-Team",
152
+ answer,
153
+ sources: sources.map((s) => ({
154
+ id: s.id,
155
+ title: s.title,
156
+ href: sourceHref(s.href),
157
+ stale: s.stale ?? false
158
+ }))
159
+ });
160
+ } catch (error) {
161
+ return webMcpError(
162
+ error instanceof Error ? error.message : "Chat nicht verf\xFCgbar."
163
+ );
164
+ }
165
+ }
166
+ }
167
+ ];
168
+
169
+ // src/browser.tsx
170
+ import { useEffect as useEffect2, useLayoutEffect, useRef as useRef2, useState as useState2 } from "react";
171
+
36
172
  // src/support-chat.tsx
37
173
  import { useEffect, useRef, useState } from "react";
38
174
 
@@ -46,6 +182,7 @@ async function supportSession(ctx) {
46
182
  },
47
183
  name: "Portal-Nutzer Demo"
48
184
  };
185
+ if (c.auth === "anonymous") return anonymousSession(ctx);
49
186
  if (c.auth === "session") return cookieSession(ctx);
50
187
  const { PublicClientApplication } = await import("@azure/msal-browser");
51
188
  const client = new PublicClientApplication({
@@ -98,12 +235,74 @@ function cookieSession(ctx) {
98
235
  };
99
236
  return current;
100
237
  }
238
+ function anonymousSession(ctx) {
239
+ const key = `csp-support:${ctx.config.apiUrl}`;
240
+ let saved;
241
+ try {
242
+ const value = JSON.parse(window.sessionStorage.getItem(key) ?? "null");
243
+ if (typeof value?.token === "string" && typeof value?.expiresAt === "number" && value.expiresAt > Date.now() && typeof value?.name === "string")
244
+ saved = value;
245
+ } catch {
246
+ }
247
+ let pending;
248
+ const current = {
249
+ name: saved?.name ?? "Besucher",
250
+ async token() {
251
+ if (saved) {
252
+ if (saved.expiresAt <= Date.now())
253
+ throw new Error(
254
+ "Chat-Sitzung abgelaufen. Bitte verbinden Sie den Chat neu."
255
+ );
256
+ return saved.token;
257
+ }
258
+ if (pending) return pending;
259
+ pending = (async () => {
260
+ const response = await (ctx.request ?? fetch)(
261
+ ctx.config.apiUrl + "/api/v1/conversations/session",
262
+ {
263
+ method: "POST",
264
+ credentials: "omit",
265
+ cache: "no-store"
266
+ }
267
+ );
268
+ if (!response.ok)
269
+ throw new Error(
270
+ "Chat derzeit nicht verf\xFCgbar. Bitte versuchen Sie es sp\xE4ter erneut."
271
+ );
272
+ const value = await response.json();
273
+ if (typeof value.token !== "string" || typeof value.expiresAt !== "number" || value.expiresAt <= Date.now() || typeof value.name !== "string")
274
+ throw new Error("Chat derzeit nicht verf\xFCgbar.");
275
+ saved = value;
276
+ current.name = value.name;
277
+ try {
278
+ window.sessionStorage.setItem(key, JSON.stringify(value));
279
+ } catch {
280
+ }
281
+ return value.token;
282
+ })();
283
+ try {
284
+ return await pending;
285
+ } finally {
286
+ pending = void 0;
287
+ }
288
+ },
289
+ async login() {
290
+ try {
291
+ window.sessionStorage.removeItem(key);
292
+ } catch {
293
+ }
294
+ window.location.reload();
295
+ }
296
+ };
297
+ return current;
298
+ }
101
299
 
102
300
  // src/support-chat.tsx
103
301
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
104
302
  function SupportChat(ctx) {
105
303
  const [current, setCurrent] = useState(null), [input, setInput] = useState(""), [error, setError] = useState(""), [busy, setBusy] = useState(false), [ready, setReady] = useState(false), [auth, setAuth] = useState(null);
106
304
  const seq = useRef(0), requestRef = useRef(null);
305
+ const anonymous = ctx.config.chatSupport?.auth === "anonymous";
107
306
  const demo = ctx.config.chatSupport?.mode === "demo";
108
307
  useEffect(() => {
109
308
  let alive = true;
@@ -261,10 +460,10 @@ function SupportChat(ctx) {
261
460
  }
262
461
  }
263
462
  return /* @__PURE__ */ jsxs("div", { className: "chat-panel support-chat text-ink bg-card border border-line p-6 rounded-card compact:p-4", children: [
264
- /* @__PURE__ */ jsx("p", { className: "text-[0.75rem] text-ink mb-3", children: demo ? "Synthetische Support-Demo. Keine \xDCbertragung an Teams." : "Ihre Nachrichten und die Antworten werden an den Thinkport-Support in Microsoft Teams weitergegeben." }),
463
+ /* @__PURE__ */ jsx("p", { className: "text-[0.75rem] text-ink mb-3", children: demo ? "Synthetische Support-Demo. Keine \xDCbertragung an einen Support-Channel." : `Ihre Nachrichten und die Antworten werden an den Support in ${ctx.config.chatSupport?.mode === "slack" ? "Slack" : "Microsoft Teams"} weitergegeben.` }),
265
464
  /* @__PURE__ */ jsxs("p", { role: "status", className: "text-[0.8125rem] mb-3", children: [
266
465
  current?.status === "support" ? `${current.support?.name} \xB7 Support` : current?.status === "closed" ? "Gespr\xE4ch abgeschlossen" : "Digitaler Assistent",
267
- auth && ready && !demo ? ` \xB7 Angemeldet als ${auth.name}` : ""
466
+ auth && ready && !demo ? anonymous ? ` \xB7 ${auth.name}` : ` \xB7 Angemeldet als ${auth.name}` : ""
268
467
  ] }),
269
468
  /* @__PURE__ */ jsx(
270
469
  "div",
@@ -305,9 +504,11 @@ function SupportChat(ctx) {
305
504
  className: "bg-accent text-accent-ink rounded-card px-4 py-2",
306
505
  disabled: !auth,
307
506
  onClick: () => void auth?.login().catch(
308
- () => setError("Microsoft-Anmeldung konnte nicht gestartet werden.")
507
+ () => setError(
508
+ anonymous ? "Chat konnte nicht neu verbunden werden." : "Microsoft-Anmeldung konnte nicht gestartet werden."
509
+ )
309
510
  ),
310
- children: ctx.config.chatSupport?.auth === "session" ? "Portal-Anmeldung erneuern" : "Mit Microsoft anmelden"
511
+ children: anonymous ? "Chat neu verbinden" : ctx.config.chatSupport?.auth === "session" ? "Portal-Anmeldung erneuern" : "Mit Microsoft anmelden"
311
512
  }
312
513
  ),
313
514
  ready && /* @__PURE__ */ jsxs("form", { onSubmit: submit, className: "chat-form flex gap-2.5 items-end", children: [
@@ -394,9 +595,11 @@ function SupportChat(ctx) {
394
595
  type: "button",
395
596
  className: "underline mt-3",
396
597
  onClick: () => void auth.login().catch(
397
- () => setError("Microsoft-Anmeldung konnte nicht gestartet werden.")
598
+ () => setError(
599
+ anonymous ? "Chat konnte nicht neu verbunden werden." : "Microsoft-Anmeldung konnte nicht gestartet werden."
600
+ )
398
601
  ),
399
- children: ctx.config.chatSupport?.auth === "session" ? "Portal-Anmeldung erneuern" : "Microsoft-Anmeldung erneuern"
602
+ children: anonymous ? "Chat neu verbinden" : ctx.config.chatSupport?.auth === "session" ? "Portal-Anmeldung erneuern" : "Microsoft-Anmeldung erneuern"
400
603
  }
401
604
  ),
402
605
  error && /* @__PURE__ */ jsx("p", { role: "alert", className: "text-danger text-[0.75rem] mt-3", children: error })
@@ -648,6 +851,7 @@ function LegacyChat(ctx) {
648
851
  var browser_default = {
649
852
  id: "chat",
650
853
  sdkVersion: cspPlugin.sdkVersion,
854
+ tools: chatTools,
651
855
  sections: [
652
856
  {
653
857
  id: "chat",
@@ -1,7 +1,7 @@
1
1
  // package.json
2
2
  var cspPlugin = {
3
3
  id: "chat",
4
- sdkVersion: "^0.8.0",
4
+ sdkVersion: "^0.10.0",
5
5
  browser: "./browser",
6
6
  server: "./server"
7
7
  };
package/dist/server.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  cspPlugin
3
- } from "./chunk-NHJR47VI.js";
3
+ } from "./chunk-MGUWXMOX.js";
4
4
 
5
5
  // src/server.ts
6
6
  import { z as z2 } from "zod";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kieksme/csp-plugin-chat",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -23,7 +23,7 @@
23
23
  },
24
24
  "dependencies": {
25
25
  "@azure/msal-browser": "^5.25.0",
26
- "@kieksme/csp-sdk": "^0.8.0",
26
+ "@kieksme/csp-sdk": "^0.10.0",
27
27
  "openai": "^6.3.0",
28
28
  "zod": "^3.25.76"
29
29
  },
@@ -35,7 +35,7 @@
35
35
  },
36
36
  "cspPlugin": {
37
37
  "id": "chat",
38
- "sdkVersion": "^0.8.0",
38
+ "sdkVersion": "^0.10.0",
39
39
  "browser": "./browser",
40
40
  "server": "./server"
41
41
  },