fix(web): align session search contracts

This commit is contained in:
Hypatia May
2026-07-27 18:38:00 +08:00
parent 621f414407
commit 4727d742db
42 changed files with 235 additions and 196 deletions

View File

@@ -1,7 +1,7 @@
// Central contract re-export point: every contract import inside
// web-runtime goes through this single file.
// Types are type-only imports from the apiproxy api/ layer (zero Node deps, browser-safe);
// the only runtime values are the RpcId constructor and the AbstractApiClient seam.
// Types and runtime protocol helpers/bounds come from the apiproxy api/ layer
// (zero Node deps, browser-safe); AbstractApiClient is the client seam.
// NEVER import the package root: it drags bootHost/cordis into the browser bundle.
// The ./api and ./client subpath exports are the browser-safe channels added for this.
@@ -19,7 +19,11 @@ export type {
// transportError moved down to the apiproxy api layer (it belongs beside
// RpcResult, its subject); re-exported here so connection consumers keep one
// contract entry point.
export { RpcId, transportError } from '@deepseek-ai/dsh-host-apiproxy/api'
export {
RpcId,
SESSION_SEARCH_RESULT_LIMIT,
transportError,
} from '@deepseek-ai/dsh-host-apiproxy/api'
export { AbstractApiClient } from '@deepseek-ai/dsh-host-apiproxy/client'
export type { IApiClient } from '@deepseek-ai/dsh-host-apiproxy/client'
export type { SessionId, SessionEvent } from '@deepseek-ai/dsh-session/types'

View File

@@ -14,7 +14,7 @@ import type {
ToolCallView, ToolEventView, ToolResultView, WorkspaceId, WorkspaceView,
} from './api.ts'
import type { RequestPayload, ResponseValue, RpcMethodMap } from '@deepseek-ai/dsh-host-apiproxy/api'
import { AbstractApiClient, RpcId } from './api.ts'
import { AbstractApiClient, RpcId, SESSION_SEARCH_RESULT_LIMIT } from './api.ts'
/** The fake carrier mints like a real one (business code never mints). */
function rpcRequest<P>(payload: P): RpcRequest<P> {
@@ -302,8 +302,9 @@ function pageOf(
function searchBlockText(block: ContentBlock): string[] {
switch (block.type) {
case 'text':
case 'reasoning':
return [block.text]
case 'reasoning':
return []
case 'tool-call':
return [block.name, block.arguments]
case 'tool-result':
@@ -425,7 +426,7 @@ interface FixtureSearchCandidate {
documentLength: number
}
/** Same rank keys as session-query-sqlite's cross-session result order. */
/** Mirrors `packages/session-query/session-query-sqlite/src/index.ts`; update both together. */
function compareSearchCandidates(a: FixtureSearchCandidate, b: FixtureSearchCandidate): number {
if (a.matchCount !== b.matchCount) return b.matchCount - a.matchCount
if (a.documentLength !== b.documentLength) return a.documentLength - b.documentLength
@@ -741,11 +742,11 @@ export function createFixtureApi(options: FixtureOptions = {}): ApiProxy {
return best === undefined ? [] : [best]
}).sort(compareSearchCandidates)
return ok(request, {
items: matches.slice(0, 20).map(match => ({
items: matches.slice(0, SESSION_SEARCH_RESULT_LIMIT).map(match => ({
sessionId: match.sessionId,
snippet: searchSnippet(match.text, match.matchStart, match.matchEnd),
})),
hasMore: matches.length > 20,
hasMore: matches.length > SESSION_SEARCH_RESULT_LIMIT,
})
},
create: async (request) => {

View File

@@ -5,6 +5,7 @@
*/
import type { Context } from 'cordis'
import type { IApiClient } from './api.ts'
import { SESSION_SEARCH_RESULT_LIMIT } from './api.ts'
import { ConnectionController, type ConnectionConfig, type ConnectionSinks, type ConnectionState } from './connection.ts'
import { FixtureApiClient } from './fixture.ts'
import { WebApiClient } from './web-api-client.ts'
@@ -19,7 +20,12 @@ export type {
ClientRequest, ServerResponse, ServerRequest, ClientResponse, RpcMessage, RpcReceipt,
IApiClient, SessionId, SessionEvent, ContentBlock, StreamChunk,
} from './api.ts'
export { RpcId, AbstractApiClient, transportError } from './api.ts'
export {
RpcId,
SESSION_SEARCH_RESULT_LIMIT,
AbstractApiClient,
transportError,
} from './api.ts'
// Connection loop types are public through ConnectionHandle.start; the
// controller remains package-internal.
@@ -37,6 +43,8 @@ export const inject: string[] = []
export interface ConnectionHandle {
/** Shared api client (fixture or real, decided at boot from the page URL). */
readonly api: IApiClient
/** Protocol-owned maximum rows for one session-search response. */
readonly sessionSearchResultLimit: number
/**
* Start the connect/pump/reconnect loop with the consumer's frame sinks.
* One consumer owns the streams (the runtime object layer); a second call
@@ -58,6 +66,7 @@ export function apply(ctx: Context): void {
let started = false
const handle: ConnectionHandle = {
api,
sessionSearchResultLimit: SESSION_SEARCH_RESULT_LIMIT,
start(sinks, config) {
if (started) throw new Error('connection: the stream loop is already owned by another consumer')
started = true

View File

@@ -89,6 +89,11 @@ describe('createFixtureApi', () => {
ok: true,
value: { items: [], hasMore: false },
})
const reasoningOnly = await api.sessions.search(req({ query: '思考过程' }), signal)
expect(reasoningOnly.result).toEqual({
ok: true,
value: { items: [], hasMore: false },
})
const aborted = new AbortController()
aborted.abort()