dsh-browser-application 0.37.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/LICENSE +19 -0
- package/README.md +27 -0
- package/cordis.patch.yml +29 -0
- package/lib/index.js +3377 -0
- package/lib/invariant.js +26 -0
- package/lib/types/bridge-url.d.ts +27 -0
- package/lib/types/browser-context.d.ts +38 -0
- package/lib/types/dsh-gateway.d.ts +42 -0
- package/lib/types/event-generation.d.ts +56 -0
- package/lib/types/extension-sessions.d.ts +26 -0
- package/lib/types/host-api.d.ts +47 -0
- package/lib/types/image-relay.d.ts +43 -0
- package/lib/types/index.d.ts +158 -0
- package/lib/types/invariant.d.ts +16 -0
- package/lib/types/remote-host-api.d.ts +12 -0
- package/lib/types/server.d.ts +166 -0
- package/lib/types/session-deferral.d.ts +33 -0
- package/lib/types/session-history.d.ts +30 -0
- package/lib/types/session-purge.d.ts +55 -0
- package/lib/types/session-workspace.d.ts +37 -0
- package/lib/types/token.d.ts +57 -0
- package/lib/types/tools.d.ts +42 -0
- package/lib/types/vision-selfcheck.d.ts +18 -0
- package/lib/types/vision.d.ts +57 -0
- package/package.json +95 -0
- package/src/bridge-url.ts +57 -0
- package/src/browser-context.ts +102 -0
- package/src/dsh-gateway.ts +66 -0
- package/src/event-generation.ts +385 -0
- package/src/extension-sessions.ts +40 -0
- package/src/host-api.ts +64 -0
- package/src/image-relay.ts +118 -0
- package/src/index.ts +575 -0
- package/src/invariant.ts +33 -0
- package/src/remote-host-api.ts +397 -0
- package/src/server.ts +658 -0
- package/src/session-deferral.ts +296 -0
- package/src/session-history.ts +220 -0
- package/src/session-purge.ts +154 -0
- package/src/session-workspace.ts +147 -0
- package/src/token.ts +100 -0
- package/src/tools.ts +301 -0
- package/src/vision-selfcheck.ts +35 -0
- package/src/vision.ts +135 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Defer real session creation until the first prompt.
|
|
3
|
+
*
|
|
4
|
+
* The panel calls `session.create` as soon as it connects, but a session that
|
|
5
|
+
* is opened and never used should leave zero trace in the store/GUI. This
|
|
6
|
+
* wrapper answers `session.create` with a provisional id (minted locally,
|
|
7
|
+
* nothing persisted), serves `session.history` for provisional ids as empty,
|
|
8
|
+
* and materializes the real session — same id, original create payload — on
|
|
9
|
+
* the first `session.prompt` for that id. Abandoned provisional ids are
|
|
10
|
+
* pruned after {@link PROVISIONAL_TTL_MS}.
|
|
11
|
+
*
|
|
12
|
+
* Provisional sessions also answer `session.models` from the host-wide
|
|
13
|
+
* `session.modelCatalog` (via the Host API adapter, plus a pending switch)
|
|
14
|
+
* and remember `session.selectModel` until materialization, so the composer can
|
|
15
|
+
* show a model switcher before the first message.
|
|
16
|
+
*
|
|
17
|
+
* @module dsh-browser-application/src/session-deferral
|
|
18
|
+
*/
|
|
19
|
+
import type { ImageAttachmentLimits } from '@deepseek-ai/dsh-attachment';
|
|
20
|
+
import type { BrowserHostApi } from './host-api.ts';
|
|
21
|
+
/**
|
|
22
|
+
* Wrap the gateway sessions API so `session.create` returns a provisional id
|
|
23
|
+
* without creating anything; the real session materializes on the first
|
|
24
|
+
* `session.prompt` for that id.
|
|
25
|
+
*
|
|
26
|
+
* @param api - Gateway API implementation.
|
|
27
|
+
* @param enabled - Whether deferral is active; false returns the API untouched.
|
|
28
|
+
* @param imageLimits - actual host image capability, used for the synthetic
|
|
29
|
+
* empty history before the deferred Session exists.
|
|
30
|
+
* @returns the original API when disabled, otherwise the wrapped API.
|
|
31
|
+
*/
|
|
32
|
+
export declare function withSessionDeferral(api: BrowserHostApi, enabled: boolean, imageLimits?: ImageAttachmentLimits): BrowserHostApi;
|
|
33
|
+
//# sourceMappingURL=session-deferral.d.ts.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session history decoding: the `session/follow` baseline, its records, and the
|
|
3
|
+
* compact chunk-row expansion older logs still use.
|
|
4
|
+
*
|
|
5
|
+
* @module dsh-browser-application/src/session-history
|
|
6
|
+
*/
|
|
7
|
+
import { type TypertGatewayLike } from './dsh-gateway.ts';
|
|
8
|
+
export interface SessionSnapshot {
|
|
9
|
+
/** Inclusive Host log tip from session/follow; required for later session/page calls. */
|
|
10
|
+
readonly cursor: number;
|
|
11
|
+
readonly records: readonly unknown[];
|
|
12
|
+
readonly hasMore: boolean;
|
|
13
|
+
readonly projections?: unknown;
|
|
14
|
+
readonly assistantStream?: unknown;
|
|
15
|
+
readonly snapshotId?: string;
|
|
16
|
+
}
|
|
17
|
+
export declare function oneShotSessionSnapshot(gateway: TypertGatewayLike, sessionId: string, outerSignal: AbortSignal, maxMessages?: number): Promise<SessionSnapshot>;
|
|
18
|
+
export declare function historyValue(snapshot: SessionSnapshot): Record<string, unknown>;
|
|
19
|
+
export declare function historyPageValue(page: unknown): Record<string, unknown>;
|
|
20
|
+
export declare function optionalNonNegativeInteger(payload: unknown, key: string): number | undefined;
|
|
21
|
+
export declare function optionalPositiveInteger(payload: unknown, key: string): number | undefined;
|
|
22
|
+
export declare function isSessionSnapshot(value: unknown): value is {
|
|
23
|
+
readonly type: 'snapshot';
|
|
24
|
+
readonly cursor: number;
|
|
25
|
+
readonly records: readonly unknown[];
|
|
26
|
+
readonly hasMore: boolean;
|
|
27
|
+
readonly projections?: unknown;
|
|
28
|
+
readonly assistantStream?: unknown;
|
|
29
|
+
};
|
|
30
|
+
//# sourceMappingURL=session-history.d.ts.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File-level removal of one session's durable storage under the dsh home.
|
|
3
|
+
*
|
|
4
|
+
* The gateway exposes no session.delete, so the bridge performs the removal
|
|
5
|
+
* itself: this module archives the session under exclusive write ownership,
|
|
6
|
+
* then removes its durable data while retaining the kernel lock's pathname.
|
|
7
|
+
* Session ids are validated against the persisted shape, only data within
|
|
8
|
+
* exact-name directories two levels below the sessions root is removed, and
|
|
9
|
+
* running sessions are refused before anything touches the disk.
|
|
10
|
+
*
|
|
11
|
+
* @module dsh-browser-application/src/session-purge
|
|
12
|
+
*/
|
|
13
|
+
/** Stable failure codes surfaced to the panel. Open set: callers must tolerate growth. */
|
|
14
|
+
export type SessionPurgeErrorCode = 'not-found' | 'running' | 'invalid-id' | 'internal';
|
|
15
|
+
/** Error thrown by {@link purgeSessionFiles}; the server turns it into a wire error. */
|
|
16
|
+
export declare class SessionPurgeError extends Error {
|
|
17
|
+
readonly code: SessionPurgeErrorCode;
|
|
18
|
+
constructor(code: SessionPurgeErrorCode, message: string, options?: ErrorOptions);
|
|
19
|
+
}
|
|
20
|
+
/** Dependencies purging needs from the plugin. */
|
|
21
|
+
export interface SessionPurgeDeps {
|
|
22
|
+
/** The dsh sessions root (`dshHomePath('sessions')`). */
|
|
23
|
+
sessionsRoot: string;
|
|
24
|
+
/** Session ids currently running; purging any of these is refused. */
|
|
25
|
+
runningSessionIds: ReadonlySet<string>;
|
|
26
|
+
/**
|
|
27
|
+
* Claim the runtime's exclusive write ownership (including its kernel lock).
|
|
28
|
+
* The returned handle must remain held until removal finishes. Opening a
|
|
29
|
+
* read handle, checking for a lock file, or checking only Agent status does
|
|
30
|
+
* not provide exclusion: idle Agents and other processes can own the log.
|
|
31
|
+
*/
|
|
32
|
+
acquireOwnership(sessionId: string): Promise<{
|
|
33
|
+
close(): Promise<void>;
|
|
34
|
+
}>;
|
|
35
|
+
/** Archive while the durable session still exists and exclusive ownership is held. */
|
|
36
|
+
archiveSession(sessionId: string): Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Validate one session id against the persisted shape. Rejects everything
|
|
40
|
+
* that could escape the sessions root (separators, dot segments) before any
|
|
41
|
+
* filesystem call sees it.
|
|
42
|
+
* @param sessionId - untrusted id from the panel.
|
|
43
|
+
* @returns the id when well-formed.
|
|
44
|
+
* @throws SessionPurgeError with code `invalid-id` otherwise.
|
|
45
|
+
*/
|
|
46
|
+
export declare function assertPurgeableSessionId(sessionId: string): string;
|
|
47
|
+
/**
|
|
48
|
+
* Permanently delete a session's data, keeping its directory and lock inode.
|
|
49
|
+
* The runtime refuses ambiguous duplicate session identities across workspaces.
|
|
50
|
+
* @param deps - root and running-set inputs.
|
|
51
|
+
* @param sessionId - validated session id.
|
|
52
|
+
* @returns nothing; throws {@link SessionPurgeError} on refusal or failure.
|
|
53
|
+
*/
|
|
54
|
+
export declare function purgeSessionFiles(deps: SessionPurgeDeps, sessionId: string): Promise<void>;
|
|
55
|
+
//# sourceMappingURL=session-purge.d.ts.map
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Best-effort workspace grouping for sessions created through the browser
|
|
3
|
+
* bridge.
|
|
4
|
+
*
|
|
5
|
+
* The wrapper touches exactly one request: an implicit `session.create`, which
|
|
6
|
+
* it gives the browser group's workspace id. Explicit workspace choices and
|
|
7
|
+
* every other gateway method pass through untouched — including the
|
|
8
|
+
* `workspace.create` and `workspace.rename` calls this module makes on its own
|
|
9
|
+
* behalf, which are issued through the same API and must not be intercepted.
|
|
10
|
+
*
|
|
11
|
+
* Grouping is best-effort by design. A failure returns the original call
|
|
12
|
+
* ungrouped rather than failing the prompt, because a conversation the user can
|
|
13
|
+
* still have is worth more than a tidy list.
|
|
14
|
+
*
|
|
15
|
+
* @module dsh-browser-application/src/session-workspace
|
|
16
|
+
*/
|
|
17
|
+
import type { BrowserHostApi } from './host-api.ts';
|
|
18
|
+
type Warn = (message: string) => void;
|
|
19
|
+
/**
|
|
20
|
+
* Add a dedicated Workspace to implicit session creation without making
|
|
21
|
+
* grouping a session-creation dependency. The first implicit create mkdirs
|
|
22
|
+
* and registers the configured path; that result, including failure, is
|
|
23
|
+
* cached for the wrapper lifetime.
|
|
24
|
+
*
|
|
25
|
+
* @param api - Injected gateway API implementation.
|
|
26
|
+
* @param workspacePath - Dedicated directory, or an empty string to opt out.
|
|
27
|
+
* @param workspaceTitle - Display name for the group, or an empty string to keep
|
|
28
|
+
* the name the desktop derives from the directory. A title is needed because
|
|
29
|
+
* that derived name is the directory's, so a fresh install shows
|
|
30
|
+
* "browser-sessions" — an internal-sounding label the user has no reason to
|
|
31
|
+
* open, and no way to rename from the interface.
|
|
32
|
+
* @param warn - Logger called once when grouping cannot be established.
|
|
33
|
+
* @returns the original API for opt-out, otherwise an API with wrapped session creation.
|
|
34
|
+
*/
|
|
35
|
+
export declare function withSessionWorkspace(api: BrowserHostApi, workspacePath: string, workspaceTitle: string, warn: Warn): BrowserHostApi;
|
|
36
|
+
export {};
|
|
37
|
+
//# sourceMappingURL=session-workspace.d.ts.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bridge bearer-token lifecycle: generation, constant-time verification, and
|
|
3
|
+
* file persistence under the dsh home directory.
|
|
4
|
+
*
|
|
5
|
+
* The token authenticates the browser extension against the bridge WebSocket.
|
|
6
|
+
* It is NOT the /api trust fence (that stays untouched); it is the bridge
|
|
7
|
+
* path's own auth because the bridge route lives outside the fence by design.
|
|
8
|
+
*
|
|
9
|
+
* @module
|
|
10
|
+
*/
|
|
11
|
+
/** File name of the persisted token inside the dsh home. */
|
|
12
|
+
export declare const TOKEN_FILE_NAME = "ext-bridge-token";
|
|
13
|
+
/**
|
|
14
|
+
* Generate a fresh token as lowercase hex.
|
|
15
|
+
* @param bytes - entropy bytes; defaults to DEFAULT_TOKEN_BYTES (256-bit).
|
|
16
|
+
* @returns the hex token string.
|
|
17
|
+
*/
|
|
18
|
+
export declare function generateToken(bytes?: number): string;
|
|
19
|
+
/**
|
|
20
|
+
* Constant-time token comparison. Length mismatch fails fast (still constant
|
|
21
|
+
* time on the compared prefix) — a wrong-length token can never verify.
|
|
22
|
+
* @param expected - the configured token.
|
|
23
|
+
* @param actual - the token presented by the client.
|
|
24
|
+
* @returns true only when both are equal-length hex and byte-equal.
|
|
25
|
+
*/
|
|
26
|
+
export declare function verifyToken(expected: string, actual: string): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Path of the persisted token file under the dsh home.
|
|
29
|
+
* @returns absolute path like `~/.dsh/ext-bridge-token`.
|
|
30
|
+
*/
|
|
31
|
+
export declare function tokenFilePath(): string;
|
|
32
|
+
/**
|
|
33
|
+
* Read the persisted token; returns undefined when absent or unreadable.
|
|
34
|
+
* @param file - token file path.
|
|
35
|
+
* @returns the stored hex token, trimmed.
|
|
36
|
+
*/
|
|
37
|
+
export declare function readTokenFile(file?: string): Promise<string | undefined>;
|
|
38
|
+
/**
|
|
39
|
+
* Persist a token atomically (temp file + rename) with 0600 permissions.
|
|
40
|
+
* @param token - hex token to persist.
|
|
41
|
+
* @param file - token file path.
|
|
42
|
+
*/
|
|
43
|
+
export declare function writeTokenFile(token: string, file?: string): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* Resolve the bridge token: an explicitly configured token wins; otherwise the
|
|
46
|
+
* persisted file is reused when present, and a fresh token is generated and
|
|
47
|
+
* persisted otherwise.
|
|
48
|
+
* @param configured - token from plugin config, or undefined.
|
|
49
|
+
* @param file - token file path (injectable for tests).
|
|
50
|
+
* @returns `{ token, file, generated }` where `generated` records whether a new token was minted.
|
|
51
|
+
*/
|
|
52
|
+
export declare function resolveToken(configured: string | undefined, file?: string): Promise<{
|
|
53
|
+
token: string;
|
|
54
|
+
file: string;
|
|
55
|
+
generated: boolean;
|
|
56
|
+
}>;
|
|
57
|
+
//# sourceMappingURL=token.d.ts.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model-facing browser tools. Every tool executes by dispatching a `tool.call`
|
|
3
|
+
* over the bridge to the connected extension, which performs the action in the
|
|
4
|
+
* user's explicitly controlled tab and returns a pure-text result.
|
|
5
|
+
*
|
|
6
|
+
* The surface is structured text by design: `browser_snapshot` renders the page
|
|
7
|
+
* with a numbered interactive inventory, and every other tool addresses elements
|
|
8
|
+
* by that inventory's stable index. Results are single `{ text }` objects.
|
|
9
|
+
*
|
|
10
|
+
* The tools differ only in name, description, parameter schema, and which
|
|
11
|
+
* arguments are forwarded, so they live in one table instead of fifteen
|
|
12
|
+
* near-identical blocks — which also makes "frame routing only on frame-local
|
|
13
|
+
* tools" a single visible column instead of a fact repeated fifteen times.
|
|
14
|
+
*
|
|
15
|
+
* @module
|
|
16
|
+
*/
|
|
17
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
18
|
+
import type { BridgeServer } from './server.ts';
|
|
19
|
+
/** Options resolved from plugin config before tool registration. */
|
|
20
|
+
export interface BrowserToolsOptions {
|
|
21
|
+
/** Per-tool-call budget in ms (also the bridge's default). */
|
|
22
|
+
toolTimeoutMs: number;
|
|
23
|
+
/** Upper bound on one snapshot's rendered characters. */
|
|
24
|
+
snapshotMaxChars: number;
|
|
25
|
+
/** Upper bound on interactive inventory items per snapshot. */
|
|
26
|
+
maxInteractiveItems: number;
|
|
27
|
+
}
|
|
28
|
+
/** The keys the extension accepts as wire action names (tool name == action name). */
|
|
29
|
+
export declare const BROWSER_TOOL_NAMES: readonly ["browser_snapshot", "browser_click", "browser_type", "browser_press", "browser_scroll", "browser_navigate", "browser_open_tab", "browser_list_tabs", "browser_follow_tab", "browser_close_tab", "browser_back", "browser_forward", "browser_reload", "browser_get_text", "browser_wait", "browser_describe_image"];
|
|
30
|
+
/**
|
|
31
|
+
* Register the browser tools on `ctx.tools`. Disposers are returned for the
|
|
32
|
+
* caller's effect to own; each tool's cooperative timeout budget is declared so
|
|
33
|
+
* the timeout policy can enforce it, and every execute forwards `exec.signal`
|
|
34
|
+
* into the bridge call (abort settles it).
|
|
35
|
+
*
|
|
36
|
+
* @param ctx - Cordis context with the tools service.
|
|
37
|
+
* @param bridge - the authenticated bridge server.
|
|
38
|
+
* @param options - resolved tool budgets.
|
|
39
|
+
* @returns disposers keyed by tool name.
|
|
40
|
+
*/
|
|
41
|
+
export declare function registerBrowserTools(ctx: Context, bridge: BridgeServer, options: BrowserToolsOptions): Map<string, () => void>;
|
|
42
|
+
//# sourceMappingURL=tools.d.ts.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The startup check that the cost switch actually took effect.
|
|
3
|
+
*
|
|
4
|
+
* A provider that ignores `thinking: {type: 'disabled'}` answers normally, so
|
|
5
|
+
* nothing looks wrong while every image costs several times the image itself —
|
|
6
|
+
* thinking tokens are the large part of the bill on a pure perception task. The
|
|
7
|
+
* request body cannot prove it was honoured; `usage` can.
|
|
8
|
+
*
|
|
9
|
+
* This runs once, in the background, and only ever warns. A deployment whose
|
|
10
|
+
* provider reports nothing stays quiet rather than crying wolf: zero means "none
|
|
11
|
+
* billed as far as this response says", not proof.
|
|
12
|
+
*
|
|
13
|
+
* @module
|
|
14
|
+
*/
|
|
15
|
+
import type { VisionClient } from './vision.ts';
|
|
16
|
+
/** Run the probe and report a switch that was silently ignored. */
|
|
17
|
+
export declare function checkThinkingIsOff(vision: VisionClient, warn: (message: string) => void): void;
|
|
18
|
+
//# sourceMappingURL=vision-selfcheck.d.ts.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The multimodal call the desktop makes on the extension's behalf.
|
|
3
|
+
*
|
|
4
|
+
* Only the desktop can hold the model credential without putting it in a browser
|
|
5
|
+
* profile, and only the desktop can reach the network through the machine's own
|
|
6
|
+
* proxy, VPN, or client certificate. This module is the transport: the prompt,
|
|
7
|
+
* the request body, and the response parser are shared with the extension's own
|
|
8
|
+
* outbound path, so "which side calls the model" changes nothing about what is
|
|
9
|
+
* asked or what counts as an answer.
|
|
10
|
+
*
|
|
11
|
+
* @module
|
|
12
|
+
*/
|
|
13
|
+
import { type VisionCallConfig, type VisionCallContext, type VisionCallImage, type VisionCallResult } from '@dsh-browser/protocol';
|
|
14
|
+
export type { VisionCallContext, VisionCallResult };
|
|
15
|
+
/** A 1×1 transparent PNG: the smallest thing a vision endpoint will accept. */
|
|
16
|
+
export declare const PROBE_IMAGE_BASE64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==";
|
|
17
|
+
/** What a probe learned about the deployment's cost controls. */
|
|
18
|
+
export interface VisionProbe {
|
|
19
|
+
ok: boolean;
|
|
20
|
+
/** Reasoning tokens the response reported; 0 when none or unreported. */
|
|
21
|
+
reasoningTokens: number;
|
|
22
|
+
message: string;
|
|
23
|
+
}
|
|
24
|
+
export interface VisionConfig extends VisionCallConfig {
|
|
25
|
+
/** Chat-completions base URL. */
|
|
26
|
+
baseUrl: string;
|
|
27
|
+
apiKey: string;
|
|
28
|
+
timeoutMs: number;
|
|
29
|
+
}
|
|
30
|
+
export interface VisionRequest extends VisionCallImage {
|
|
31
|
+
context: VisionCallContext;
|
|
32
|
+
}
|
|
33
|
+
export type VisionResult = VisionCallResult;
|
|
34
|
+
export declare class VisionClient {
|
|
35
|
+
private readonly config;
|
|
36
|
+
private readonly fetchImpl;
|
|
37
|
+
constructor(config: VisionConfig, fetchImpl?: typeof fetch);
|
|
38
|
+
/**
|
|
39
|
+
* Describe one image.
|
|
40
|
+
*
|
|
41
|
+
* @param request - normalized bytes plus the page context around them.
|
|
42
|
+
* @returns one line of description, or a classified failure.
|
|
43
|
+
*/
|
|
44
|
+
describe(request: VisionRequest): Promise<VisionResult>;
|
|
45
|
+
/**
|
|
46
|
+
* Send one minimal request and read the billing back.
|
|
47
|
+
*
|
|
48
|
+
* A thinking switch can be ignored silently — the request succeeds and the
|
|
49
|
+
* answer looks fine, while every image costs several times what it should. The
|
|
50
|
+
* only way to know is to look at `usage`, so this sends the smallest possible
|
|
51
|
+
* image once and reports what came back.
|
|
52
|
+
*
|
|
53
|
+
* @returns what the provider reported, or why the probe could not run.
|
|
54
|
+
*/
|
|
55
|
+
probe(): Promise<VisionProbe>;
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=vision.d.ts.map
|
package/package.json
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-browser-application",
|
|
3
|
+
"version": "0.37.0",
|
|
4
|
+
"description": "dsh 浏览器设置:把扩展连到 dsh 桌面端,提供 browser_* 工具,并在需要时替你识别页面上的图片。这是它的全部配置项。",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=20"
|
|
9
|
+
},
|
|
10
|
+
"dsh": {
|
|
11
|
+
"bundle": {
|
|
12
|
+
"patch": "./cordis.patch.yml"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"main": "lib/index.js",
|
|
16
|
+
"types": "lib/types/index.d.ts",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./lib/types/index.d.ts",
|
|
20
|
+
"default": "./lib/index.js"
|
|
21
|
+
},
|
|
22
|
+
"./invariant": {
|
|
23
|
+
"types": "./lib/types/invariant.d.ts",
|
|
24
|
+
"default": "./lib/invariant.js"
|
|
25
|
+
},
|
|
26
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
27
|
+
"./package.json": "./package.json",
|
|
28
|
+
"./src/*": "./src/*"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"lib/index.js",
|
|
32
|
+
"lib/invariant.js",
|
|
33
|
+
"lib/types/**/*.d.ts",
|
|
34
|
+
"cordis.patch.yml",
|
|
35
|
+
"src"
|
|
36
|
+
],
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"@deepseek-ai/cordis": "~4.0.4",
|
|
39
|
+
"@deepseek-ai/dsh-agent": "0.2.0-rc.2",
|
|
40
|
+
"@deepseek-ai/dsh-api-gateway": "0.2.0-rc.2",
|
|
41
|
+
"@deepseek-ai/dsh-attachment": "0.2.0-rc.2",
|
|
42
|
+
"@deepseek-ai/dsh-client-connection": "0.2.0-rc.2",
|
|
43
|
+
"@deepseek-ai/dsh-home-paths": "0.2.0-rc.2",
|
|
44
|
+
"@deepseek-ai/dsh-host-webserver": "0.2.0-rc.2",
|
|
45
|
+
"@deepseek-ai/dsh-invariants": "0.2.0-rc.2",
|
|
46
|
+
"@deepseek-ai/dsh-llm": "0.2.0-rc.2",
|
|
47
|
+
"@deepseek-ai/dsh-session-persistence": "0.2.0-rc.2",
|
|
48
|
+
"@deepseek-ai/dsh-tools": "0.2.0-rc.2"
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"@deepseek-ai/schemastery": "~3.18.4",
|
|
52
|
+
"ws": "^8.21.0"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@deepseek-ai/cordis": "~4.0.4",
|
|
56
|
+
"@deepseek-ai/dsh-agent": "0.2.0-rc.2",
|
|
57
|
+
"@deepseek-ai/dsh-api-gateway": "0.2.0-rc.2",
|
|
58
|
+
"@deepseek-ai/dsh-attachment": "0.2.0-rc.2",
|
|
59
|
+
"@deepseek-ai/dsh-client-connection": "0.2.0-rc.2",
|
|
60
|
+
"@deepseek-ai/dsh-home-paths": "0.2.0-rc.2",
|
|
61
|
+
"@deepseek-ai/dsh-host-webserver": "0.2.0-rc.2",
|
|
62
|
+
"@deepseek-ai/dsh-invariants": "0.2.0-rc.2",
|
|
63
|
+
"@deepseek-ai/dsh-llm": "0.2.0-rc.2",
|
|
64
|
+
"@deepseek-ai/dsh-ptc-runtime": "0.2.0-rc.2",
|
|
65
|
+
"@deepseek-ai/dsh-sandbox": "0.2.0-rc.2",
|
|
66
|
+
"@deepseek-ai/dsh-sandbox-policy": "0.2.0-rc.2",
|
|
67
|
+
"@deepseek-ai/dsh-scope": "0.2.0-rc.2",
|
|
68
|
+
"@deepseek-ai/dsh-session": "0.2.0-rc.2",
|
|
69
|
+
"@deepseek-ai/dsh-session-persistence": "0.2.0-rc.2",
|
|
70
|
+
"@deepseek-ai/dsh-system-prompt": "0.2.0-rc.2",
|
|
71
|
+
"@deepseek-ai/dsh-tools": "0.2.0-rc.2",
|
|
72
|
+
"@deepseek-ai/dsh-user-approval": "0.2.0-rc.2",
|
|
73
|
+
"@dsh-browser/protocol": "workspace:*",
|
|
74
|
+
"@types/node": "^22.10.0",
|
|
75
|
+
"@types/ws": "^8.18.1",
|
|
76
|
+
"playwright-core": "^1.50.0",
|
|
77
|
+
"tsdown": "0.22.2",
|
|
78
|
+
"typescript": "^5.7.2",
|
|
79
|
+
"vitest": "^3.2.0"
|
|
80
|
+
},
|
|
81
|
+
"scripts": {
|
|
82
|
+
"build": "tsc -b --pretty false && tsdown",
|
|
83
|
+
"typecheck": "tsc -b --pretty false",
|
|
84
|
+
"test": "vitest run"
|
|
85
|
+
},
|
|
86
|
+
"repository": {
|
|
87
|
+
"type": "git",
|
|
88
|
+
"url": "git+https://github.com/youbaiyun/dsh-browser-application.git",
|
|
89
|
+
"directory": "packages/bridge"
|
|
90
|
+
},
|
|
91
|
+
"homepage": "https://github.com/youbaiyun/dsh-browser-application#readme",
|
|
92
|
+
"bugs": {
|
|
93
|
+
"url": "https://github.com/youbaiyun/dsh-browser-application/issues"
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the browser-extension bridge WebSocket URL from the current page
|
|
3
|
+
* location or a discovery response. Shared by the settings row and tests.
|
|
4
|
+
* @module dsh-browser-application/src/bridge-url
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { BRIDGE_CONFIG_PATH, BRIDGE_PATH } from '@dsh-browser/protocol'
|
|
8
|
+
|
|
9
|
+
/** Minimal Location fields needed to rebuild a loopback-friendly ws URL. */
|
|
10
|
+
export interface BridgeLocationLike {
|
|
11
|
+
protocol: string
|
|
12
|
+
hostname: string
|
|
13
|
+
port: string
|
|
14
|
+
host: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Build `ws(s)://…/ext/bridge` from the page that hosts the dsh web UI.
|
|
19
|
+
* Loopback hostnames are normalized to `127.0.0.1` so the address pastes cleanly
|
|
20
|
+
* into the Chrome extension settings.
|
|
21
|
+
*/
|
|
22
|
+
export function bridgeWsUrlFromLocation(
|
|
23
|
+
location: BridgeLocationLike,
|
|
24
|
+
bridgePath: string = BRIDGE_PATH,
|
|
25
|
+
): string {
|
|
26
|
+
const wsProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:'
|
|
27
|
+
const hostname = location.hostname === 'localhost' ? '127.0.0.1' : location.hostname
|
|
28
|
+
const host = location.port === '' ? hostname : `${hostname}:${location.port}`
|
|
29
|
+
return `${wsProtocol}//${host}${bridgePath}`
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Prefer the discovery endpoint; fall back to reconstructing from `location`.
|
|
34
|
+
* @param fetchImpl - injectable fetch (defaults to global fetch).
|
|
35
|
+
* @param location - page location used for fallback and relative discovery URL.
|
|
36
|
+
*/
|
|
37
|
+
export async function resolveBridgeWsUrl(
|
|
38
|
+
location: BridgeLocationLike & { origin?: string },
|
|
39
|
+
fetchImpl: typeof fetch = fetch,
|
|
40
|
+
bridgeConfigPath: string = BRIDGE_CONFIG_PATH,
|
|
41
|
+
): Promise<string> {
|
|
42
|
+
const fallback = bridgeWsUrlFromLocation(location)
|
|
43
|
+
try {
|
|
44
|
+
const base = location.origin ?? `${location.protocol}//${location.host}`
|
|
45
|
+
const response = await fetchImpl(`${base}${bridgeConfigPath}`, {
|
|
46
|
+
signal: AbortSignal.timeout(1_500),
|
|
47
|
+
})
|
|
48
|
+
if (!response.ok) return fallback
|
|
49
|
+
const body = await response.json() as { wsUrl?: unknown }
|
|
50
|
+
if (typeof body.wsUrl === 'string' && (body.wsUrl.startsWith('ws://') || body.wsUrl.startsWith('wss://'))) {
|
|
51
|
+
return body.wsUrl
|
|
52
|
+
}
|
|
53
|
+
} catch {
|
|
54
|
+
// Discovery is best-effort: the settings row still shows the reconstructed URL.
|
|
55
|
+
}
|
|
56
|
+
return fallback
|
|
57
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model-facing browser page context injected after an explicit tab handoff.
|
|
3
|
+
*
|
|
4
|
+
* The extension captures the page immediately after the user chooses to
|
|
5
|
+
* follow it. A live Agent receives that snapshot at once; a deferred session
|
|
6
|
+
* keeps only its newest snapshot until `agent/created` publishes the
|
|
7
|
+
* Agent. Live inboxes also keep only the newest unclaimed browser snapshot.
|
|
8
|
+
* Injection deliberately does not wake an idle Agent — the snapshot is
|
|
9
|
+
* claimed together with the user's next message.
|
|
10
|
+
*
|
|
11
|
+
* @module
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { Agent, AgentRegistry } from '@deepseek-ai/dsh-agent'
|
|
15
|
+
import { createUserMessage, type ContextFormed, type UserMessage } from '@deepseek-ai/dsh-llm'
|
|
16
|
+
|
|
17
|
+
declare module '@deepseek-ai/dsh-llm' {
|
|
18
|
+
interface MessageSourceMap {
|
|
19
|
+
/** Followed-tab browser page snapshot owned by bridge-browser. */
|
|
20
|
+
'browser-context': {
|
|
21
|
+
kind: 'browser-context'
|
|
22
|
+
} & ContextFormed
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** MessageSource.kind for snapshot supersession and transcript presentation. */
|
|
27
|
+
export const BROWSER_CONTEXT_KIND = 'browser-context' as const
|
|
28
|
+
|
|
29
|
+
/** Bound orphaned provisional sessions while retaining normal recent tabs. */
|
|
30
|
+
const DEFAULT_MAX_PENDING = 32
|
|
31
|
+
|
|
32
|
+
/** Build one immutable context message from a captured browser snapshot. */
|
|
33
|
+
export function createBrowserSnapshotMessage(snapshot: string): UserMessage {
|
|
34
|
+
const text = [
|
|
35
|
+
'The user chose to follow the newly active browser tab. The browser page context was refreshed immediately after that choice.',
|
|
36
|
+
'The following is an already completed browser_snapshot of the current page. Use its stable indices directly for the next request; do not take an immediate duplicate snapshot unless required context is missing.',
|
|
37
|
+
snapshot,
|
|
38
|
+
].join('\n\n')
|
|
39
|
+
return createUserMessage({
|
|
40
|
+
content: [{ type: 'text', text }],
|
|
41
|
+
source: {
|
|
42
|
+
kind: BROWSER_CONTEXT_KIND,
|
|
43
|
+
form: 'snapshot',
|
|
44
|
+
sections: [{ name: 'browser-page', text }],
|
|
45
|
+
},
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Supersede pending tab context through the durable Inbox command surface. */
|
|
50
|
+
function injectLatestSnapshot(agent: Agent, snapshot: string): void {
|
|
51
|
+
for (const message of agent.inbox.nextStep) {
|
|
52
|
+
if (message.source.kind === BROWSER_CONTEXT_KIND
|
|
53
|
+
&& message.source.form === 'snapshot') {
|
|
54
|
+
agent.inbox.remove(message.id)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
agent.inject(createBrowserSnapshotMessage(snapshot))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Deliver followed-page snapshots to live or not-yet-materialized Agents. */
|
|
61
|
+
export class BrowserContextInjector {
|
|
62
|
+
private readonly pending = new Map<string, string>()
|
|
63
|
+
|
|
64
|
+
constructor(
|
|
65
|
+
private readonly agents: Pick<AgentRegistry, 'get'>,
|
|
66
|
+
private readonly maxPending = DEFAULT_MAX_PENDING,
|
|
67
|
+
) {
|
|
68
|
+
if (!Number.isInteger(maxPending) || maxPending < 1) {
|
|
69
|
+
throw new Error('browser context maxPending must be a positive integer')
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Inject now when possible; otherwise retain the newest snapshot per session. */
|
|
74
|
+
inject(sessionId: string, snapshot: string): 'injected' | 'queued' {
|
|
75
|
+
const agent = this.agents.get(sessionId as Parameters<AgentRegistry['get']>[0])
|
|
76
|
+
if (agent !== undefined) {
|
|
77
|
+
this.pending.delete(sessionId)
|
|
78
|
+
injectLatestSnapshot(agent, snapshot)
|
|
79
|
+
return 'injected'
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Refresh insertion order when the same provisional session follows again.
|
|
83
|
+
this.pending.delete(sessionId)
|
|
84
|
+
while (this.pending.size >= this.maxPending) {
|
|
85
|
+
const oldest = this.pending.keys().next().value as string | undefined
|
|
86
|
+
if (oldest === undefined) break
|
|
87
|
+
this.pending.delete(oldest)
|
|
88
|
+
}
|
|
89
|
+
this.pending.set(sessionId, snapshot)
|
|
90
|
+
return 'queued'
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Flush one provisional session at the supported Agent startup boundary. */
|
|
94
|
+
activate(agent: Agent): boolean {
|
|
95
|
+
const sessionId = String(agent.id)
|
|
96
|
+
const snapshot = this.pending.get(sessionId)
|
|
97
|
+
if (snapshot === undefined) return false
|
|
98
|
+
injectLatestSnapshot(agent, snapshot)
|
|
99
|
+
this.pending.delete(sessionId)
|
|
100
|
+
return true
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural subset of the dsh Host services the bridge adapts to: the
|
|
3
|
+
* TypertGateway wire seam, the Connection fetch handler, and the version-aware
|
|
4
|
+
* wire-stream opener.
|
|
5
|
+
*
|
|
6
|
+
* @module dsh-browser-application/src/dsh-gateway
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { HostRpcFailure } from './host-api.ts'
|
|
10
|
+
|
|
11
|
+
/** Structural subset of dsh 0.2's Host TypertGateway service. */
|
|
12
|
+
export interface TypertGatewayLike {
|
|
13
|
+
readonly wireStream: {
|
|
14
|
+
/**
|
|
15
|
+
* dsh 0.2: `(endpoint, payload, uplink, peer, signal)`.
|
|
16
|
+
* Legacy stubs/tests: `(endpoint, payload, signal)`.
|
|
17
|
+
*/
|
|
18
|
+
open(
|
|
19
|
+
endpoint: string,
|
|
20
|
+
payload: unknown,
|
|
21
|
+
uplinkOrSignal: AsyncIterable<unknown> | AbortSignal,
|
|
22
|
+
peer?: unknown,
|
|
23
|
+
signal?: AbortSignal,
|
|
24
|
+
): Promise<AsyncIterable<unknown>>
|
|
25
|
+
failure(error: unknown): HostRpcFailure
|
|
26
|
+
}
|
|
27
|
+
invoke(request: {
|
|
28
|
+
readonly namespace: string
|
|
29
|
+
readonly method: string
|
|
30
|
+
readonly args: Readonly<Record<string, unknown>>
|
|
31
|
+
readonly signal?: AbortSignal
|
|
32
|
+
}): Promise<unknown>
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Empty Client→Host uplink for in-process Host wireStream.open calls.
|
|
37
|
+
* dsh 0.2 requires the uplink slot; Gateway-owned endpoints ($events) discard
|
|
38
|
+
* it immediately, and Remote streams still need a valid AsyncIterable.
|
|
39
|
+
*/
|
|
40
|
+
const EMPTY_WIRE_UPLINK: AsyncIterable<unknown> = {
|
|
41
|
+
async *[Symbol.asyncIterator]() { /* no uplink items */ },
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Open a Host wire stream against either dsh 0.2 or the legacy three-arg form.
|
|
46
|
+
*
|
|
47
|
+
* - arity 3: composition/unit stubs still use `(endpoint, payload, signal)`.
|
|
48
|
+
* - arity 5: real dsh 0.2 TypertGatewayWireStream.
|
|
49
|
+
* - arity 0: Cordis/service wrappers — must use the five-arg call. Treating
|
|
50
|
+
* these as three-arg maps AbortSignal onto uplink and leaves signal
|
|
51
|
+
* undefined (hello.ok → stream-failed → WS 1011).
|
|
52
|
+
*/
|
|
53
|
+
export function openWireStream(gateway: TypertGatewayLike, endpoint: string, payload: unknown, signal: AbortSignal): Promise<AsyncIterable<unknown>> {
|
|
54
|
+
const open = gateway.wireStream.open
|
|
55
|
+
if (open.length === 3) {
|
|
56
|
+
return open(endpoint, payload, signal)
|
|
57
|
+
}
|
|
58
|
+
return open(endpoint, payload, EMPTY_WIRE_UPLINK, undefined, signal)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Structural subset of dsh 0.2's Host Connection service. */
|
|
62
|
+
export interface HostConnectionLike {
|
|
63
|
+
createSharedFetchHandler(channel: '/api'): {
|
|
64
|
+
fetch(request: Request): Promise<Response>
|
|
65
|
+
}
|
|
66
|
+
}
|