dewee-webchat 0.5.0-beta.2 → 0.5.0-beta.3
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 +10 -0
- package/dist/{chunk-KXW4EWXA.js → chunk-N7BJKIPT.js} +46 -2
- package/dist/{chunk-XCO4TCKF.js → chunk-Q5RDSRZG.js} +261 -8
- package/dist/dewee-webchat.js +33 -28
- package/dist/{handler-SsDyK0TG.d.ts → handler-BchS1kD4.d.ts} +5 -1
- package/dist/index.d.ts +21 -2
- package/dist/index.js +2 -2
- package/dist/{protocol-BbjGSfZa.d.ts → protocol-DjEdC6QU.d.ts} +35 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +2 -2
- package/dist/server-identity.d.ts +2 -2
- package/dist/server.d.ts +3 -3
- package/dist/server.js +131 -21
- package/dist/vue.d.ts +52 -52
- package/dist/vue.js +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -368,6 +368,16 @@ used), `allowInsecureHttp` does not apply, a user switch under `rebindEachReques
|
|
|
368
368
|
previous user's session at CCP, and logout revokes it through [`handler.revoke`](#signed-in-users).
|
|
369
369
|
The integration's sign-in mode must allow signed-in users.
|
|
370
370
|
|
|
371
|
+
### Hosted credential requests
|
|
372
|
+
|
|
373
|
+
Credential requests are off by default and require CCP transport, signed-in identity, and Gateway/CCP
|
|
374
|
+
support enabled for the agent. To opt in, add `credentialRequests: { formOrigin: "https://app.example.com" }`
|
|
375
|
+
to the server handler options, using the exact HTTPS origin of your Dewee-hosted form. Use the ASCII
|
|
376
|
+
punycode form for an internationalized domain. The form is
|
|
377
|
+
opened only after the visitor chooses it; a workspace approval administrator must authenticate there.
|
|
378
|
+
The widget never collects the credential value. Anonymous sessions, direct Gateway transport, and
|
|
379
|
+
an untrusted form origin cannot use this flow. See the [HTTP contract](../../docs/http-contract.md#hosted-credential-requests).
|
|
380
|
+
|
|
371
381
|
```ts
|
|
372
382
|
import { createDeweeHandler, optionsFromEnv } from "dewee-webchat/server";
|
|
373
383
|
import { deweeIdentitySdk } from "dewee-webchat/server/identity";
|
|
@@ -21,7 +21,7 @@ function isLinkErrorCode(code) {
|
|
|
21
21
|
}
|
|
22
22
|
var HEADER_GATEWAY_URL = "x-dewee-gateway-url";
|
|
23
23
|
var HEADER_GATEWAY_TOKEN = "x-dewee-gateway-token";
|
|
24
|
-
var VERSION = "0.5.0-beta.
|
|
24
|
+
var VERSION = "0.5.0-beta.3";
|
|
25
25
|
|
|
26
26
|
// src/actions/page-block.ts
|
|
27
27
|
var ACTION_NAME_RE = /^[a-z][a-z0-9_.-]{0,63}$/;
|
|
@@ -135,6 +135,48 @@ function parseJobAccessLaunch(raw) {
|
|
|
135
135
|
return { bootstrap_url: url.href, launch_token: data.launch_token, expires_at: data.expires_at };
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
+
// src/credential-request.ts
|
|
139
|
+
var uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
140
|
+
var statuses = /* @__PURE__ */ new Set(["pending", "applying", "resolved", "cancelled", "expired", "failed"]);
|
|
141
|
+
var record = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : null;
|
|
142
|
+
var text = (v, max) => typeof v === "string" && v.length <= max;
|
|
143
|
+
var date = (v) => text(v, 64) && Number.isFinite(Date.parse(v));
|
|
144
|
+
function parseCredentialRequest(raw, expectedAgent) {
|
|
145
|
+
const v = record(raw);
|
|
146
|
+
const target = record(v?.target);
|
|
147
|
+
if (!v || !target || !text(v.id, 36) || !uuid.test(v.id) || v.schema_version !== "webchat-credential-request.v1" || typeof v.status !== "string" || !statuses.has(v.status) || !text(v.title, 300) || !text(v.purpose, 4e3) || !text(target.type, 100) || !text(target.mode, 100) || !Array.isArray(v.fields) || v.fields.length > 32 || !date(v.expires_at) || v.agent_key !== expectedAgent || v.completion !== "hosted" || typeof v.resumable !== "boolean") return null;
|
|
148
|
+
const fields = [];
|
|
149
|
+
for (const item of v.fields) {
|
|
150
|
+
const f = record(item);
|
|
151
|
+
if (!f || !text(f.key, 100) || !/^[A-Za-z0-9_.-]+$/.test(f.key) || !text(f.label, 300) || !text(f.kind, 100) || typeof f.required !== "boolean") return null;
|
|
152
|
+
fields.push({ key: f.key, label: f.label, kind: f.kind, required: f.required });
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
id: v.id,
|
|
156
|
+
schema_version: "webchat-credential-request.v1",
|
|
157
|
+
status: v.status,
|
|
158
|
+
title: v.title,
|
|
159
|
+
purpose: v.purpose,
|
|
160
|
+
target: { type: target.type, mode: target.mode },
|
|
161
|
+
fields,
|
|
162
|
+
expires_at: v.expires_at,
|
|
163
|
+
agent_key: expectedAgent,
|
|
164
|
+
completion: "hosted",
|
|
165
|
+
resumable: v.resumable
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
function parseCredentialHandoff(raw, expectedId, trustedOrigin) {
|
|
169
|
+
const v = record(raw);
|
|
170
|
+
if (!v || v.credential_request_id !== expectedId || !text(v.url, 4096) || !date(v.expires_at) || Date.parse(v.expires_at) <= Date.now()) return null;
|
|
171
|
+
try {
|
|
172
|
+
const url = new URL(v.url);
|
|
173
|
+
if (url.protocol !== "https:" || url.origin !== trustedOrigin || url.username || url.password || url.hash || url.pathname !== "/credential-requests/open" || !/^\?token=[A-Za-z0-9_-]{43}$/.test(url.search)) return null;
|
|
174
|
+
return { credential_request_id: expectedId, url: url.href, expires_at: v.expires_at };
|
|
175
|
+
} catch {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
138
180
|
export {
|
|
139
181
|
isIdentityErrorCode,
|
|
140
182
|
ADMISSION_ERROR_CODES,
|
|
@@ -152,5 +194,7 @@ export {
|
|
|
152
194
|
hashManifest,
|
|
153
195
|
buildPageBlock,
|
|
154
196
|
stripPageBlock,
|
|
155
|
-
parseJobAccessLaunch
|
|
197
|
+
parseJobAccessLaunch,
|
|
198
|
+
parseCredentialRequest,
|
|
199
|
+
parseCredentialHandoff
|
|
156
200
|
};
|
|
@@ -7,9 +7,11 @@ import {
|
|
|
7
7
|
isAdmissionErrorCode,
|
|
8
8
|
isIdentityErrorCode,
|
|
9
9
|
isLinkErrorCode,
|
|
10
|
+
parseCredentialHandoff,
|
|
11
|
+
parseCredentialRequest,
|
|
10
12
|
parseJobAccessLaunch,
|
|
11
13
|
stripPageBlock
|
|
12
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-N7BJKIPT.js";
|
|
13
15
|
|
|
14
16
|
// src/actions/directives.ts
|
|
15
17
|
var DIRECTIVE_LANG = "dewee-action";
|
|
@@ -846,10 +848,34 @@ var ApiClient = class {
|
|
|
846
848
|
return this.parse(res);
|
|
847
849
|
}
|
|
848
850
|
async history(agent, conversationId) {
|
|
851
|
+
return (await this.historyDetails(agent, conversationId)).messages;
|
|
852
|
+
}
|
|
853
|
+
async historyDetails(agent, conversationId) {
|
|
849
854
|
const res = await this.signedInRequest(
|
|
850
855
|
async () => this.request(this.url("history", { agent, conversationId }), { headers: await this.headers(true) })
|
|
851
856
|
);
|
|
852
|
-
|
|
857
|
+
const data = await this.parse(res);
|
|
858
|
+
const cards = this.session?.verified && this.config?.credentialFormOrigin && Array.isArray(data.credentialRequests) ? data.credentialRequests.slice(0, 100).map((v) => parseCredentialRequest(v, agent)).filter((v) => v !== null) : [];
|
|
859
|
+
return { messages: data.messages, credentialRequests: cards };
|
|
860
|
+
}
|
|
861
|
+
/** Never retry a credential action under a newly authenticated identity. */
|
|
862
|
+
async credentialRequest(route, agent, conversationId, credentialRequestId, signal) {
|
|
863
|
+
const session = await this.ensureSession();
|
|
864
|
+
const origin = (await this.getConfig()).credentialFormOrigin;
|
|
865
|
+
if (!session.verified || !origin) throw new ApiError("Credential request is unavailable", 403, "credential_request_unavailable");
|
|
866
|
+
try {
|
|
867
|
+
const headers = await this.headers(true);
|
|
868
|
+
if (this.session !== session) throw new Error("identity changed");
|
|
869
|
+
const response = await this.request(this.url(route), { method: "POST", headers, body: JSON.stringify({ agent, conversationId, credentialRequestId }), redirect: "error", signal });
|
|
870
|
+
const data = await this.parse(response);
|
|
871
|
+
if (this.session !== session) throw new Error("identity changed");
|
|
872
|
+
const result = route === "credential-request-handoff" ? parseCredentialHandoff(data, credentialRequestId, origin) : parseCredentialRequest(data.credentialRequest, agent);
|
|
873
|
+
if (!result || "id" in result && result.id !== credentialRequestId) throw new Error("invalid credential request");
|
|
874
|
+
return result;
|
|
875
|
+
} catch (error) {
|
|
876
|
+
const code = error instanceof ApiError && ["credential_requests_unavailable", "credential_request_not_found", "credential_request_not_ready", "credential_request_resumed"].includes(error.code ?? "") ? error.code : "credential_request_unavailable";
|
|
877
|
+
throw new ApiError("Credential request is unavailable", error instanceof ApiError ? error.status : 0, code);
|
|
878
|
+
}
|
|
853
879
|
}
|
|
854
880
|
/**
|
|
855
881
|
* Sends a request with the current session. A signed-in session the backend refuses (expired,
|
|
@@ -868,7 +894,7 @@ var ApiClient = class {
|
|
|
868
894
|
async chat(input, onEvent, signal) {
|
|
869
895
|
await this.ensureSession();
|
|
870
896
|
let res = await this.request(this.url("chat"), { method: "POST", headers: await this.headers(true), body: JSON.stringify(input), signal });
|
|
871
|
-
if (res.status === 401) {
|
|
897
|
+
if (res.status === 401 && !input.credentialRequestId) {
|
|
872
898
|
const before = this.visitorId();
|
|
873
899
|
this.forgetSession();
|
|
874
900
|
await this.ensureSession();
|
|
@@ -895,6 +921,11 @@ var ApiClient = class {
|
|
|
895
921
|
} catch {
|
|
896
922
|
continue;
|
|
897
923
|
}
|
|
924
|
+
if (event.type === "credential_request") {
|
|
925
|
+
const view = this.session?.verified && this.config?.credentialFormOrigin ? parseCredentialRequest(event.credentialRequest, input.agent) : null;
|
|
926
|
+
if (!view) continue;
|
|
927
|
+
event = { type: "credential_request", credentialRequest: view };
|
|
928
|
+
}
|
|
898
929
|
if (event.type === "error" && event.code && RENEW_CODES.has(event.code)) renew = true;
|
|
899
930
|
try {
|
|
900
931
|
onEvent(event);
|
|
@@ -1148,7 +1179,8 @@ var en = {
|
|
|
1148
1179
|
jobAccessPopupClosed: "The window was closed. Choose Job access again to retry.",
|
|
1149
1180
|
pageShare: "Share this page with the assistant for this message",
|
|
1150
1181
|
pageChanged: "The page changed. Review it and choose to share again before sending.",
|
|
1151
|
-
pageUnavailable: "Page details are unavailable. You can send without them."
|
|
1182
|
+
pageUnavailable: "Page details are unavailable. You can send without them.",
|
|
1183
|
+
credential: { open: "Open secure form", cancel: "Cancel request", continue: "Continue conversation", unavailable: "Could not open or update the request. Please try again.", intro: "Enter credentials only on the authenticated Dewee form.", required: "required", optional: "optional", expires: "Expires", status: { pending: "Waiting for you", applying: "Processing", resolved: "Completed", cancelled: "Cancelled", expired: "Expired", failed: "Failed" } }
|
|
1152
1184
|
};
|
|
1153
1185
|
var vi = {
|
|
1154
1186
|
title: "Tr\u1EE3 l\xFD",
|
|
@@ -1211,7 +1243,8 @@ var vi = {
|
|
|
1211
1243
|
jobAccessPopupClosed: "C\u1EEDa s\u1ED5 \u0111\xE3 \u0111\xF3ng. Ch\u1ECDn Quy\u1EC1n c\xF4ng vi\u1EC7c l\u1EA7n n\u1EEFa \u0111\u1EC3 th\u1EED l\u1EA1i.",
|
|
1212
1244
|
pageShare: "Chia s\u1EBB trang n\xE0y v\u1EDBi tr\u1EE3 l\xFD trong tin nh\u1EAFn n\xE0y",
|
|
1213
1245
|
pageChanged: "Trang \u0111\xE3 thay \u0111\u1ED5i. H\xE3y xem l\u1EA1i v\xE0 ch\u1ECDn chia s\u1EBB tr\u01B0\u1EDBc khi g\u1EEDi.",
|
|
1214
|
-
pageUnavailable: "Kh\xF4ng th\u1EC3 \u0111\u1ECDc n\u1ED9i dung trang. B\u1EA1n v\u1EABn c\xF3 th\u1EC3 g\u1EEDi tin nh\u1EAFn."
|
|
1246
|
+
pageUnavailable: "Kh\xF4ng th\u1EC3 \u0111\u1ECDc n\u1ED9i dung trang. B\u1EA1n v\u1EABn c\xF3 th\u1EC3 g\u1EEDi tin nh\u1EAFn.",
|
|
1247
|
+
credential: { open: "M\u1EDF bi\u1EC3u m\u1EABu b\u1EA3o m\u1EADt", cancel: "H\u1EE7y y\xEAu c\u1EA7u", continue: "Ti\u1EBFp t\u1EE5c tr\xF2 chuy\u1EC7n", unavailable: "Ch\u01B0a th\u1EC3 m\u1EDF ho\u1EB7c c\u1EADp nh\u1EADt y\xEAu c\u1EA7u. B\u1EA1n th\u1EED l\u1EA1i nh\xE9.", intro: "Ch\u1EC9 nh\u1EADp th\xF4ng tin x\xE1c th\u1EF1c tr\xEAn bi\u1EC3u m\u1EABu Dewee sau khi \u0111\u0103ng nh\u1EADp.", required: "b\u1EAFt bu\u1ED9c", optional: "t\xF9y ch\u1ECDn", expires: "H\u1EBFt h\u1EA1n", status: { pending: "\u0110ang ch\u1EDD b\u1EA1n", applying: "\u0110ang x\u1EED l\xFD", resolved: "\u0110\xE3 ho\xE0n t\u1EA5t", cancelled: "\u0110\xE3 h\u1EE7y", expired: "\u0110\xE3 h\u1EBFt h\u1EA1n", failed: "Kh\xF4ng th\xE0nh c\xF4ng" } }
|
|
1215
1248
|
};
|
|
1216
1249
|
function pickStrings(locale) {
|
|
1217
1250
|
const l = (locale || (typeof document !== "undefined" ? document.documentElement.lang : "") || (typeof navigator !== "undefined" ? navigator.language : "") || "en").toLowerCase();
|
|
@@ -1758,6 +1791,11 @@ details > summary svg.ic { width: 14px; height: 14px; flex: none; }
|
|
|
1758
1791
|
.btn.allow { background: var(--dw-accent); border-color: var(--dw-accent); color: var(--dw-accent-contrast); }
|
|
1759
1792
|
.btn.allow.danger { background: #c2334f; border-color: #c2334f; color: #fff; }
|
|
1760
1793
|
.btn:focus-visible { outline: 3px solid color-mix(in srgb, var(--dw-accent) 45%, transparent); outline-offset: 2px; }
|
|
1794
|
+
.credential-request .bubble { overflow-wrap: anywhere; max-width: 100%; }
|
|
1795
|
+
.credential-request p { margin: 8px 0; }
|
|
1796
|
+
.credential-request .spin { display: inline-block; vertical-align: middle; }
|
|
1797
|
+
.credential-request ul { margin: 8px 0; padding-inline-start: 20px; }
|
|
1798
|
+
.credential-actions { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
1761
1799
|
|
|
1762
1800
|
/* ---------- composer ---------- */
|
|
1763
1801
|
.composer { padding: 10px 12px 12px; border-top: 1px solid var(--dw-border); background: var(--dw-bg); }
|
|
@@ -1796,6 +1834,7 @@ textarea::placeholder { color: var(--dw-muted); }
|
|
|
1796
1834
|
|
|
1797
1835
|
// src/widget/element.ts
|
|
1798
1836
|
var DEFAULT_COOLDOWN_MS = 5e3;
|
|
1837
|
+
var CREDENTIAL_GONE_CODES = /* @__PURE__ */ new Set(["credential_requests_unavailable", "credential_request_not_found"]);
|
|
1799
1838
|
var BOOL_ATTRS = ["show-reasoning", "show-tool-calls", "allow-agent-switch", "open", "persist", "hide-branding", "launcher-avatar", "dom-actions"];
|
|
1800
1839
|
var ATTRS = [
|
|
1801
1840
|
"endpoint",
|
|
@@ -1878,6 +1917,11 @@ var DeweeChatElement = class extends ElementBase {
|
|
|
1878
1917
|
messages = [];
|
|
1879
1918
|
running = null;
|
|
1880
1919
|
jobAccessLaunch = null;
|
|
1920
|
+
credentialCards = /* @__PURE__ */ new Map();
|
|
1921
|
+
credentialGeneration = 0;
|
|
1922
|
+
credentialBusy = /* @__PURE__ */ new Set();
|
|
1923
|
+
credentialPopups = /* @__PURE__ */ new Map();
|
|
1924
|
+
credentialRequestsInFlight = /* @__PURE__ */ new Set();
|
|
1881
1925
|
activated = false;
|
|
1882
1926
|
probedPage = false;
|
|
1883
1927
|
strings = pickStrings();
|
|
@@ -2057,6 +2101,7 @@ var DeweeChatElement = class extends ElementBase {
|
|
|
2057
2101
|
}
|
|
2058
2102
|
/** One request/response with the agent. */
|
|
2059
2103
|
async runTurn(message, hidden, attachment) {
|
|
2104
|
+
const generation = this.credentialGeneration;
|
|
2060
2105
|
this.pushMessage({ role: "user", content: message, hidden });
|
|
2061
2106
|
const reply = { role: "assistant", content: "", status: "streaming", startedAt: Date.now() };
|
|
2062
2107
|
this.pushMessage(reply);
|
|
@@ -2064,7 +2109,9 @@ var DeweeChatElement = class extends ElementBase {
|
|
|
2064
2109
|
this.emit("dewee:send", { message, agent: this.agentId, conversationId: this.conversationId, auto: hidden || void 0 });
|
|
2065
2110
|
try {
|
|
2066
2111
|
await this.client().ensureSession();
|
|
2067
|
-
await this.client().chat({ ...this.chatBody(message), ...attachment ? { page_context: attachment } : {} }, (e) =>
|
|
2112
|
+
await this.client().chat({ ...this.chatBody(message), ...attachment ? { page_context: attachment } : {} }, (e) => {
|
|
2113
|
+
if (generation === this.credentialGeneration) this.onStreamEvent(reply, e);
|
|
2114
|
+
}, this.running.signal);
|
|
2068
2115
|
if (reply.status === "streaming") reply.status = "done";
|
|
2069
2116
|
} catch (e) {
|
|
2070
2117
|
if (e.name === "AbortError") {
|
|
@@ -2192,6 +2239,8 @@ var DeweeChatElement = class extends ElementBase {
|
|
|
2192
2239
|
*/
|
|
2193
2240
|
async renewSession() {
|
|
2194
2241
|
this.stop();
|
|
2242
|
+
this.clearCredentials();
|
|
2243
|
+
this.renderLog();
|
|
2195
2244
|
try {
|
|
2196
2245
|
await this.client().renewSession();
|
|
2197
2246
|
} catch (e) {
|
|
@@ -2284,6 +2333,7 @@ var DeweeChatElement = class extends ElementBase {
|
|
|
2284
2333
|
/** Starts a fresh conversation with the current agent. */
|
|
2285
2334
|
newConversation() {
|
|
2286
2335
|
this.stop();
|
|
2336
|
+
this.clearCredentials();
|
|
2287
2337
|
this.conversationId = randomConversationId();
|
|
2288
2338
|
if (this.config.persist !== false) store2.set(this.convKey(), this.conversationId);
|
|
2289
2339
|
this.messages = [];
|
|
@@ -2300,6 +2350,7 @@ var DeweeChatElement = class extends ElementBase {
|
|
|
2300
2350
|
}
|
|
2301
2351
|
}
|
|
2302
2352
|
disconnectedCallback() {
|
|
2353
|
+
this.clearCredentials();
|
|
2303
2354
|
this.pageGeneration++;
|
|
2304
2355
|
this.sharedPage = void 0;
|
|
2305
2356
|
this.els.pageChoice.checked = false;
|
|
@@ -2644,6 +2695,8 @@ var DeweeChatElement = class extends ElementBase {
|
|
|
2644
2695
|
await this.loadConversation();
|
|
2645
2696
|
}
|
|
2646
2697
|
async loadConversation() {
|
|
2698
|
+
this.clearCredentials();
|
|
2699
|
+
const generation = this.credentialGeneration;
|
|
2647
2700
|
const persist = this.config.persist !== false;
|
|
2648
2701
|
const saved = persist ? store2.get(this.convKey()) : null;
|
|
2649
2702
|
this.conversationId = saved || randomConversationId();
|
|
@@ -2652,8 +2705,10 @@ var DeweeChatElement = class extends ElementBase {
|
|
|
2652
2705
|
this.renderLog();
|
|
2653
2706
|
if (!saved) return;
|
|
2654
2707
|
try {
|
|
2655
|
-
const history = await this.client().
|
|
2656
|
-
|
|
2708
|
+
const history = await this.client().historyDetails(this.agentId, this.conversationId);
|
|
2709
|
+
if (generation !== this.credentialGeneration) return;
|
|
2710
|
+
this.messages = decorateHistory(history.messages.map((m) => ({ ...m, status: "done" })));
|
|
2711
|
+
for (const card of history.credentialRequests) this.credentialCards.set(card.id, card);
|
|
2657
2712
|
this.renderLog();
|
|
2658
2713
|
} catch {
|
|
2659
2714
|
}
|
|
@@ -2664,8 +2719,202 @@ var DeweeChatElement = class extends ElementBase {
|
|
|
2664
2719
|
log.replaceChildren();
|
|
2665
2720
|
if (!this.messages.length) log.appendChild(this.renderWelcome());
|
|
2666
2721
|
for (const m of this.messages) log.appendChild(this.renderMessage(m));
|
|
2722
|
+
for (const card of this.credentialCards.values()) log.appendChild(this.renderCredential(card));
|
|
2667
2723
|
log.scrollTop = log.scrollHeight;
|
|
2668
2724
|
}
|
|
2725
|
+
clearCredentials() {
|
|
2726
|
+
this.credentialGeneration++;
|
|
2727
|
+
for (const id of this.credentialPopups.keys()) this.closeCredentialPopup(id);
|
|
2728
|
+
for (const request of this.credentialRequestsInFlight) request.abort();
|
|
2729
|
+
this.credentialRequestsInFlight.clear();
|
|
2730
|
+
this.credentialCards.clear();
|
|
2731
|
+
this.credentialBusy.clear();
|
|
2732
|
+
}
|
|
2733
|
+
closeCredentialPopup(id) {
|
|
2734
|
+
const active = this.credentialPopups.get(id);
|
|
2735
|
+
if (!active) return;
|
|
2736
|
+
this.credentialPopups.delete(id);
|
|
2737
|
+
if (active.timer) clearTimeout(active.timer);
|
|
2738
|
+
active.controller.abort();
|
|
2739
|
+
try {
|
|
2740
|
+
active.popup.close();
|
|
2741
|
+
} catch {
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
discardCredential(id) {
|
|
2745
|
+
this.closeCredentialPopup(id);
|
|
2746
|
+
this.credentialCards.delete(id);
|
|
2747
|
+
}
|
|
2748
|
+
credentialButton(card, label, run, disabled = false) {
|
|
2749
|
+
const button = document.createElement("button");
|
|
2750
|
+
button.type = "button";
|
|
2751
|
+
button.className = "btn";
|
|
2752
|
+
button.textContent = label;
|
|
2753
|
+
button.disabled = disabled || this.credentialBusy.has(card.id);
|
|
2754
|
+
button.addEventListener("click", run);
|
|
2755
|
+
return button;
|
|
2756
|
+
}
|
|
2757
|
+
renderCredential(card) {
|
|
2758
|
+
const root = document.createElement("section");
|
|
2759
|
+
root.className = "msg assistant credential-request";
|
|
2760
|
+
const bubble = document.createElement("div");
|
|
2761
|
+
bubble.className = "bubble";
|
|
2762
|
+
const title = document.createElement("strong");
|
|
2763
|
+
title.textContent = card.title;
|
|
2764
|
+
const purpose = document.createElement("p");
|
|
2765
|
+
purpose.textContent = card.purpose;
|
|
2766
|
+
const intro = document.createElement("p");
|
|
2767
|
+
intro.textContent = this.strings.credential.intro;
|
|
2768
|
+
const destination = document.createElement("p");
|
|
2769
|
+
destination.textContent = [card.target.type, card.target.mode].filter(Boolean).join(" \xB7 ");
|
|
2770
|
+
const status = document.createElement("p");
|
|
2771
|
+
status.setAttribute("role", "status");
|
|
2772
|
+
const effective = card.status === "pending" && Date.parse(card.expires_at) <= Date.now() ? "expired" : card.status;
|
|
2773
|
+
if (this.credentialBusy.has(card.id)) {
|
|
2774
|
+
root.setAttribute("aria-busy", "true");
|
|
2775
|
+
const spinner = document.createElement("span");
|
|
2776
|
+
spinner.className = "spin";
|
|
2777
|
+
spinner.setAttribute("aria-hidden", "true");
|
|
2778
|
+
status.append(spinner, ` ${this.strings.credential.status.applying}`);
|
|
2779
|
+
} else status.textContent = this.strings.credential.status[effective];
|
|
2780
|
+
const list = document.createElement("ul");
|
|
2781
|
+
for (const field of card.fields) {
|
|
2782
|
+
const li = document.createElement("li");
|
|
2783
|
+
li.textContent = `${field.label} (${field.required ? this.strings.credential.required : this.strings.credential.optional})`;
|
|
2784
|
+
list.appendChild(li);
|
|
2785
|
+
}
|
|
2786
|
+
const expiry = document.createElement("p");
|
|
2787
|
+
expiry.textContent = `${this.strings.credential.expires}: ${new Date(card.expires_at).toLocaleString()}`;
|
|
2788
|
+
const actions = document.createElement("div");
|
|
2789
|
+
actions.className = "credential-actions";
|
|
2790
|
+
if (effective === "pending") {
|
|
2791
|
+
actions.append(
|
|
2792
|
+
this.credentialButton(card, this.strings.credential.open, () => {
|
|
2793
|
+
void this.openCredential(card);
|
|
2794
|
+
}, this.credentialPopups.has(card.id)),
|
|
2795
|
+
this.credentialButton(card, this.strings.credential.cancel, () => {
|
|
2796
|
+
void this.updateCredential(card, "credential-request-cancel");
|
|
2797
|
+
})
|
|
2798
|
+
);
|
|
2799
|
+
}
|
|
2800
|
+
if (card.resumable) actions.append(this.credentialButton(card, this.strings.credential.continue, () => {
|
|
2801
|
+
void this.resumeCredential(card);
|
|
2802
|
+
}));
|
|
2803
|
+
bubble.append(title, purpose, destination, list, intro, expiry, status, actions);
|
|
2804
|
+
root.appendChild(bubble);
|
|
2805
|
+
return root;
|
|
2806
|
+
}
|
|
2807
|
+
async updateCredential(card, route) {
|
|
2808
|
+
if (!this.credentialCards.has(card.id) || this.credentialBusy.has(card.id)) return;
|
|
2809
|
+
const generation = this.credentialGeneration, agent = this.agentId, conversation = this.conversationId;
|
|
2810
|
+
const controller = new AbortController();
|
|
2811
|
+
this.credentialRequestsInFlight.add(controller);
|
|
2812
|
+
this.credentialBusy.add(card.id);
|
|
2813
|
+
this.renderLog();
|
|
2814
|
+
try {
|
|
2815
|
+
const result = await this.client().credentialRequest(route, agent, conversation, card.id, controller.signal);
|
|
2816
|
+
if (generation !== this.credentialGeneration || !("id" in result)) return;
|
|
2817
|
+
this.credentialCards.set(result.id, result);
|
|
2818
|
+
if (route === "credential-request-cancel") this.closeCredentialPopup(card.id);
|
|
2819
|
+
} catch (error) {
|
|
2820
|
+
if (generation === this.credentialGeneration && error instanceof ApiError && CREDENTIAL_GONE_CODES.has(error.code ?? "")) this.discardCredential(card.id);
|
|
2821
|
+
else if (generation === this.credentialGeneration && route !== "credential-request") this.showNotice(this.strings.credential.unavailable);
|
|
2822
|
+
} finally {
|
|
2823
|
+
this.credentialRequestsInFlight.delete(controller);
|
|
2824
|
+
if (generation === this.credentialGeneration) {
|
|
2825
|
+
this.credentialBusy.delete(card.id);
|
|
2826
|
+
this.renderLog();
|
|
2827
|
+
}
|
|
2828
|
+
}
|
|
2829
|
+
}
|
|
2830
|
+
async openCredential(card) {
|
|
2831
|
+
if (this.credentialCards.get(card.id) !== card || this.credentialBusy.has(card.id) || this.credentialPopups.has(card.id)) return;
|
|
2832
|
+
let popup;
|
|
2833
|
+
try {
|
|
2834
|
+
popup = window.open("about:blank", `dewee-credential-${crypto.randomUUID()}`, "popup,width=720,height=800");
|
|
2835
|
+
} catch {
|
|
2836
|
+
popup = null;
|
|
2837
|
+
}
|
|
2838
|
+
if (!popup) {
|
|
2839
|
+
this.showNotice(this.strings.credential.unavailable);
|
|
2840
|
+
return;
|
|
2841
|
+
}
|
|
2842
|
+
const generation = this.credentialGeneration, agent = this.agentId, conversation = this.conversationId;
|
|
2843
|
+
const active = { popup, controller: new AbortController(), timer: void 0 };
|
|
2844
|
+
this.credentialPopups.set(card.id, active);
|
|
2845
|
+
this.credentialBusy.add(card.id);
|
|
2846
|
+
this.renderLog();
|
|
2847
|
+
try {
|
|
2848
|
+
popup.opener = null;
|
|
2849
|
+
const result = await this.client().credentialRequest("credential-request-handoff", agent, conversation, card.id, active.controller.signal);
|
|
2850
|
+
if (generation !== this.credentialGeneration || popup.closed || !("url" in result)) throw new Error("cancelled");
|
|
2851
|
+
const meta = popup.document.createElement("meta");
|
|
2852
|
+
meta.name = "referrer";
|
|
2853
|
+
meta.content = "no-referrer";
|
|
2854
|
+
popup.document.head.append(meta);
|
|
2855
|
+
popup.location.replace(result.url);
|
|
2856
|
+
const poll = async () => {
|
|
2857
|
+
if (generation !== this.credentialGeneration) return;
|
|
2858
|
+
const before = this.credentialCards.get(card.id);
|
|
2859
|
+
if (before && (before.status === "pending" || before.status === "applying")) await this.updateCredential(before, "credential-request");
|
|
2860
|
+
const current = this.credentialCards.get(card.id);
|
|
2861
|
+
if (generation === this.credentialGeneration && !popup.closed && current && Date.parse(current.expires_at) > Date.now() && this.credentialPopups.get(card.id) === active) {
|
|
2862
|
+
if (current.status === "pending" || current.status === "applying") active.timer = setTimeout(poll, 4e3);
|
|
2863
|
+
else {
|
|
2864
|
+
this.closeCredentialPopup(card.id);
|
|
2865
|
+
this.renderLog();
|
|
2866
|
+
}
|
|
2867
|
+
} else {
|
|
2868
|
+
if (this.credentialPopups.get(card.id) === active) this.closeCredentialPopup(card.id);
|
|
2869
|
+
if (generation === this.credentialGeneration) this.renderLog();
|
|
2870
|
+
}
|
|
2871
|
+
};
|
|
2872
|
+
active.timer = setTimeout(poll, 4e3);
|
|
2873
|
+
} catch {
|
|
2874
|
+
if (this.credentialPopups.get(card.id) === active) this.closeCredentialPopup(card.id);
|
|
2875
|
+
if (generation === this.credentialGeneration) this.showNotice(this.strings.credential.unavailable);
|
|
2876
|
+
} finally {
|
|
2877
|
+
if (generation === this.credentialGeneration) {
|
|
2878
|
+
this.credentialBusy.delete(card.id);
|
|
2879
|
+
this.renderLog();
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
}
|
|
2883
|
+
async resumeCredential(card) {
|
|
2884
|
+
if (this.credentialCards.get(card.id) !== card || this.running || this.credentialBusy.has(card.id)) return;
|
|
2885
|
+
const generation = this.credentialGeneration, agent = this.agentId, conversation = this.conversationId;
|
|
2886
|
+
const reply = { role: "assistant", content: "", status: "streaming", startedAt: Date.now() };
|
|
2887
|
+
const controller = new AbortController();
|
|
2888
|
+
this.credentialBusy.add(card.id);
|
|
2889
|
+
this.pushMessage(reply);
|
|
2890
|
+
this.setRunning(controller);
|
|
2891
|
+
try {
|
|
2892
|
+
await this.client().chat({ message: "", agent, conversationId: conversation, credentialRequestId: card.id }, (event) => {
|
|
2893
|
+
if (generation === this.credentialGeneration) this.onStreamEvent(reply, event);
|
|
2894
|
+
}, this.running.signal);
|
|
2895
|
+
if (reply.status === "streaming") reply.status = "done";
|
|
2896
|
+
if (generation === this.credentialGeneration) {
|
|
2897
|
+
this.credentialBusy.delete(card.id);
|
|
2898
|
+
await this.updateCredential(card, "credential-request");
|
|
2899
|
+
}
|
|
2900
|
+
} catch (error) {
|
|
2901
|
+
if (generation === this.credentialGeneration) {
|
|
2902
|
+
this.messages = this.messages.filter((message) => message !== reply);
|
|
2903
|
+
if (error instanceof ApiError && CREDENTIAL_GONE_CODES.has(error.code ?? "")) this.discardCredential(card.id);
|
|
2904
|
+
else if (error instanceof ApiError && error.code === "credential_request_resumed") {
|
|
2905
|
+
this.credentialBusy.delete(card.id);
|
|
2906
|
+
await this.updateCredential(card, "credential-request");
|
|
2907
|
+
} else this.showNotice(this.strings.credential.unavailable);
|
|
2908
|
+
}
|
|
2909
|
+
} finally {
|
|
2910
|
+
if (this.running === controller) this.setRunning(null);
|
|
2911
|
+
if (generation === this.credentialGeneration) {
|
|
2912
|
+
this.credentialBusy.delete(card.id);
|
|
2913
|
+
this.updateMessage(reply);
|
|
2914
|
+
this.renderLog();
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
2917
|
+
}
|
|
2669
2918
|
renderWelcome() {
|
|
2670
2919
|
const c = this.config;
|
|
2671
2920
|
const s = this.strings;
|
|
@@ -2852,6 +3101,10 @@ var DeweeChatElement = class extends ElementBase {
|
|
|
2852
3101
|
t.isError = e.isError;
|
|
2853
3102
|
break;
|
|
2854
3103
|
}
|
|
3104
|
+
case "credential_request":
|
|
3105
|
+
this.credentialCards.set(e.credentialRequest.id, e.credentialRequest);
|
|
3106
|
+
this.renderLog();
|
|
3107
|
+
break;
|
|
2855
3108
|
case "done":
|
|
2856
3109
|
this.finishThinking(m);
|
|
2857
3110
|
if (e.content && e.content.length >= m.content.length) m.content = e.content;
|