@coffer-org/plugin-webchat 7.0.1 → 7.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ChatWidget-BhGpxS9f.js +2179 -0
- package/dist/index.js +2 -0
- package/dist/runtime/actions.d.ts +1 -1
- package/dist/runtime/actions.js +40 -24
- package/dist/runtime/channel-registry.d.ts +11 -4
- package/dist/runtime/channel-registry.js +91 -13
- package/dist/runtime/connector.d.ts +3 -7
- package/dist/runtime/connector.js +14 -55
- package/dist/runtime/format.d.ts +0 -1
- package/dist/runtime/format.js +2 -11
- package/dist/runtime/index.d.ts +1 -2
- package/dist/runtime/index.js +6 -7
- package/dist/runtime/send.d.ts +2 -0
- package/dist/runtime/send.js +54 -29
- package/dist/schema.js +133 -84
- package/dist/web.js +1 -1
- package/package.json +5 -6
- package/dist/ChatWidget-DkEKl1-2.js +0 -24846
package/dist/runtime/send.js
CHANGED
|
@@ -2,14 +2,18 @@ import { randomUUID } from 'node:crypto';
|
|
|
2
2
|
import { handleIncoming, liveAgentId } from '@coffer-org/server/orchestrator';
|
|
3
3
|
import { beginTurn } from '@coffer-org/server/turn-gate';
|
|
4
4
|
import { HttpError } from '@coffer-org/server/plugin-hooks';
|
|
5
|
-
import {
|
|
5
|
+
import { lastMessageId, conversationEmitter, onMessage, } from '@coffer-org/server/conversation-store';
|
|
6
|
+
import { getConversation, setConversation, readAndTouchConversation, } from '@coffer-org/server/conversation-store';
|
|
7
|
+
import { mayWrite, mayRead } from '@coffer-org/server/orchestrator';
|
|
6
8
|
import { policy } from "./config.js";
|
|
7
9
|
import { makeStreamingConnector } from "./connector.js";
|
|
8
|
-
import { broadcast } from "./channel-registry.js";
|
|
10
|
+
import { broadcast, initFanout } from "./channel-registry.js";
|
|
9
11
|
import { webChannelSystem } from "./format.js";
|
|
10
12
|
import { loadReasoningDisplay } from "./settings.js";
|
|
11
13
|
import { getLogger } from '@coffer-org/sdk/logger';
|
|
12
14
|
const log = getLogger('webchat');
|
|
15
|
+
initFanout();
|
|
16
|
+
export { conversationEmitter };
|
|
13
17
|
export const CAPABILITIES = {
|
|
14
18
|
events: ['delta', 'reasoning', 'segment', 'suggestions', 'title'],
|
|
15
19
|
privateChats: true,
|
|
@@ -39,59 +43,73 @@ export async function sendAction(body, caller, deps = {}) {
|
|
|
39
43
|
throw new HttpError(400, 'missing required field: convId');
|
|
40
44
|
if (!text.trim())
|
|
41
45
|
throw new HttpError(400, 'missing required field: text');
|
|
42
|
-
const
|
|
43
|
-
if (
|
|
46
|
+
const conversationId = await openConversation(convId, caller.id, 'write');
|
|
47
|
+
if (conversationId === null) {
|
|
44
48
|
return { msgId: null, botMsgId: null };
|
|
45
49
|
}
|
|
46
|
-
const selection = await
|
|
50
|
+
const selection = await readAndTouchConversation(conversationId);
|
|
47
51
|
if (selection.owner === null) {
|
|
48
|
-
await
|
|
52
|
+
await setConversation(conversationId, { owner: caller.id });
|
|
49
53
|
}
|
|
50
54
|
const agentId = liveAgentId(selection.agentId);
|
|
51
55
|
const msgId = randomUUID();
|
|
52
56
|
const botMsgId = randomUUID();
|
|
53
|
-
const nowSec = Math.floor(Date.now() / 1000);
|
|
54
57
|
const attachments = parseAttachments(body['attachments']);
|
|
55
58
|
const explicitReplyTo = typeof body['replyTo'] === 'string' && body['replyTo'] ? body['replyTo'] : null;
|
|
56
|
-
const replyTo = explicitReplyTo ?? (await
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const
|
|
59
|
+
const replyTo = explicitReplyTo ?? (await lastMessageId(conversationId));
|
|
60
|
+
let userMsgId = msgId;
|
|
61
|
+
let userMsgResolved = false;
|
|
62
|
+
let resolveUserMsg;
|
|
63
|
+
const userMsgPromise = new Promise((resolve) => {
|
|
64
|
+
resolveUserMsg = resolve;
|
|
65
|
+
});
|
|
66
|
+
const unsubUserMsg = onMessage((m) => {
|
|
67
|
+
if (!userMsgResolved && m.conversationId === conversationId && m.role === 'user') {
|
|
68
|
+
userMsgResolved = true;
|
|
69
|
+
resolveUserMsg(m.msgId);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
61
72
|
const { connector, recorded, suggestions } = makeStreamingConnector({
|
|
62
|
-
|
|
73
|
+
conversationId,
|
|
63
74
|
botMsgId,
|
|
64
75
|
display: await loadReasoningDisplay(),
|
|
65
76
|
});
|
|
66
77
|
const turnContext = pageContext(body['context']);
|
|
67
|
-
const gate = await beginTurn('webchat',
|
|
68
|
-
|
|
78
|
+
const gate = await beginTurn('webchat', conversationId, { supersede: true });
|
|
79
|
+
const turnPromise = (async () => {
|
|
69
80
|
try {
|
|
70
81
|
await doHandle(connector, {
|
|
71
|
-
envelope: { connectorId: 'webchat',
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
82
|
+
envelope: { connectorId: 'webchat', conversationId, turnId: msgId },
|
|
83
|
+
sender: { userId: Number(caller.id) },
|
|
84
|
+
message: {
|
|
85
|
+
text,
|
|
86
|
+
...(attachments?.length ? { attachments } : {}),
|
|
87
|
+
replyToMsgId: replyTo,
|
|
77
88
|
},
|
|
89
|
+
capabilities: CAPABILITIES,
|
|
90
|
+
systemPrompt: { channel: [await webChannelSystem()] },
|
|
78
91
|
...(turnContext.length ? { turnContext } : {}),
|
|
79
92
|
...(agentId ? { agentId } : {}),
|
|
80
93
|
...(selection.presetId ? { presetId: selection.presetId } : {}),
|
|
81
|
-
sender: { id: caller.id, idKind: 'coffer-user' },
|
|
82
94
|
signal: gate.signal,
|
|
83
95
|
}, { policy: policy() });
|
|
84
|
-
broadcast(
|
|
96
|
+
broadcast(conversationId, 'done', {
|
|
97
|
+
msgId: recorded() ?? botMsgId,
|
|
98
|
+
parentMsgId: userMsgId,
|
|
99
|
+
suggestions: suggestions(),
|
|
100
|
+
});
|
|
85
101
|
}
|
|
86
102
|
catch (err) {
|
|
87
|
-
log.error(`sendAction: handleIncoming threw for chat ${
|
|
88
|
-
broadcast(
|
|
103
|
+
log.error(`sendAction: handleIncoming threw for chat ${conversationId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
104
|
+
broadcast(conversationId, 'error', { message: null, parentMsgId: userMsgId });
|
|
89
105
|
}
|
|
90
106
|
finally {
|
|
91
107
|
gate.end();
|
|
92
108
|
}
|
|
93
109
|
})();
|
|
94
|
-
|
|
110
|
+
userMsgId = await Promise.race([userMsgPromise, turnPromise.then(() => msgId)]);
|
|
111
|
+
unsubUserMsg();
|
|
112
|
+
return { msgId: userMsgId, botMsgId };
|
|
95
113
|
}
|
|
96
114
|
function parseAttachments(value) {
|
|
97
115
|
if (!Array.isArray(value))
|
|
@@ -105,11 +123,18 @@ function parseAttachments(value) {
|
|
|
105
123
|
return [
|
|
106
124
|
{
|
|
107
125
|
name: r['name'],
|
|
108
|
-
...(typeof r['mime'] === 'string' ? { mime: r['mime'] } : {}),
|
|
109
|
-
...(typeof r['size'] === 'number' ? { size: r['size'] } : {}),
|
|
110
|
-
...(typeof r['label'] === 'string' ? { label: r['label'] } : {}),
|
|
126
|
+
...(typeof r['mime'] === 'string' && r['mime'] ? { mime: r['mime'] } : {}),
|
|
127
|
+
...(typeof r['size'] === 'number' && Number.isFinite(r['size']) ? { size: r['size'] } : {}),
|
|
128
|
+
...(typeof r['label'] === 'string' && r['label'] ? { label: r['label'] } : {}),
|
|
111
129
|
},
|
|
112
130
|
];
|
|
113
131
|
});
|
|
114
132
|
return refs.length ? refs : undefined;
|
|
115
133
|
}
|
|
134
|
+
async function openConversation(convId, viewerId, need) {
|
|
135
|
+
if (!convId || convId.includes(':'))
|
|
136
|
+
return null;
|
|
137
|
+
const conv = await getConversation(convId);
|
|
138
|
+
const allowed = need === 'read' ? mayRead(conv, viewerId) : mayWrite(conv, viewerId);
|
|
139
|
+
return allowed ? convId : null;
|
|
140
|
+
}
|
package/dist/schema.js
CHANGED
|
@@ -24,6 +24,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
24
24
|
//#region ../sdk/src/plugin.ts
|
|
25
25
|
function definePlugin(p) {
|
|
26
26
|
if (!p.id) throw new Error("[plugin] missing id");
|
|
27
|
+
if (!p.label) throw new Error(`[plugin] ${p.id}: missing label`);
|
|
28
|
+
if (!p.description) throw new Error(`[plugin] ${p.id}: missing description`);
|
|
27
29
|
if (!p.version) console.warn(`[plugin] ${p.id}: missing version`);
|
|
28
30
|
return p;
|
|
29
31
|
}
|
|
@@ -4134,16 +4136,18 @@ function jsonRefined(inner, code) {
|
|
|
4134
4136
|
});
|
|
4135
4137
|
});
|
|
4136
4138
|
}
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4139
|
+
/**
|
|
4140
|
+
* An empty value is no value: `''` and `null` become `undefined`, and the field's own schema
|
|
4141
|
+
* never sees them. Every field is optional, so this is the ONLY shape — a field cannot demand
|
|
4142
|
+
* a value, and the rules it carries judge what was entered rather than whether anything was.
|
|
4143
|
+
*
|
|
4144
|
+
* This used to have a second branch, for `required` fields, which turned the same emptiness
|
|
4145
|
+
* into a `required` issue through a `z.unknown()` guard piped into the schema. That branch is
|
|
4146
|
+
* why a `.min(1)` on a required string was unreachable: the empty string had already been
|
|
4147
|
+
* converted to the issue before the string schema ran.
|
|
4148
|
+
*/
|
|
4149
|
+
function optionalize(schema) {
|
|
4150
|
+
return preprocess((v) => v === "" || v === null ? void 0 : v, schema.optional());
|
|
4147
4151
|
}
|
|
4148
4152
|
//#endregion
|
|
4149
4153
|
//#region ../sdk/src/fields/meta.ts
|
|
@@ -4225,7 +4229,7 @@ function applyMultiple(base, multiple) {
|
|
|
4225
4229
|
multiple: true
|
|
4226
4230
|
},
|
|
4227
4231
|
json: true,
|
|
4228
|
-
zod: optionalize(s
|
|
4232
|
+
zod: optionalize(s)
|
|
4229
4233
|
};
|
|
4230
4234
|
}
|
|
4231
4235
|
/** Inline option entry → OptionItem (plain string = value and label at once). */
|
|
@@ -4340,13 +4344,6 @@ function wrapKey(key, opts, meta) {
|
|
|
4340
4344
|
noLabel: true
|
|
4341
4345
|
}
|
|
4342
4346
|
};
|
|
4343
|
-
if (opts.role) m = {
|
|
4344
|
-
...m,
|
|
4345
|
-
hints: {
|
|
4346
|
-
...m.hints,
|
|
4347
|
-
role: opts.role
|
|
4348
|
-
}
|
|
4349
|
-
};
|
|
4350
4347
|
if (opts.editor) m = {
|
|
4351
4348
|
...m,
|
|
4352
4349
|
hints: {
|
|
@@ -4805,7 +4802,6 @@ function normalizeOpts(rawIn) {
|
|
|
4805
4802
|
faces: v.faces,
|
|
4806
4803
|
emphasis: v.emphasis,
|
|
4807
4804
|
noLabel: v.noLabel,
|
|
4808
|
-
role: v.role,
|
|
4809
4805
|
editor: v.editor,
|
|
4810
4806
|
activate: v.activate
|
|
4811
4807
|
};
|
|
@@ -4968,13 +4964,10 @@ function email(o) {
|
|
|
4968
4964
|
registerPreset("email", "string", {}, {
|
|
4969
4965
|
kind: "email",
|
|
4970
4966
|
widget: "email",
|
|
4971
|
-
build(
|
|
4972
|
-
const required = o.required ?? false;
|
|
4973
|
-
let s = string$1().email({ message: vmsg("email") });
|
|
4974
|
-
if (required) s = s.min(1, { message: vmsg("min_length", { min: 1 }) });
|
|
4967
|
+
build() {
|
|
4975
4968
|
return {
|
|
4976
4969
|
column: "text",
|
|
4977
|
-
zod:
|
|
4970
|
+
zod: string$1().email({ message: vmsg("email") }),
|
|
4978
4971
|
hints: {
|
|
4979
4972
|
format: "email",
|
|
4980
4973
|
inputType: "email"
|
|
@@ -4997,12 +4990,10 @@ function tel(o) {
|
|
|
4997
4990
|
registerPreset("tel", "string", {}, {
|
|
4998
4991
|
kind: "tel",
|
|
4999
4992
|
widget: "tel",
|
|
5000
|
-
build(
|
|
5001
|
-
const required = o.required ?? false;
|
|
5002
|
-
const base_z = string$1().regex(TEL_RE, { message: vmsg("pattern", { messageKey: "core.presets.tel" }) });
|
|
4993
|
+
build() {
|
|
5003
4994
|
return {
|
|
5004
4995
|
column: "text",
|
|
5005
|
-
zod:
|
|
4996
|
+
zod: string$1().regex(TEL_RE, { message: vmsg("pattern", { messageKey: "core.presets.tel" }) }),
|
|
5006
4997
|
hints: {
|
|
5007
4998
|
format: "tel",
|
|
5008
4999
|
inputType: "tel"
|
|
@@ -5026,10 +5017,10 @@ registerPreset("password", "string", {}, {
|
|
|
5026
5017
|
kind: "password",
|
|
5027
5018
|
widget: "password",
|
|
5028
5019
|
selfManages: /* @__PURE__ */ new Set(["multiple"]),
|
|
5029
|
-
build(
|
|
5020
|
+
build() {
|
|
5030
5021
|
return {
|
|
5031
5022
|
column: "text",
|
|
5032
|
-
zod:
|
|
5023
|
+
zod: string$1(),
|
|
5033
5024
|
hints: {
|
|
5034
5025
|
format: "password",
|
|
5035
5026
|
inputType: "password"
|
|
@@ -5091,6 +5082,31 @@ function internalOauthGrants(o) {
|
|
|
5091
5082
|
}
|
|
5092
5083
|
});
|
|
5093
5084
|
}
|
|
5085
|
+
/**
|
|
5086
|
+
* The account page's linked-accounts section. Same trick as `internalApiToken`: a container
|
|
5087
|
+
* with a custom `ui.kind`, drawn by its own renderer, which reads `/api/auth/links` itself.
|
|
5088
|
+
* The declared children are what one ROW holds — the renderer reads them through the slot
|
|
5089
|
+
* contract, so this is a declaration, not a layout.
|
|
5090
|
+
*
|
|
5091
|
+
* @layer preset
|
|
5092
|
+
* @base group
|
|
5093
|
+
* @prim —
|
|
5094
|
+
* @widget internalIdentityLinks
|
|
5095
|
+
* @example f.internalIdentityLinks({ label: 'auth.linksTitle' })
|
|
5096
|
+
*/
|
|
5097
|
+
function internalIdentityLinks(o) {
|
|
5098
|
+
return group({
|
|
5099
|
+
scope: "nest",
|
|
5100
|
+
label: o.label,
|
|
5101
|
+
multiple: true,
|
|
5102
|
+
ui: { kind: "internalIdentityLinks" },
|
|
5103
|
+
fields: {
|
|
5104
|
+
provider: string({}),
|
|
5105
|
+
externalId: string({}),
|
|
5106
|
+
linkedAt: string({})
|
|
5107
|
+
}
|
|
5108
|
+
});
|
|
5109
|
+
}
|
|
5094
5110
|
var SLUG_RE = /^[a-z0-9-]+$/;
|
|
5095
5111
|
/**
|
|
5096
5112
|
* Slug — kind 'slug', `^[a-z0-9-]+$`.
|
|
@@ -5211,10 +5227,10 @@ function colorname(o) {
|
|
|
5211
5227
|
registerPreset("colorname", "string", {}, {
|
|
5212
5228
|
kind: "colorname",
|
|
5213
5229
|
widget: "colorname",
|
|
5214
|
-
build(
|
|
5230
|
+
build() {
|
|
5215
5231
|
return {
|
|
5216
5232
|
column: "text",
|
|
5217
|
-
zod:
|
|
5233
|
+
zod: string$1().refine((v) => CSS_COLOR_NAMES.has(v.toLowerCase()), { message: vmsg("colorname") }),
|
|
5218
5234
|
hints: {}
|
|
5219
5235
|
};
|
|
5220
5236
|
}
|
|
@@ -5234,10 +5250,10 @@ function title(o) {
|
|
|
5234
5250
|
registerPreset("title", "string", {}, {
|
|
5235
5251
|
kind: "title",
|
|
5236
5252
|
widget: "title",
|
|
5237
|
-
build(
|
|
5253
|
+
build() {
|
|
5238
5254
|
return {
|
|
5239
5255
|
column: "text",
|
|
5240
|
-
zod:
|
|
5256
|
+
zod: string$1(),
|
|
5241
5257
|
hints: {}
|
|
5242
5258
|
};
|
|
5243
5259
|
}
|
|
@@ -5258,12 +5274,10 @@ function link(o) {
|
|
|
5258
5274
|
registerPreset("link", "string", {}, {
|
|
5259
5275
|
kind: "link",
|
|
5260
5276
|
widget: "link",
|
|
5261
|
-
build(
|
|
5262
|
-
const required = o.required ?? false;
|
|
5263
|
-
const base_z = string$1().regex(LINK_RE, { message: vmsg("pattern", { messageKey: "core.presets.link" }) });
|
|
5277
|
+
build() {
|
|
5264
5278
|
return {
|
|
5265
5279
|
column: "text",
|
|
5266
|
-
zod:
|
|
5280
|
+
zod: string$1().regex(LINK_RE, { message: vmsg("pattern", { messageKey: "core.presets.link" }) }),
|
|
5267
5281
|
hints: {
|
|
5268
5282
|
format: "url",
|
|
5269
5283
|
inputType: "url"
|
|
@@ -5948,6 +5962,7 @@ var presets = {
|
|
|
5948
5962
|
currency,
|
|
5949
5963
|
money,
|
|
5950
5964
|
internalApiToken,
|
|
5965
|
+
internalIdentityLinks,
|
|
5951
5966
|
internalOauthGrants
|
|
5952
5967
|
};
|
|
5953
5968
|
//#endregion
|
|
@@ -8062,7 +8077,6 @@ function group(o) {
|
|
|
8062
8077
|
scope,
|
|
8063
8078
|
multiple: o.multiple,
|
|
8064
8079
|
compute: o.value,
|
|
8065
|
-
required: o.required,
|
|
8066
8080
|
unique: r.unique,
|
|
8067
8081
|
label: o.label,
|
|
8068
8082
|
icon: o.icon,
|
|
@@ -8095,7 +8109,6 @@ function row(o) {
|
|
|
8095
8109
|
icon: o.icon,
|
|
8096
8110
|
fields: o.fields,
|
|
8097
8111
|
multiple: o.multiple,
|
|
8098
|
-
required: o.required,
|
|
8099
8112
|
rules: o.rules,
|
|
8100
8113
|
ui: { display: "scroll" }
|
|
8101
8114
|
});
|
|
@@ -8143,7 +8156,6 @@ function url(o) {
|
|
|
8143
8156
|
return group({
|
|
8144
8157
|
scope: "nest",
|
|
8145
8158
|
label: o.label,
|
|
8146
|
-
required: o.required,
|
|
8147
8159
|
ui: { kind: "url" },
|
|
8148
8160
|
fields: {
|
|
8149
8161
|
scheme: string({}),
|
|
@@ -8195,7 +8207,6 @@ function keyed(o) {
|
|
|
8195
8207
|
}),
|
|
8196
8208
|
scope: "nest",
|
|
8197
8209
|
multiple: true,
|
|
8198
|
-
required: o.required,
|
|
8199
8210
|
unique: o.unique ?? [role],
|
|
8200
8211
|
fixed: o.fixed,
|
|
8201
8212
|
view: { by: role }
|
|
@@ -8316,7 +8327,7 @@ registerType("string", {
|
|
|
8316
8327
|
widget: "text",
|
|
8317
8328
|
build(o) {
|
|
8318
8329
|
const cfg = o.config ?? {};
|
|
8319
|
-
const min = cfg.min ??
|
|
8330
|
+
const min = cfg.min ?? 0;
|
|
8320
8331
|
return {
|
|
8321
8332
|
column: "text",
|
|
8322
8333
|
zod: stringContent((str) => {
|
|
@@ -8344,9 +8355,9 @@ registerType("string", {
|
|
|
8344
8355
|
function string(o) {
|
|
8345
8356
|
return declare("string", o);
|
|
8346
8357
|
}
|
|
8347
|
-
/** Shared by localDir/localFile's build(): a server path,
|
|
8348
|
-
function pathZod(
|
|
8349
|
-
return stringContent((s) =>
|
|
8358
|
+
/** Shared by localDir/localFile's build(): a server path, judged only when one was entered. */
|
|
8359
|
+
function pathZod(_o) {
|
|
8360
|
+
return stringContent((s) => s);
|
|
8350
8361
|
}
|
|
8351
8362
|
registerType("localDir", {
|
|
8352
8363
|
kind: "localDir",
|
|
@@ -9005,10 +9016,9 @@ var MEASURED_ROLES = (opts) => {
|
|
|
9005
9016
|
accepts: ENUMERATED,
|
|
9006
9017
|
normalize: (d) => {
|
|
9007
9018
|
if (d && accepts(unitRole, d)) return d;
|
|
9008
|
-
const { label,
|
|
9019
|
+
const { label, value } = d?.opts ?? {};
|
|
9009
9020
|
return declare("string", {
|
|
9010
9021
|
label,
|
|
9011
|
-
required,
|
|
9012
9022
|
value,
|
|
9013
9023
|
noSearch: d?.opts.noSearch,
|
|
9014
9024
|
ui: uiOf(d?.opts),
|
|
@@ -9167,7 +9177,7 @@ registerType("geo", {
|
|
|
9167
9177
|
label: string$1().nullish()
|
|
9168
9178
|
}),
|
|
9169
9179
|
structureCode: "geo_structure",
|
|
9170
|
-
|
|
9180
|
+
acceptsEmptyRow: true
|
|
9171
9181
|
});
|
|
9172
9182
|
/**
|
|
9173
9183
|
* Geographic point composite {lat, lng, label?}; lat/lng validated to valid coordinate
|
|
@@ -9245,7 +9255,7 @@ registerType("illustrated", {
|
|
|
9245
9255
|
roles: ILLUSTRATED_ROLES,
|
|
9246
9256
|
roleZod: () => defaultRoleZod(ILLUSTRATED_ROLES),
|
|
9247
9257
|
structureCode: "illustrated_structure",
|
|
9248
|
-
|
|
9258
|
+
acceptsEmptyRow: true,
|
|
9249
9259
|
collectionView: "illustratedList",
|
|
9250
9260
|
hints: (o) => o.config ? { config: o.config } : {}
|
|
9251
9261
|
});
|
|
@@ -9289,7 +9299,7 @@ registerType("attachment", {
|
|
|
9289
9299
|
roles: ATTACHMENT_ROLES,
|
|
9290
9300
|
roleZod: () => defaultRoleZod(ATTACHMENT_ROLES),
|
|
9291
9301
|
structureCode: "attachment_structure",
|
|
9292
|
-
|
|
9302
|
+
acceptsEmptyRow: true,
|
|
9293
9303
|
collectionView: "attachmentList"
|
|
9294
9304
|
});
|
|
9295
9305
|
/**
|
|
@@ -9390,11 +9400,10 @@ function periodRole(key, label, mkSubDecl) {
|
|
|
9390
9400
|
if (d && accepts(def, d)) return d;
|
|
9391
9401
|
const canonical = mkSubDecl(label);
|
|
9392
9402
|
if (!d) return canonical;
|
|
9393
|
-
const { label: l,
|
|
9403
|
+
const { label: l, value } = d.opts;
|
|
9394
9404
|
return declare(canonical.factory, {
|
|
9395
9405
|
...canonical.opts,
|
|
9396
9406
|
label: l ?? canonical.opts.label,
|
|
9397
|
-
required,
|
|
9398
9407
|
value,
|
|
9399
9408
|
noSearch: d.opts.noSearch,
|
|
9400
9409
|
ui: uiOf(d.opts)
|
|
@@ -9678,8 +9687,7 @@ function keyValue(raw) {
|
|
|
9678
9687
|
type: valueType
|
|
9679
9688
|
}
|
|
9680
9689
|
},
|
|
9681
|
-
by: "key"
|
|
9682
|
-
required: o.required
|
|
9690
|
+
by: "key"
|
|
9683
9691
|
});
|
|
9684
9692
|
}
|
|
9685
9693
|
/** Roles of a range: the two endpoints, integer or real depending on `isInt`. */
|
|
@@ -9866,12 +9874,11 @@ var field = new Proxy({}, {
|
|
|
9866
9874
|
has: (_t, k) => k in composedField()
|
|
9867
9875
|
});
|
|
9868
9876
|
function toClient(field) {
|
|
9869
|
-
const { kind, label, agent,
|
|
9877
|
+
const { kind, label, agent, prim, hints, options, strict, relation, json, hidden, derived, parts, cache } = field;
|
|
9870
9878
|
return {
|
|
9871
9879
|
kind,
|
|
9872
9880
|
label,
|
|
9873
9881
|
...agent !== void 0 && { agent },
|
|
9874
|
-
required,
|
|
9875
9882
|
prim,
|
|
9876
9883
|
hints,
|
|
9877
9884
|
options,
|
|
@@ -9949,7 +9956,7 @@ function accepts(def, d) {
|
|
|
9949
9956
|
}
|
|
9950
9957
|
/**
|
|
9951
9958
|
* The correction every role gets for free: keep what the author said ABOUT the field
|
|
9952
|
-
* (`label`, `
|
|
9959
|
+
* (`label`, `ui`, `value` — a constant or a ComputeFn), replace the type, and
|
|
9953
9960
|
* drop what belonged to the OLD type (`rules` — a number's min/max mean nothing to a
|
|
9954
9961
|
* picture — and `multiple`, because a composite role owns exactly one column). A role whose
|
|
9955
9962
|
* own default needs more than a bare factory (`measured`'s unit, `period`'s endpoints)
|
|
@@ -9957,10 +9964,9 @@ function accepts(def, d) {
|
|
|
9957
9964
|
*/
|
|
9958
9965
|
function retypeTo(factory) {
|
|
9959
9966
|
return (d) => {
|
|
9960
|
-
const { label,
|
|
9967
|
+
const { label, value } = d?.opts ?? {};
|
|
9961
9968
|
return declare(factory, {
|
|
9962
9969
|
label,
|
|
9963
|
-
required,
|
|
9964
9970
|
value,
|
|
9965
9971
|
noSearch: d?.opts.noSearch,
|
|
9966
9972
|
ui: uiOf(d?.opts)
|
|
@@ -10146,13 +10152,69 @@ function partColumns(parts) {
|
|
|
10146
10152
|
}
|
|
10147
10153
|
return cols;
|
|
10148
10154
|
}
|
|
10149
|
-
|
|
10155
|
+
/**
|
|
10156
|
+
* A composite's write-time row shape, DERIVED from its resolved parts. `shape` carries
|
|
10157
|
+
* the composite's DEFAULT schema per role — the literal the factory writes by hand
|
|
10158
|
+
* (`{ value: numSchema, unit: z.string().refine(…) }`) — and each part decides which
|
|
10159
|
+
* schema guards its (role-named) key:
|
|
10160
|
+
*
|
|
10161
|
+
* - a plain STORED part is keyed by its role name, matching the value object and the
|
|
10162
|
+
* column `<field>__<role>` that `flattenEmbeddedAt`/`nestEmbeddedAt` read. A role left
|
|
10163
|
+
* to its default filling keeps the default schema — byte-identical to the hand-written
|
|
10164
|
+
* literal, so every existing structure/unit/range code and message is preserved.
|
|
10165
|
+
* - an OVERRIDDEN stored part is validated by its OWN zod (`meta.zod`) instead of the
|
|
10166
|
+
* default schema for that role. This is what makes a RETYPED filling real: an
|
|
10167
|
+
* `f.int({})` magnitude rejects 5.5 (its own `int` check) and gets an integer column,
|
|
10168
|
+
* and an `f.relation({…})` currency accepts a record id instead of being measured
|
|
10169
|
+
* against an ISO-4217 string rule it was never meant to satisfy. The filling also
|
|
10170
|
+
* brings its own optionality and its own min/max — an author replacing a filling
|
|
10171
|
+
* takes over that role's constraints, so `f.real({ rules: { min: 0 } })` is how a
|
|
10172
|
+
* replaced magnitude keeps a bound.
|
|
10173
|
+
* - a COMPUTED-AND-STORED part ('computedStored') is keyed by its role name like any
|
|
10174
|
+
* other part, but its schema is `z.unknown().optional()`: the SERVER owns the value, so
|
|
10175
|
+
* whatever the client sends there is ignored — `stripServerOwnedParts` deletes it a
|
|
10176
|
+
* moment later and the write path recomputes the column. It is stripped rather than
|
|
10177
|
+
* `z.never()`-rejected because a read hands the client that key (it is a real column,
|
|
10178
|
+
* present in every response), so rejecting it would break a read → PATCH-the-whole-object
|
|
10179
|
+
* round trip.
|
|
10180
|
+
* - a CLIENT-OWNED stored part of a composite also accepts
|
|
10181
|
+
* an explicit `null` (`tolerateNull`), which is what closes the read → write round trip
|
|
10182
|
+
* for a PARTIALLY FILLED composite. A SELECT returns every sub-column, so a record whose
|
|
10183
|
+
* magnitude column is set and whose unit column is empty reads back as
|
|
10184
|
+
* `{value: 900, unit: null}` (`nestEmbeddedAt` drops the key only when EVERY column is
|
|
10185
|
+
* empty — `0` is a value), and feeding that exact object back used to fail with
|
|
10186
|
+
* `measured_structure`: the write path refused the shape its own read produced. The null
|
|
10187
|
+
* slot now parses through and is written back as NULL, so the round trip is closed and
|
|
10188
|
+
* clearing ONE role of a filled composite works instead of erroring.
|
|
10189
|
+
* NULL, not absence: a read emits every stored role (empty ones as `null`), so `null` is
|
|
10190
|
+
* how "this record has no value there" arrives, while an ABSENT key is a writer that
|
|
10191
|
+
* never mentioned a role it owns — which stays the structure error it has always been
|
|
10192
|
+
* (`{unit: 'kg'}` with no magnitude, `{lat: 50}` with no longitude). That is the
|
|
10193
|
+
* all-or-nothing rule for a DECLARATION, and it is untouched.
|
|
10194
|
+
* A composite used to be able to override this: `required: true` kept every client-owned
|
|
10195
|
+
* slot MANDATORY, null included, so that "tolerate null" could not turn a demanded field
|
|
10196
|
+
* optional. Nothing demands a value now, so there is no override and the rule above is the
|
|
10197
|
+
* only one.
|
|
10198
|
+
*/
|
|
10199
|
+
/**
|
|
10200
|
+
* How much a CLIENT-OWNED slot forgives in the row shape: exactly one thing, an explicit
|
|
10201
|
+
* `null`. `.nullable()`, never `.nullish()` — a part that is MISSING entirely is still a
|
|
10202
|
+
* malformed row (`{lat: 5.5}` is half a coordinate), which is the one rule about composites
|
|
10203
|
+
* that judges CORRECTNESS rather than presence.
|
|
10204
|
+
*
|
|
10205
|
+
* There used to be a `RowTolerance` of three levels, and the field's `required` is what chose
|
|
10206
|
+
* between them: 'strict' when the field demanded a value, 'null-or-missing' when the value was
|
|
10207
|
+
* ABSENT under that demand — the third existing only so an absent required composite did not
|
|
10208
|
+
* report the same absence twice, once as `required` and once as a structure code. With nothing
|
|
10209
|
+
* able to demand a value there is no first issue to restate, so both levels and the type
|
|
10210
|
+
* itself are gone.
|
|
10211
|
+
*/
|
|
10212
|
+
function partsRowShape(shape, parts) {
|
|
10150
10213
|
const out = { ...shape };
|
|
10151
10214
|
for (const p of parts) if (p.mode === "computedStored") out[p.key] = unknown().optional();
|
|
10152
10215
|
else {
|
|
10153
10216
|
const own = p.overridden ? p.meta.zod : shape[p.role] ?? unknown().optional();
|
|
10154
|
-
|
|
10155
|
-
out[p.key] = relaxed ? tolerance === "null-or-missing" ? own.nullish() : own.nullable() : own;
|
|
10217
|
+
out[p.key] = own.nullable();
|
|
10156
10218
|
}
|
|
10157
10219
|
return out;
|
|
10158
10220
|
}
|
|
@@ -10181,7 +10243,7 @@ function stripServerOwnedParts(value, parts) {
|
|
|
10181
10243
|
* dimensions, check(single)) and differed only in the validation code and the per-role
|
|
10182
10244
|
* default zod.
|
|
10183
10245
|
*
|
|
10184
|
-
* raw → jsonValue →
|
|
10246
|
+
* raw → jsonValue → carries nothing? (passthrough, when `acceptsEmptyRow`)
|
|
10185
10247
|
* → carries nothing at all? (pass through, there is nothing to judge)
|
|
10186
10248
|
* → rowSchema (partsRowShape) → stripServerOwnedParts → optional per-type refine
|
|
10187
10249
|
*
|
|
@@ -10191,28 +10253,16 @@ function stripServerOwnedParts(value, parts) {
|
|
|
10191
10253
|
* exactly once.
|
|
10192
10254
|
*/
|
|
10193
10255
|
function buildComposite(def, opts, parts) {
|
|
10194
|
-
const
|
|
10195
|
-
const roleZod = def.roleZod(opts, parts);
|
|
10196
|
-
const rowSchema = object(partsRowShape(roleZod, parts, required === true ? "strict" : "null"));
|
|
10197
|
-
const absentRowSchema = required === true ? object(partsRowShape(roleZod, parts, "null-or-missing")) : rowSchema;
|
|
10256
|
+
const rowSchema = object(partsRowShape(def.roleZod(opts, parts), parts));
|
|
10198
10257
|
const roleKeys = (which) => parts.filter((p) => p.mode === "stored" && (which?.(p) ?? true)).map(partValueKey);
|
|
10199
|
-
const
|
|
10200
|
-
const clientKeys = def.absenceRoles ? roleKeys() : void 0;
|
|
10258
|
+
const clientKeys = def.acceptsEmptyRow ? roleKeys() : void 0;
|
|
10201
10259
|
const zod = unknown().transform((raw, ctx) => {
|
|
10202
10260
|
const parsed = jsonValue(raw);
|
|
10203
10261
|
const p = parsed;
|
|
10204
10262
|
const isRow = p == null || typeof p === "object" && !Array.isArray(p);
|
|
10205
10263
|
/** Every one of `keys` empty in this value — `null`/absent alike. */
|
|
10206
10264
|
const allEmpty = (keys) => isRow && (p == null || keys.length > 0 && keys.every((k) => p[k] == null));
|
|
10207
|
-
|
|
10208
|
-
if (absenceKeys && clientKeys) {
|
|
10209
|
-
absent = allEmpty(absenceKeys);
|
|
10210
|
-
if (absent && required) ctx.addIssue({
|
|
10211
|
-
code: ZodIssueCode.custom,
|
|
10212
|
-
message: vmsg("required")
|
|
10213
|
-
});
|
|
10214
|
-
if (allEmpty(clientKeys)) return raw;
|
|
10215
|
-
}
|
|
10265
|
+
if (clientKeys && allEmpty(clientKeys)) return raw;
|
|
10216
10266
|
if (typeof parsed === "string") {
|
|
10217
10267
|
ctx.addIssue({
|
|
10218
10268
|
code: ZodIssueCode.custom,
|
|
@@ -10220,7 +10270,7 @@ function buildComposite(def, opts, parts) {
|
|
|
10220
10270
|
});
|
|
10221
10271
|
return NEVER;
|
|
10222
10272
|
}
|
|
10223
|
-
const r =
|
|
10273
|
+
const r = rowSchema.safeParse(parsed);
|
|
10224
10274
|
if (!r.success) {
|
|
10225
10275
|
const message = typeof def.structureCode === "function" ? def.structureCode(r.error) : vmsg(def.structureCode);
|
|
10226
10276
|
ctx.addIssue({
|
|
@@ -10273,12 +10323,10 @@ function materialize(decl, name) {
|
|
|
10273
10323
|
if (isComposite(type) && (opts.multiple ?? false) && parts.length > 0 && !type.selfManages?.has("multiple")) return collectionGroup(name, opts, parts, type.collectionView);
|
|
10274
10324
|
const built = isComposite(type) ? buildComposite(type, opts, parts) : type.build(opts, parts, name);
|
|
10275
10325
|
const selfOptional = type.selfManages?.has("optional") ?? false;
|
|
10276
|
-
const required = selfOptional ? false : opts.required ?? false;
|
|
10277
10326
|
const base = {
|
|
10278
10327
|
kind: type.kind,
|
|
10279
10328
|
label: opts.label ?? "",
|
|
10280
10329
|
...opts.agent !== void 0 && { agent: opts.agent },
|
|
10281
|
-
required,
|
|
10282
10330
|
prim: type.prim,
|
|
10283
10331
|
column: built.column,
|
|
10284
10332
|
hints: built.hints ?? {},
|
|
@@ -10289,7 +10337,7 @@ function materialize(decl, name) {
|
|
|
10289
10337
|
columns: built.columns,
|
|
10290
10338
|
parts
|
|
10291
10339
|
},
|
|
10292
|
-
zod: selfOptional ? built.zod : optionalize(built.zod
|
|
10340
|
+
zod: selfOptional ? built.zod : optionalize(built.zod)
|
|
10293
10341
|
};
|
|
10294
10342
|
return wrapKey(name, opts, type.selfManages?.has("multiple") ? base : applyMultiple(base, opts.multiple ?? false));
|
|
10295
10343
|
}
|
|
@@ -10319,7 +10367,6 @@ function collectionGroup(key, opts, parts, collectionView) {
|
|
|
10319
10367
|
key,
|
|
10320
10368
|
scope: "nest",
|
|
10321
10369
|
multiple: true,
|
|
10322
|
-
required: opts.required ?? false,
|
|
10323
10370
|
label: opts.label ?? key,
|
|
10324
10371
|
...opts.agent !== void 0 && { agent: opts.agent },
|
|
10325
10372
|
display: "wrap",
|
|
@@ -10448,6 +10495,8 @@ var src_default = definePlugin({
|
|
|
10448
10495
|
id: "webchat",
|
|
10449
10496
|
version: "1.0.0",
|
|
10450
10497
|
dependsOn: [],
|
|
10498
|
+
label: "webchat.plugin.label",
|
|
10499
|
+
description: "webchat.plugin.description",
|
|
10451
10500
|
settings: defineSettings({
|
|
10452
10501
|
label: "webchat.settings.label",
|
|
10453
10502
|
fields: { reasoning_display: field.string({
|
package/dist/web.js
CHANGED
|
@@ -11,7 +11,7 @@ import { definePluginUI } from "@coffer-org/web-ui";
|
|
|
11
11
|
var ui_default = definePluginUI({ slots: [{
|
|
12
12
|
slot: "overlay",
|
|
13
13
|
id: "webchat",
|
|
14
|
-
load: () => import("./ChatWidget-
|
|
14
|
+
load: () => import("./ChatWidget-BhGpxS9f.js")
|
|
15
15
|
}] });
|
|
16
16
|
//#endregion
|
|
17
17
|
export { ui_default as default };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coffer-org/plugin-webchat",
|
|
3
|
-
"version": "7.0
|
|
3
|
+
"version": "7.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=24"
|
|
@@ -27,11 +27,10 @@
|
|
|
27
27
|
"test:ui": "vitest run --root ."
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@coffer-org/
|
|
31
|
-
"@coffer-org/
|
|
32
|
-
"
|
|
33
|
-
"react
|
|
34
|
-
"remark-gfm": "^4.0.1"
|
|
30
|
+
"@coffer-org/markdown": "^7.4.0",
|
|
31
|
+
"@coffer-org/sdk": "^7.4.0",
|
|
32
|
+
"@coffer-org/server": "^7.4.0",
|
|
33
|
+
"lucide-react": "^1.17.0"
|
|
35
34
|
},
|
|
36
35
|
"coffer": {
|
|
37
36
|
"runtime": "node",
|