@yadsh/dsh-qa-surface 0.10.0 → 0.11.1
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 +310 -48
- package/lib/access/service.js +49 -10
- package/lib/access/service.js.map +1 -1
- package/lib/account-remotes.js +41 -0
- package/lib/account-remotes.js.map +1 -1
- package/lib/accounts/database.js +140 -0
- package/lib/accounts/database.js.map +1 -1
- package/lib/accounts/errors.js.map +1 -1
- package/lib/accounts/service-token.js +103 -0
- package/lib/accounts/service-token.js.map +1 -0
- package/lib/accounts/store.js +291 -5
- package/lib/accounts/store.js.map +1 -1
- package/lib/admin/conversation-log.js +14 -8
- package/lib/admin/conversation-log.js.map +1 -1
- package/lib/admin/permissions.js +5 -0
- package/lib/admin/permissions.js.map +1 -1
- package/lib/admin/quality-store.js +4 -0
- package/lib/admin/quality-store.js.map +1 -1
- package/lib/admin/service.js +173 -0
- package/lib/admin/service.js.map +1 -1
- package/lib/admin/session-log.js +39 -8
- package/lib/admin/session-log.js.map +1 -1
- package/lib/attachment-rules.js +39 -6
- package/lib/attachment-rules.js.map +1 -1
- package/lib/cli.js +83 -1
- package/lib/cli.js.map +1 -1
- package/lib/client/QaAccountsController.js +213 -4
- package/lib/client/QaAccountsController.js.map +1 -1
- package/lib/client/QaSessionController.js +37 -21
- package/lib/client/QaSessionController.js.map +1 -1
- package/lib/client/QaSurface.js +28 -4
- package/lib/client/QaSurface.js.map +1 -1
- package/lib/client/admin/QaAdmin.js +9 -1
- package/lib/client/admin/QaAdmin.js.map +1 -1
- package/lib/client/admin/pages/SkillFiles.js +78 -0
- package/lib/client/admin/pages/SkillFiles.js.map +1 -0
- package/lib/client/admin/pages/Users.js +65 -1
- package/lib/client/admin/pages/Users.js.map +1 -1
- package/lib/client/admin/routes.js +8 -1
- package/lib/client/admin/routes.js.map +1 -1
- package/lib/client/admin/styles.js +2 -0
- package/lib/client/admin/styles.js.map +1 -1
- package/lib/client/attachments.js +41 -11
- package/lib/client/attachments.js.map +1 -1
- package/lib/client/components/QaAuthGate.js +19 -10
- package/lib/client/components/QaAuthGate.js.map +1 -1
- package/lib/client/components/QaChangelog.js +43 -0
- package/lib/client/components/QaChangelog.js.map +1 -1
- package/lib/client/index.js +13 -0
- package/lib/client/index.js.map +1 -1
- package/lib/client/lineage.js +64 -0
- package/lib/client/lineage.js.map +1 -1
- package/lib/client/settings/sections/attachments.js +6 -5
- package/lib/client/settings/sections/attachments.js.map +1 -1
- package/lib/client/settings/sections/notes.js +22 -4
- package/lib/client/settings/sections/notes.js.map +1 -1
- package/lib/client/styles.js +9 -0
- package/lib/client/styles.js.map +1 -1
- package/lib/client/types.js.map +1 -1
- package/lib/client/user-settings/IntegrationTokensPage.js +150 -0
- package/lib/client/user-settings/IntegrationTokensPage.js.map +1 -0
- package/lib/client/user-settings/PasswordSettingsPage.js +48 -0
- package/lib/client/user-settings/PasswordSettingsPage.js.map +1 -0
- package/lib/client/user-settings/SkillCatalog.js +2 -2
- package/lib/client/user-settings/SkillCatalog.js.map +1 -1
- package/lib/client/user-settings/UserSettingsDialog.js +17 -2
- package/lib/client/user-settings/UserSettingsDialog.js.map +1 -1
- package/lib/client/user-settings/copy.js +20 -0
- package/lib/client/user-settings/copy.js.map +1 -1
- package/lib/client.js +2480 -137
- package/lib/client.js.map +1 -1
- package/lib/config-resolvers/attachments.js +6 -5
- package/lib/config-resolvers/attachments.js.map +1 -1
- package/lib/config-resolvers/defaults.js +23 -2
- package/lib/config-resolvers/defaults.js.map +1 -1
- package/lib/config-resolvers/integration.js +94 -0
- package/lib/config-resolvers/integration.js.map +1 -0
- package/lib/config-resolvers/notes.js +2 -0
- package/lib/config-resolvers/notes.js.map +1 -1
- package/lib/config.js +72 -0
- package/lib/config.js.map +1 -1
- package/lib/index.js +197 -2
- package/lib/index.js.map +1 -1
- package/lib/integration/answer.js +100 -0
- package/lib/integration/answer.js.map +1 -0
- package/lib/integration/attachments.js +246 -0
- package/lib/integration/attachments.js.map +1 -0
- package/lib/integration/contract.js +53 -0
- package/lib/integration/contract.js.map +1 -0
- package/lib/integration/host-runner.js +243 -0
- package/lib/integration/host-runner.js.map +1 -0
- package/lib/integration/http.js +576 -0
- package/lib/integration/http.js.map +1 -0
- package/lib/integration/service.js +373 -0
- package/lib/integration/service.js.map +1 -0
- package/lib/integration/transcript-reader.js +202 -0
- package/lib/integration/transcript-reader.js.map +1 -0
- package/lib/integration/transcript.js +148 -0
- package/lib/integration/transcript.js.map +1 -0
- package/lib/personal-skills/index.js +2 -2
- package/lib/personal-skills/index.js.map +1 -1
- package/lib/personal-skills/paths.js +14 -8
- package/lib/personal-skills/paths.js.map +1 -1
- package/lib/personal-skills/provider.js +16 -4
- package/lib/personal-skills/provider.js.map +1 -1
- package/lib/personal-skills/service.js +240 -30
- package/lib/personal-skills/service.js.map +1 -1
- package/lib/prompt-notes.js +58 -0
- package/lib/prompt-notes.js.map +1 -1
- package/lib/provenance/document-preview.js +10 -1
- package/lib/provenance/document-preview.js.map +1 -1
- package/lib/qa-tools/catalog.js +12 -1
- package/lib/qa-tools/catalog.js.map +1 -1
- package/lib/qa-tools/docs-tools.js +563 -0
- package/lib/qa-tools/docs-tools.js.map +1 -0
- package/lib/resolve-config.js +4 -0
- package/lib/resolve-config.js.map +1 -1
- package/lib/secure-session.js +83 -3
- package/lib/secure-session.js.map +1 -1
- package/lib/shared/integration-tokens.js +23 -0
- package/lib/shared/integration-tokens.js.map +1 -0
- package/lib/typert.host.js +1176 -213
- package/lib/typert.remote-client.d.ts +27 -1
- package/lib/typert.remote-client.js +911 -69
- package/lib/types/access/service.d.ts +33 -2
- package/lib/types/account-remotes.d.ts +30 -1
- package/lib/types/accounts/database.d.ts +53 -0
- package/lib/types/accounts/errors.d.ts +5 -1
- package/lib/types/accounts/service-token.d.ts +70 -0
- package/lib/types/accounts/store.d.ts +131 -1
- package/lib/types/admin/conversation-log.d.ts +12 -0
- package/lib/types/admin/service.d.ts +56 -1
- package/lib/types/admin/session-log.d.ts +21 -0
- package/lib/types/attachment-rules.d.ts +26 -3
- package/lib/types/client/QaAccountsController.d.ts +64 -5
- package/lib/types/client/QaSessionController.d.ts +24 -2
- package/lib/types/client/admin/pages/SkillFiles.d.ts +17 -0
- package/lib/types/client/admin/routes.d.ts +2 -0
- package/lib/types/client/attachments.d.ts +21 -4
- package/lib/types/client/lineage.d.ts +42 -0
- package/lib/types/client/settings/sections/notes.d.ts +5 -3
- package/lib/types/client/types.d.ts +71 -1
- package/lib/types/client/user-settings/IntegrationTokensPage.d.ts +21 -0
- package/lib/types/client/user-settings/PasswordSettingsPage.d.ts +19 -0
- package/lib/types/client/user-settings/UserSettingsDialog.d.ts +11 -2
- package/lib/types/client/user-settings/copy.d.ts +10 -1
- package/lib/types/config-resolvers/integration.d.ts +47 -0
- package/lib/types/config.d.ts +10 -0
- package/lib/types/index.d.ts +73 -1
- package/lib/types/integration/answer.d.ts +65 -0
- package/lib/types/integration/attachments.d.ts +85 -0
- package/lib/types/integration/contract.d.ts +196 -0
- package/lib/types/integration/host-runner.d.ts +44 -0
- package/lib/types/integration/http.d.ts +79 -0
- package/lib/types/integration/service.d.ts +120 -0
- package/lib/types/integration/transcript-reader.d.ts +100 -0
- package/lib/types/integration/transcript.d.ts +59 -0
- package/lib/types/personal-skills/index.d.ts +2 -2
- package/lib/types/personal-skills/paths.d.ts +15 -3
- package/lib/types/personal-skills/provider.d.ts +14 -2
- package/lib/types/personal-skills/service.d.ts +89 -14
- package/lib/types/prompt-notes.d.ts +28 -0
- package/lib/types/provenance/document-preview.d.ts +4 -0
- package/lib/types/qa-tools/catalog.d.ts +1 -1
- package/lib/types/qa-tools/docs-tools.d.ts +160 -0
- package/lib/types/secure-session.d.ts +52 -1
- package/lib/types/shared/integration-tokens.d.ts +24 -0
- package/lib/types/types.d.ts +170 -2
- package/package.json +2 -2
package/lib/types/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
2
2
|
import type { Context } from "@deepseek-ai/cordis";
|
|
3
3
|
import type { QaTurnSources } from "./provenance/types.js";
|
|
4
|
-
import type { QaAccountProfileInput, QaAccountSession, QaAccountStartersInput, QaAccountUserPublic, QaApprovalDecision, QaClaimResult, QaDocumentPreview, QaLockdownProof, QaOwnershipEntry, QaPendingApproval, QaPendingQuestion, QaQuestionAnswerItem, QaSurfaceConfig, ResolvedQaSurfaceConfig, QaSkillDocument, QaSkillDraftInput, QaSkillRemoval, QaSkillSummary, QaSkillToolDescriptor, QaSkillValidation, QaSourceFilePreview, QaWhoamiResult, QaWorkspaceFile, QaWorkspaceListing, QaPrincipal, QaAccessAdminSnapshot, QaAdminAuditEvent, QaAdminOverview, QaAdminPage, QaAdminUserDetail, QaAdminUserRow, QaAdminUserUpdate, QaAuditQuery, QaCapabilitySelection, QaConversationDetail, QaConversationDeletion, QaConversationQuery, QaConversationReview, QaConversationReviewInput, QaConversationSummary, QaCurrentAccess, QaFeedbackQuery, QaFeedbackRow, QaMessageFeedback, QaMessageFeedbackInput, QaQualityMetrics, QaReviewQueueItem, QaReviewQueueRow, QaSessionAccess, QaSlashCatalog, QaSlashExecution, QaSlashSubmitAttachment, QaSkillActivationRecord, QaSkillAssignmentOverride, QaSubrole, QaUserAccess, QaUserQuery } from "./types.js";
|
|
4
|
+
import type { QaAccountProfileInput, QaAccountSession, QaAccountStartersInput, QaAccountUserPublic, QaApprovalDecision, QaClaimResult, QaDocumentPreview, QaIssuedServiceToken, QaLockdownProof, QaOwnershipEntry, QaPasswordResetRequest, QaPendingApproval, QaPendingQuestion, QaQuestionAnswerItem, QaServiceTokenCreateInput, QaServiceTokenSummary, QaSurfaceConfig, ResolvedQaSurfaceConfig, QaAdminSkillScope, QaAdminSkillsView, QaSkillDocument, QaSkillDraftInput, QaSkillRemoval, QaSkillSummary, QaSkillToolDescriptor, QaSkillValidation, QaSourceFilePreview, QaWhoamiResult, QaWorkspaceFile, QaWorkspaceListing, QaPrincipal, QaAccessAdminSnapshot, QaAdminAuditEvent, QaAdminOverview, QaAdminPage, QaAdminUserDetail, QaAdminUserRow, QaAdminUserUpdate, QaAuditQuery, QaCapabilitySelection, QaConversationDetail, QaConversationDeletion, QaConversationQuery, QaConversationReview, QaConversationReviewInput, QaConversationSummary, QaCurrentAccess, QaFeedbackQuery, QaFeedbackRow, QaMessageFeedback, QaMessageFeedbackInput, QaQualityMetrics, QaReviewQueueItem, QaReviewQueueRow, QaSessionAccess, QaSlashCatalog, QaSlashExecution, QaSlashSubmitAttachment, QaSkillActivationRecord, QaSkillAssignmentOverride, QaSubrole, QaUserAccess, QaUserQuery } from "./types.js";
|
|
5
5
|
export declare const name = "qa-surface";
|
|
6
6
|
export declare const inject: string[];
|
|
7
7
|
import { QA_SURFACE_SETTINGS_NAMESPACE } from "./shared/settings.js";
|
|
@@ -51,6 +51,14 @@ export declare class QaSurface extends TypertRemoteService {
|
|
|
51
51
|
private webServer;
|
|
52
52
|
private disposeRoute;
|
|
53
53
|
private routeKey;
|
|
54
|
+
/**
|
|
55
|
+
* The HTTP API an external application asks questions through. Constructed
|
|
56
|
+
* always — it is cheap and stateless until a request arrives — and exposed
|
|
57
|
+
* on the network only while the deployment turns it on.
|
|
58
|
+
*/
|
|
59
|
+
private readonly integration;
|
|
60
|
+
private disposeIntegrationRoutes;
|
|
61
|
+
private integrationRouteKey;
|
|
54
62
|
/**
|
|
55
63
|
* The quality store opens its file on first use, so a deployment that never
|
|
56
64
|
* opens the admin console never grows one.
|
|
@@ -108,6 +116,15 @@ export declare class QaSurface extends TypertRemoteService {
|
|
|
108
116
|
accountsRegister(email: string, password: string): QaAccountSession;
|
|
109
117
|
accountsLogin(email: string, password: string): QaAccountSession;
|
|
110
118
|
/** Identity probe; safe to call with an empty or expired token. */
|
|
119
|
+
accountsChangePassword(token: string, currentPassword: string, nextPassword: string): QaAccountSession;
|
|
120
|
+
/**
|
|
121
|
+
* The sign-in screen's "забыли пароль?" path. It answers the same way for
|
|
122
|
+
* every address: whether an account exists is not something this endpoint
|
|
123
|
+
* tells a caller.
|
|
124
|
+
*/
|
|
125
|
+
accountsRequestPasswordReset(email: string): {
|
|
126
|
+
readonly accepted: true;
|
|
127
|
+
};
|
|
111
128
|
accountsWhoami(token: string): QaWhoamiResult;
|
|
112
129
|
/**
|
|
113
130
|
* Migrate a browser's local chat index into server-side ownership. The
|
|
@@ -140,6 +157,29 @@ export declare class QaSurface extends TypertRemoteService {
|
|
|
140
157
|
* deployment's limits are enforced in the store before anything is written.
|
|
141
158
|
*/
|
|
142
159
|
accountsUpdateStarters(token: string, input: QaAccountStartersInput): QaAccountUserPublic;
|
|
160
|
+
/**
|
|
161
|
+
* The caller's own integration tokens. The list is the account's own: the
|
|
162
|
+
* token names it, and a secret is never part of a summary, so reading this
|
|
163
|
+
* cannot repeat a credential that was already handed over.
|
|
164
|
+
*/
|
|
165
|
+
accountsListServiceTokens(token: string): {
|
|
166
|
+
readonly tokens: readonly QaServiceTokenSummary[];
|
|
167
|
+
};
|
|
168
|
+
/**
|
|
169
|
+
* Mint one integration token for the caller. The answer carries the
|
|
170
|
+
* plaintext exactly once — nothing stores it, so nothing can repeat it — and
|
|
171
|
+
* the account is always the authenticated one: there is no owner field on
|
|
172
|
+
* the wire.
|
|
173
|
+
*/
|
|
174
|
+
accountsCreateServiceToken(token: string, input: QaServiceTokenCreateInput): QaIssuedServiceToken;
|
|
175
|
+
/**
|
|
176
|
+
* Revoke one of the caller's own integration tokens. This is the immediate
|
|
177
|
+
* kill switch the profile page offers: the next request that presents the
|
|
178
|
+
* credential is refused, without waiting for its expiry.
|
|
179
|
+
*/
|
|
180
|
+
accountsRevokeServiceToken(token: string, tokenId: string): {
|
|
181
|
+
readonly revoked: boolean;
|
|
182
|
+
};
|
|
143
183
|
/**
|
|
144
184
|
* Serve the effective QA configuration to the browser. The DSH gateway pins
|
|
145
185
|
* settings RPCs to loopback, so a browser served over the LAN always sees
|
|
@@ -171,12 +211,35 @@ export declare class QaSurface extends TypertRemoteService {
|
|
|
171
211
|
accessUpdateSkillOverride(token: string, input: QaSkillAssignmentOverride): readonly QaSkillAssignmentOverride[];
|
|
172
212
|
/** What each skill activation actually granted in this session. */
|
|
173
213
|
accessSkillActivations(token: string, sessionId: string): readonly QaSkillActivationRecord[];
|
|
214
|
+
/**
|
|
215
|
+
* The skill catalog of one scope: the deployment's shared skills, or the
|
|
216
|
+
* personal skills of the account the scope names. Reading another account's
|
|
217
|
+
* skills is an administrative act, so it needs `skills.manage`.
|
|
218
|
+
*/
|
|
219
|
+
adminSkills(token: string, scope: QaAdminSkillScope): Promise<QaAdminSkillsView>;
|
|
220
|
+
/** One skill file with the body, revision and administrator mark. */
|
|
221
|
+
adminSkill(token: string, scope: QaAdminSkillScope, name: string): Promise<QaSkillDocument>;
|
|
222
|
+
/**
|
|
223
|
+
* Create (`name` null) or replace one skill in place. The write is recorded
|
|
224
|
+
* as this administrator's, and the owner sees that it was.
|
|
225
|
+
*/
|
|
226
|
+
adminSkillSave(token: string, scope: QaAdminSkillScope, name: string | null, input: QaSkillDraftInput): Promise<QaSkillDocument>;
|
|
227
|
+
/** Remove one skill into the trash beside its own skills root. */
|
|
228
|
+
adminSkillDelete(token: string, scope: QaAdminSkillScope, name: string, expectedRevision: string | null): Promise<QaSkillRemoval>;
|
|
229
|
+
/** The file a save would write, plus every diagnostic for the draft. */
|
|
230
|
+
adminSkillValidate(token: string, scope: QaAdminSkillScope, name: string | null, input: QaSkillDraftInput): Promise<QaSkillValidation>;
|
|
231
|
+
/** The tool catalog the picker offers for one scope. */
|
|
232
|
+
adminSkillTools(token: string, scope: QaAdminSkillScope): Promise<{
|
|
233
|
+
readonly tools: readonly QaSkillToolDescriptor[];
|
|
234
|
+
}>;
|
|
174
235
|
/** Counters, attention lines and the newest quality signals. */
|
|
175
236
|
adminOverview(token: string): Promise<QaAdminOverview>;
|
|
176
237
|
adminUsers(token: string, query: QaUserQuery, cursor: string | null, limit: number | null): Promise<QaAdminPage<QaAdminUserRow>>;
|
|
177
238
|
adminUser(token: string, userId: string): Promise<QaAdminUserDetail>;
|
|
178
239
|
/** Authorization role, status and subrole assignment of one account. */
|
|
179
240
|
adminUpdateUser(token: string, userId: string, update: QaAdminUserUpdate): Promise<QaAdminUserDetail>;
|
|
241
|
+
adminPasswordResetRequests(token: string): Promise<readonly QaPasswordResetRequest[]>;
|
|
242
|
+
adminResetPassword(token: string, userId: string, password: string): Promise<QaAdminUserDetail>;
|
|
180
243
|
adminConversations(token: string, query: QaConversationQuery, cursor: string | null, limit: number | null): Promise<QaAdminPage<QaConversationSummary>>;
|
|
181
244
|
/** One conversation with its messages, tool calls and review state. */
|
|
182
245
|
adminConversation(token: string, conversationId: string): Promise<QaConversationDetail>;
|
|
@@ -322,6 +385,15 @@ export declare class QaSurface extends TypertRemoteService {
|
|
|
322
385
|
* the transcript row is read from.
|
|
323
386
|
*/
|
|
324
387
|
slashExecute(token: string, sessionId: string, line: string, attachments: readonly QaSlashSubmitAttachment[]): Promise<QaSlashExecution>;
|
|
388
|
+
/**
|
|
389
|
+
* Register (or drop) the integration API's routes for the current config.
|
|
390
|
+
*
|
|
391
|
+
* The key is the whole registration decision — whether the API is on and
|
|
392
|
+
* where it lives — so a config change that does not move the endpoints
|
|
393
|
+
* leaves the live routes alone, and turning the API off disposes them
|
|
394
|
+
* immediately rather than at the next restart.
|
|
395
|
+
*/
|
|
396
|
+
private refreshIntegrationRoutes;
|
|
325
397
|
private refreshRoute;
|
|
326
398
|
/**
|
|
327
399
|
* A deployment that set `lockdown.allowSlashCommands: true` before this
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { StoredSessionEvent } from "../admin/conversation-log.js";
|
|
2
|
+
/**
|
|
3
|
+
* Reading one answered turn out of a session log.
|
|
4
|
+
*
|
|
5
|
+
* The log is the durable record of the conversation, and it is what the
|
|
6
|
+
* integration API publishes: the live assistant stream is a presentation
|
|
7
|
+
* channel that can be abandoned mid-attempt, while `assistant/message` events
|
|
8
|
+
* are what was actually committed. A turn with tools in it commits several
|
|
9
|
+
* assistant messages — the intermediate steps carry tool calls and usually no
|
|
10
|
+
* prose — so the answer is the *last one that has text*, not simply the last
|
|
11
|
+
* one written. If a later empty message follows a text one, publishing the
|
|
12
|
+
* empty message would answer the bridge with nothing.
|
|
13
|
+
*/
|
|
14
|
+
/** The answer of one settled turn, plus how the turn ended. */
|
|
15
|
+
export interface QaTurnAnswer {
|
|
16
|
+
/** The publishable text; empty when the turn committed no prose at all. */
|
|
17
|
+
readonly answer: string;
|
|
18
|
+
/** True when the last committed assistant message was interrupted. */
|
|
19
|
+
readonly interrupted: boolean;
|
|
20
|
+
/** Durable seq of the message the answer came from, when there was one. */
|
|
21
|
+
readonly seq: number | null;
|
|
22
|
+
}
|
|
23
|
+
/** The empty answer: what a turn that committed nothing readable looks like. */
|
|
24
|
+
export declare const QA_EMPTY_ANSWER: QaTurnAnswer;
|
|
25
|
+
/**
|
|
26
|
+
* Project the answer of the turn that starts after `afterSeq`.
|
|
27
|
+
*
|
|
28
|
+
* Only events after the caller's own prompt are read: a chat is continued
|
|
29
|
+
* across questions, and picking the last assistant message of the whole log
|
|
30
|
+
* would hand question #2 the answer to question #1 whenever the new turn
|
|
31
|
+
* committed nothing.
|
|
32
|
+
*
|
|
33
|
+
* @param events - durable events, in any order.
|
|
34
|
+
* @param afterSeq - the seq of the prompt whose answer is wanted.
|
|
35
|
+
* @returns the publishable answer and whether the turn was interrupted.
|
|
36
|
+
*/
|
|
37
|
+
export declare function answerAfter(events: readonly StoredSessionEvent[], afterSeq: number): QaTurnAnswer;
|
|
38
|
+
/**
|
|
39
|
+
* Cut an answer down to the size the caller publishes.
|
|
40
|
+
*
|
|
41
|
+
* The bridge posts the answer into a ticket comment, which has a size budget
|
|
42
|
+
* of its own: an over-long comment is not rejected there, it is cut — and a cut
|
|
43
|
+
* that lands in the middle of a sentence, or worse inside a code block, reads
|
|
44
|
+
* like a broken answer. The cut therefore prefers the last paragraph break,
|
|
45
|
+
* then the last line, then the last sentence, and only then an arbitrary word
|
|
46
|
+
* boundary, and it appends an ellipsis so the reader sees that text is missing
|
|
47
|
+
* rather than finished. A boundary that would leave less than half the budget
|
|
48
|
+
* used is refused: a single early paragraph break must not turn a full answer
|
|
49
|
+
* into one line.
|
|
50
|
+
* @param answer - the settled answer, already trimmed.
|
|
51
|
+
* @param maxCharacters - the publish budget; a non-positive value never cuts.
|
|
52
|
+
* @returns the answer, or its bounded head.
|
|
53
|
+
*/
|
|
54
|
+
export declare function boundAnswer(answer: string, maxCharacters: number): string;
|
|
55
|
+
/**
|
|
56
|
+
* The seq of the newest user-authored message in a log, which is the floor
|
|
57
|
+
* `answerAfter` reads from when the caller has just submitted a prompt.
|
|
58
|
+
* Synthetic context (injected notes, skill payloads) is skipped: it is model
|
|
59
|
+
* input recorded as a user message, and one arriving after the answer would
|
|
60
|
+
* otherwise hide it.
|
|
61
|
+
* @param events - durable events, in any order.
|
|
62
|
+
* @returns the seq, or 0 when the log holds no human prompt yet.
|
|
63
|
+
*/
|
|
64
|
+
export declare function lastPromptSeq(events: readonly StoredSessionEvent[]): number;
|
|
65
|
+
//# sourceMappingURL=answer.d.ts.map
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { PromptContentPart } from "@deepseek-ai/dsh-api-session-controller";
|
|
2
|
+
import type { DocumentsFace } from "@yadsh/dsh-documents";
|
|
3
|
+
import type { PluginLogger } from "@yadsh/dsh-plugin-log";
|
|
4
|
+
import { QaIntegrationAttachmentError, type QaFileAttachment } from "./contract.js";
|
|
5
|
+
/**
|
|
6
|
+
* Bringing a ticket's attachments into the prompt.
|
|
7
|
+
*
|
|
8
|
+
* An image rides the prompt inline, because that is what the harness carries.
|
|
9
|
+
* A file does not: `PromptContentPart` names one as an opaque receipt minted by
|
|
10
|
+
* the harness's own upload service, which an external caller has no session to
|
|
11
|
+
* reach — and that service is what stores the copy the model later reads with
|
|
12
|
+
* its file tools. The bridge's non-image attachments are therefore read *here*
|
|
13
|
+
* and arrive as text: a text file is decoded, a document is extracted through
|
|
14
|
+
* the deployment's own document pipeline (the same runtime, providers and
|
|
15
|
+
* limits the `document_*` tools use), and the result joins the prompt as one
|
|
16
|
+
* more text part.
|
|
17
|
+
*
|
|
18
|
+
* That is a deliberate trade. It costs the fidelity of a format the pipeline
|
|
19
|
+
* cannot read — such a request is refused with the bridge's own fallback signal
|
|
20
|
+
* rather than answered with a question the model cannot see the attachment for.
|
|
21
|
+
* It buys a path that works in every deployment shape: nothing is written into
|
|
22
|
+
* a shared workspace, the staging directory is temporary and removed after the
|
|
23
|
+
* extraction, and no second converter is grown inside this plugin.
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* Plain-text formats the bridge sends; their bytes *are* the content, so
|
|
27
|
+
* decoding them needs no pipeline and cannot fail on a deployment.
|
|
28
|
+
*/
|
|
29
|
+
export declare const QA_INTEGRATION_TEXT_MEDIA_TYPES: readonly string[];
|
|
30
|
+
/**
|
|
31
|
+
* Documents the deployment's pipeline is asked to extract. PDF and the OOXML
|
|
32
|
+
* family are what the bridge's own filter lets through; whether a particular
|
|
33
|
+
* one is readable is the pipeline's answer, not a list kept here.
|
|
34
|
+
*/
|
|
35
|
+
export declare const QA_INTEGRATION_DOCUMENT_MEDIA_TYPES: readonly string[];
|
|
36
|
+
/** Total inlined attachment text per question, across every attachment. */
|
|
37
|
+
export declare const QA_INTEGRATION_MAX_ATTACHMENT_CHARS = 120000;
|
|
38
|
+
/** No single attachment may claim the whole budget. */
|
|
39
|
+
export declare const QA_INTEGRATION_MAX_ATTACHMENT_CHARS_PER_FILE = 60000;
|
|
40
|
+
/** A file name is a label in the prompt, not a path. */
|
|
41
|
+
export declare const QA_INTEGRATION_MAX_ATTACHMENT_NAME = 120;
|
|
42
|
+
/** Whether the bytes are the content: decoding needs only UTF-8. */
|
|
43
|
+
export declare function isIntegrationTextMediaType(mediaType: string): boolean;
|
|
44
|
+
/** Whether the attachment has to go through the document pipeline. */
|
|
45
|
+
export declare function isIntegrationDocumentMediaType(mediaType: string): boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Reduce a caller-supplied file name to the label a prompt can carry.
|
|
48
|
+
*
|
|
49
|
+
* The name arrives from another system and is used as a log field, a heading in
|
|
50
|
+
* the prompt and the file name of one temporary file — so directory parts,
|
|
51
|
+
* traversal, control characters and an unbounded length all have to go before
|
|
52
|
+
* it is any of those. An empty result is named by its media type.
|
|
53
|
+
* @param raw - the name the caller sent.
|
|
54
|
+
* @param mediaType - its media type, for the fallback name.
|
|
55
|
+
* @returns a safe, non-empty label.
|
|
56
|
+
*/
|
|
57
|
+
export declare function safeAttachmentName(raw: string, mediaType: string): string;
|
|
58
|
+
/** What the extraction needs from the Host, as a port. */
|
|
59
|
+
export interface QaAttachmentDependencies {
|
|
60
|
+
/** The document pipeline, or undefined on a deployment without it. */
|
|
61
|
+
readonly documents: DocumentsFace | undefined;
|
|
62
|
+
/** The chat the prompt belongs to; the pipeline records artifacts against it. */
|
|
63
|
+
readonly sessionId: string;
|
|
64
|
+
readonly signal: AbortSignal;
|
|
65
|
+
readonly logger: PluginLogger;
|
|
66
|
+
/** Total inlined characters for this question. */
|
|
67
|
+
readonly maxChars?: number;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Turn the request's file attachments into prompt text parts.
|
|
71
|
+
*
|
|
72
|
+
* The parts are appended after the question, in the order the caller sent them,
|
|
73
|
+
* and the question itself is untouched. A file the Host cannot read refuses the
|
|
74
|
+
* whole request: answering with the question but not its attachments would hand
|
|
75
|
+
* the model a question about material it never received, which is exactly what
|
|
76
|
+
* the bridge's 415 fallback exists to avoid.
|
|
77
|
+
*
|
|
78
|
+
* @param files - the non-image attachments of one request.
|
|
79
|
+
* @param deps - pipeline, session and budgets.
|
|
80
|
+
* @returns one text part per readable attachment, plus a note when the budget cut the list.
|
|
81
|
+
*/
|
|
82
|
+
export declare function attachmentPromptParts(files: readonly QaFileAttachment[], deps: QaAttachmentDependencies): Promise<readonly PromptContentPart[]>;
|
|
83
|
+
/** Re-exported so the runner does not reach into the contract for one name. */
|
|
84
|
+
export { QaIntegrationAttachmentError };
|
|
85
|
+
//# sourceMappingURL=attachments.d.ts.map
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The integration API's wire contract: what an external application may post to
|
|
3
|
+
* `/qa/api/ask`, what it gets back, and the refusal vocabulary the HTTP layer
|
|
4
|
+
* maps onto status codes.
|
|
5
|
+
*
|
|
6
|
+
* The shape follows the ticket bridge specification the deployment was asked to
|
|
7
|
+
* implement (issue #177): a Markdown `answer`, the `sources` that back it, a
|
|
8
|
+
* `confidence` bucket, an `escalate` flag for "the assistant could not answer,
|
|
9
|
+
* send it to a specialist", and a `reason` for the logs. `chat_id` is the DSH
|
|
10
|
+
* session identity: sending it back as `session_id` continues the same
|
|
11
|
+
* conversation, which is the whole point of the bridge keeping it.
|
|
12
|
+
*/
|
|
13
|
+
import type { QaSourceReference } from "../provenance/types.js";
|
|
14
|
+
/** Why one integration request was refused, in the bridge's own vocabulary. */
|
|
15
|
+
export type QaIntegrationReason = "unauthorized" | "forbidden" | "not-found" | "invalid-request" | "unsupported-media" | "payload-too-large" | "rate-limited" | "busy" | "unavailable" | "timeout";
|
|
16
|
+
/**
|
|
17
|
+
* HTTP status per refusal. The mapping is part of the contract, not a detail:
|
|
18
|
+
* the bridge retries on 5xx and 429, falls back to a bodyless JSON request on
|
|
19
|
+
* 415, and treats 401/403 as a configuration error it must not spin on.
|
|
20
|
+
*/
|
|
21
|
+
export declare const QA_INTEGRATION_STATUS: Readonly<Record<QaIntegrationReason, number>>;
|
|
22
|
+
/** One refusal, carrying the status the HTTP layer answers with. */
|
|
23
|
+
export declare class QaIntegrationError extends Error {
|
|
24
|
+
readonly reason: QaIntegrationReason;
|
|
25
|
+
constructor(reason: QaIntegrationReason, message: string, options?: {
|
|
26
|
+
readonly cause?: unknown;
|
|
27
|
+
});
|
|
28
|
+
get status(): number;
|
|
29
|
+
}
|
|
30
|
+
/** One image the caller attached, already bounded and decoded to base64. */
|
|
31
|
+
export interface QaInlineImageAttachment {
|
|
32
|
+
readonly kind: "image";
|
|
33
|
+
/** A raster media type; anything else is refused before it gets here. */
|
|
34
|
+
readonly mediaType: string;
|
|
35
|
+
/** base64 bytes, exactly as the harness prompt content carries them. */
|
|
36
|
+
readonly data: string;
|
|
37
|
+
readonly name?: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* One non-image file the caller attached, still as bytes.
|
|
41
|
+
*
|
|
42
|
+
* Files cannot ride a prompt the way images do: the harness carries a file as
|
|
43
|
+
* an opaque receipt minted by its own upload service, which an external caller
|
|
44
|
+
* has no session to reach. What the bridge sends instead is read on the Host —
|
|
45
|
+
* text files decoded, documents extracted through the deployment's document
|
|
46
|
+
* pipeline — and the resulting text joins the prompt, which is the same
|
|
47
|
+
* material the model would have read out of the stored copy.
|
|
48
|
+
*/
|
|
49
|
+
export interface QaFileAttachment {
|
|
50
|
+
readonly kind: "file";
|
|
51
|
+
readonly mediaType: string;
|
|
52
|
+
readonly name: string;
|
|
53
|
+
/** Raw bytes, bounded by the configured per-attachment ceiling. */
|
|
54
|
+
readonly bytes: Buffer;
|
|
55
|
+
}
|
|
56
|
+
/** One attachment of a parsed request, in the order the caller sent it. */
|
|
57
|
+
export type QaIntegrationAttachment = QaInlineImageAttachment | QaFileAttachment;
|
|
58
|
+
/**
|
|
59
|
+
* Why one attachment could not become prompt content, in the vocabulary the
|
|
60
|
+
* service maps onto HTTP.
|
|
61
|
+
*
|
|
62
|
+
* `unsupported` is the bridge's own fallback signal (415): repeat the question
|
|
63
|
+
* without attachments. `unavailable` is the same answer for a document the
|
|
64
|
+
* deployment cannot read right now, and the two are deliberately distinct only
|
|
65
|
+
* in the log — a caller sees one status either way.
|
|
66
|
+
*/
|
|
67
|
+
export type QaIntegrationAttachmentReason = "unsupported" | "unavailable";
|
|
68
|
+
/** One attachment the Host could not turn into prompt content. */
|
|
69
|
+
export declare class QaIntegrationAttachmentError extends Error {
|
|
70
|
+
readonly attachment: string;
|
|
71
|
+
readonly reason: QaIntegrationAttachmentReason;
|
|
72
|
+
constructor(attachment: string, reason: QaIntegrationAttachmentReason, message: string);
|
|
73
|
+
}
|
|
74
|
+
/** A parsed `POST /qa/api/ask` body, whichever encoding carried it. */
|
|
75
|
+
export interface QaAskRequest {
|
|
76
|
+
/** The question; at least one non-whitespace character. */
|
|
77
|
+
readonly message: string;
|
|
78
|
+
/** Product version the bridge extracted from the ticket, when it knew one. */
|
|
79
|
+
readonly version: string | null;
|
|
80
|
+
/** The chat to continue, or null for a new one. */
|
|
81
|
+
readonly sessionId: string | null;
|
|
82
|
+
/** Ticket metadata the operator sees in the log line. */
|
|
83
|
+
readonly context: QaAskContext;
|
|
84
|
+
readonly attachments: readonly QaIntegrationAttachment[];
|
|
85
|
+
}
|
|
86
|
+
/** The ticket metadata the bridge sends along; all of it is optional. */
|
|
87
|
+
export interface QaAskContext {
|
|
88
|
+
readonly ticketKey: string | null;
|
|
89
|
+
readonly reporter: string | null;
|
|
90
|
+
readonly reporterName: string | null;
|
|
91
|
+
}
|
|
92
|
+
/** A successful answer, in the exact shape the bridge publishes. */
|
|
93
|
+
export interface QaAskAnswer {
|
|
94
|
+
readonly chatId: string;
|
|
95
|
+
readonly answer: string;
|
|
96
|
+
readonly sources: readonly string[];
|
|
97
|
+
readonly confidence: "high" | "medium" | "low";
|
|
98
|
+
/** True when the assistant produced nothing worth publishing. */
|
|
99
|
+
readonly escalate: boolean;
|
|
100
|
+
/** Why it escalated — for the bridge's log, never for a client. */
|
|
101
|
+
readonly reason: string;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* One message of a conversation as an external caller reads it.
|
|
105
|
+
*
|
|
106
|
+
* The text is the same flattening the answer itself uses (prose only, with
|
|
107
|
+
* attachments named), so a caller that keeps asking questions and a caller
|
|
108
|
+
* that reads back the transcript never see two versions of one message.
|
|
109
|
+
*/
|
|
110
|
+
export interface QaIntegrationMessage {
|
|
111
|
+
/** Durable position of the message; the cursor for the next read. */
|
|
112
|
+
readonly seq: number;
|
|
113
|
+
readonly role: "user" | "assistant";
|
|
114
|
+
readonly text: string;
|
|
115
|
+
/** ISO timestamp, when the log recorded one for the event. */
|
|
116
|
+
readonly at?: string;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* A conversation as the caller that owns it reads it back.
|
|
120
|
+
*
|
|
121
|
+
* `sessions:read` exists for exactly this: a bridge that escalated a question,
|
|
122
|
+
* or that wants to show the specialist what was already said, reads the chat
|
|
123
|
+
* it started without asking it again. The read is bounded from both ends —
|
|
124
|
+
* `after` is the caller's own cursor, the newest `limit` messages are the
|
|
125
|
+
* window — so a long conversation costs one page, not one transcript per call.
|
|
126
|
+
*/
|
|
127
|
+
export interface QaIntegrationTranscript {
|
|
128
|
+
readonly chatId: string;
|
|
129
|
+
/** The window, oldest first. */
|
|
130
|
+
readonly messages: readonly QaIntegrationMessage[];
|
|
131
|
+
/** Newest seq the caller has now seen; 0 when the chat holds no messages. */
|
|
132
|
+
readonly lastSeq: number;
|
|
133
|
+
/** True when older messages exist below the window. */
|
|
134
|
+
readonly truncated: boolean;
|
|
135
|
+
}
|
|
136
|
+
/** `GET /qa/api/health` payload; the bridge only needs `ok`. */
|
|
137
|
+
export interface QaHealth {
|
|
138
|
+
readonly ok: boolean;
|
|
139
|
+
readonly version: string;
|
|
140
|
+
readonly models: readonly string[];
|
|
141
|
+
readonly uptimeS: number;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* What one answered turn hands back to the service: the answer text, the
|
|
145
|
+
* sources that back it and whether the turn was cut short. The runner owns the
|
|
146
|
+
* session mechanics; nothing about sessions leaks into the wire contract.
|
|
147
|
+
*/
|
|
148
|
+
export interface QaIntegrationTurn {
|
|
149
|
+
readonly chatId: string;
|
|
150
|
+
readonly answer: string;
|
|
151
|
+
readonly sources: readonly QaSourceReference[];
|
|
152
|
+
/** True when the turn was cancelled or the log recorded an interruption. */
|
|
153
|
+
readonly interrupted: boolean;
|
|
154
|
+
}
|
|
155
|
+
/** The one host capability the integration service needs, as a port. */
|
|
156
|
+
export interface QaIntegrationRunner {
|
|
157
|
+
/**
|
|
158
|
+
* Send one question into a chat and resolve once its turn has settled.
|
|
159
|
+
*
|
|
160
|
+
* Implementations must enforce that `userId` may use `chatId` — the service
|
|
161
|
+
* has already checked the credential, and whoever owns session creation owns
|
|
162
|
+
* the ownership boundary that goes with it.
|
|
163
|
+
*/
|
|
164
|
+
run(input: {
|
|
165
|
+
readonly userId: string;
|
|
166
|
+
/** The chat to continue, or null for a new conversation. */
|
|
167
|
+
readonly chatId: string | null;
|
|
168
|
+
readonly message: string;
|
|
169
|
+
readonly attachments: readonly QaIntegrationAttachment[];
|
|
170
|
+
readonly ticketKey: string | null;
|
|
171
|
+
readonly signal: AbortSignal;
|
|
172
|
+
/**
|
|
173
|
+
* Called as soon as the chat exists, before the turn settles. The service
|
|
174
|
+
* needs the identity of a chat whose answer it may never see: a timed-out
|
|
175
|
+
* question still has to hand the bridge a chat it can continue.
|
|
176
|
+
*/
|
|
177
|
+
readonly onChat?: (chatId: string) => void;
|
|
178
|
+
}): Promise<QaIntegrationTurn>;
|
|
179
|
+
/**
|
|
180
|
+
* Read back the messages of one conversation.
|
|
181
|
+
*
|
|
182
|
+
* The service has already authenticated the credential and established that
|
|
183
|
+
* the chat belongs to that account; the implementation reads the log and
|
|
184
|
+
* projects it, and reports an unreadable log rather than an empty answer.
|
|
185
|
+
*/
|
|
186
|
+
transcript(input: {
|
|
187
|
+
readonly chatId: string;
|
|
188
|
+
/** Exclusive lower bound: only messages after this seq are returned. */
|
|
189
|
+
readonly after: number;
|
|
190
|
+
/** Newest messages to return; older ones are reported as truncated. */
|
|
191
|
+
readonly limit: number;
|
|
192
|
+
}): Promise<QaIntegrationTranscript>;
|
|
193
|
+
/** Model ids the deployment can route right now, for the health payload. */
|
|
194
|
+
models(): Promise<readonly string[]>;
|
|
195
|
+
}
|
|
196
|
+
//# sourceMappingURL=contract.d.ts.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Context } from "@deepseek-ai/cordis";
|
|
2
|
+
import type { PluginLogger } from "@yadsh/dsh-plugin-log";
|
|
3
|
+
import type { QaAccessService } from "../access/service.js";
|
|
4
|
+
import type { QaAccounts } from "../accounts/store.js";
|
|
5
|
+
import type { QaSessionLogReader } from "../admin/session-log.js";
|
|
6
|
+
import type { QaProvenanceHost } from "../provenance/host-store.js";
|
|
7
|
+
import type { QaPolicyAdmission } from "../secure-session.js";
|
|
8
|
+
import type { ResolvedQaSurfaceConfig } from "../types.js";
|
|
9
|
+
import type { DocumentsFace } from "@yadsh/dsh-documents";
|
|
10
|
+
import type { QaIntegrationRunner } from "./contract.js";
|
|
11
|
+
/**
|
|
12
|
+
* The session half of the integration API: open a chat, send one question,
|
|
13
|
+
* wait for the turn, read the answer.
|
|
14
|
+
*
|
|
15
|
+
* It is deliberately not a new turn engine. Opening a chat goes through the
|
|
16
|
+
* same three steps the browser's `createSession` takes — the deployment
|
|
17
|
+
* preflight, the per-user workspace, then `secureSession` — because that last
|
|
18
|
+
* call is what pins the QA tool policy, attaches the QA catalog and records
|
|
19
|
+
* the chat as attested. A question is admitted with
|
|
20
|
+
* `sessionController.prompt`, the same call the browser makes; the wait is
|
|
21
|
+
* `agent.whenIdle()`, which is the harness's own definition of "this turn is
|
|
22
|
+
* over"; the answer is read back from the durable log rather than from the
|
|
23
|
+
* live assistant stream, so what the bridge publishes is what was committed.
|
|
24
|
+
*/
|
|
25
|
+
export interface QaIntegrationRunnerOptions {
|
|
26
|
+
ctx: Context;
|
|
27
|
+
getConfig(): ResolvedQaSurfaceConfig;
|
|
28
|
+
/** The accounts store, or undefined while accounts are disabled. */
|
|
29
|
+
accounts(): QaAccounts | undefined;
|
|
30
|
+
admission: QaPolicyAdmission;
|
|
31
|
+
access: QaAccessService;
|
|
32
|
+
sessionLog: QaSessionLogReader;
|
|
33
|
+
provenance: QaProvenanceHost;
|
|
34
|
+
/**
|
|
35
|
+
* The deployment's document pipeline, resolved per call so a provider that
|
|
36
|
+
* installs later still counts. Absent on a deployment that has none, where
|
|
37
|
+
* document attachments are refused with the caller's own fallback signal.
|
|
38
|
+
*/
|
|
39
|
+
documents(): DocumentsFace | undefined;
|
|
40
|
+
logger: PluginLogger;
|
|
41
|
+
}
|
|
42
|
+
/** Build the runner for one plugin instance. */
|
|
43
|
+
export declare function createQaIntegrationRunner(options: QaIntegrationRunnerOptions): QaIntegrationRunner;
|
|
44
|
+
//# sourceMappingURL=host-runner.d.ts.map
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import type { WebServer } from "@deepseek-ai/dsh-host-webserver";
|
|
3
|
+
import type { PluginLogger } from "@yadsh/dsh-plugin-log";
|
|
4
|
+
import type { ResolvedQaSurfaceConfig } from "../types.js";
|
|
5
|
+
import { type QaAskRequest } from "./contract.js";
|
|
6
|
+
import type { QaIntegrationService } from "./service.js";
|
|
7
|
+
/** The bridge's own ceiling: five attachments per question. */
|
|
8
|
+
export declare const QA_INTEGRATION_MAX_FILES = 5;
|
|
9
|
+
export interface QaIntegrationRouteOptions {
|
|
10
|
+
config: ResolvedQaSurfaceConfig;
|
|
11
|
+
service: QaIntegrationService;
|
|
12
|
+
logger: PluginLogger;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Register the integration routes on the host web server.
|
|
16
|
+
* @param webServer - the route table owner.
|
|
17
|
+
* @param options - resolved config, the service and the plugin logger.
|
|
18
|
+
* @returns the disposer removing every route this call registered.
|
|
19
|
+
*/
|
|
20
|
+
export declare function registerQaIntegrationRoutes(webServer: Pick<WebServer, "register">, options: QaIntegrationRouteOptions): () => void;
|
|
21
|
+
/** The page size a caller gets when it does not ask for one. */
|
|
22
|
+
export declare const QA_INTEGRATION_DEFAULT_TRANSCRIPT_LIMIT = 50;
|
|
23
|
+
/**
|
|
24
|
+
* Parse one `POST /ask` body out of whichever encoding carried it.
|
|
25
|
+
*
|
|
26
|
+
* JSON is the primary form. Multipart exists because the bridge sends the
|
|
27
|
+
* ticket's attachments through it, and images ride a prompt inline in this
|
|
28
|
+
* harness — so an image-only multipart request is answered like a JSON one. A
|
|
29
|
+
* non-image attachment has nowhere to go: files reach a prompt as a receipt
|
|
30
|
+
* minted by the browser upload path, which an external caller has no session
|
|
31
|
+
* to mint. That request is refused with 415, which is exactly the signal the
|
|
32
|
+
* bridge's own fallback waits for: it repeats the question without
|
|
33
|
+
* attachments rather than failing.
|
|
34
|
+
*
|
|
35
|
+
* @param body - the raw request body.
|
|
36
|
+
* @param contentType - the request's content type, parameters included.
|
|
37
|
+
* @param config - resolved config, for the attachment ceilings.
|
|
38
|
+
* @returns the normalized request.
|
|
39
|
+
*/
|
|
40
|
+
export declare function parseAskBody(body: Buffer, contentType: string | undefined, config: ResolvedQaSurfaceConfig): QaAskRequest;
|
|
41
|
+
/** One image part of a multipart body, bounded as it was read. */
|
|
42
|
+
interface MultipartFile {
|
|
43
|
+
readonly filename: string;
|
|
44
|
+
readonly mediaType: string;
|
|
45
|
+
readonly bytes: Buffer;
|
|
46
|
+
}
|
|
47
|
+
interface MultipartBody {
|
|
48
|
+
readonly fields: ReadonlyMap<string, string>;
|
|
49
|
+
readonly files: readonly MultipartFile[];
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Split a multipart body into its text fields and files.
|
|
53
|
+
*
|
|
54
|
+
* Hand-written and deliberately minimal: one request shape (a flat form of
|
|
55
|
+
* scalar fields and `files` parts), one boundary, no nested multiparts. The
|
|
56
|
+
* body is already bounded by the caller, so no part can grow without limit.
|
|
57
|
+
*
|
|
58
|
+
* @param body - the raw request body.
|
|
59
|
+
* @param boundary - the boundary parameter of the content type.
|
|
60
|
+
* @returns text fields and file parts, in the order they appeared.
|
|
61
|
+
*/
|
|
62
|
+
export declare function parseMultipart(body: Buffer, boundary: string): MultipartBody;
|
|
63
|
+
/**
|
|
64
|
+
* Read the whole request body under a byte ceiling.
|
|
65
|
+
* @param request - the incoming request.
|
|
66
|
+
* @param limit - accepted body size in bytes.
|
|
67
|
+
* @returns the body.
|
|
68
|
+
*/
|
|
69
|
+
export declare function readBody(request: IncomingMessage, limit: number): Promise<Buffer>;
|
|
70
|
+
/**
|
|
71
|
+
* One refusal, mapped onto its status.
|
|
72
|
+
*
|
|
73
|
+
* An unexpected error is answered as `unavailable` rather than leaking a stack
|
|
74
|
+
* trace: the bridge retries a 5xx, and the deployment's own log holds the
|
|
75
|
+
* detail.
|
|
76
|
+
*/
|
|
77
|
+
export declare function sendRefusal(response: ServerResponse, error: unknown): void;
|
|
78
|
+
export {};
|
|
79
|
+
//# sourceMappingURL=http.d.ts.map
|