@klarkxy/dsh-plugin-kit 0.1.0 → 0.3.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/README.md +24 -2
- package/docs/README.zh-CN.md +21 -2
- package/lib/cli-media.d.ts +70 -0
- package/lib/cli-media.js +249 -0
- package/lib/cli-media.js.map +1 -0
- package/lib/cli-process.d.ts +33 -0
- package/lib/cli-process.js +166 -0
- package/lib/cli-process.js.map +1 -0
- package/lib/client-utils.d.ts +7 -1
- package/lib/client-utils.js +25 -1
- package/lib/client-utils.js.map +1 -1
- package/lib/contracts.d.ts +67 -111
- package/lib/contracts.js +34 -2
- package/lib/contracts.js.map +1 -1
- package/lib/domain.d.ts +26 -0
- package/lib/domain.js +41 -0
- package/lib/domain.js.map +1 -0
- package/lib/host-rpc.d.ts +22 -3
- package/lib/host-rpc.js +87 -68
- package/lib/host-rpc.js.map +1 -1
- package/lib/index.d.ts +3 -2
- package/lib/index.js +2 -1
- package/lib/llm-call.d.ts +5 -1
- package/lib/llm-call.js +5 -1
- package/lib/llm-call.js.map +1 -1
- package/lib/model-menu.d.ts +3 -36
- package/lib/model-menu.js +11 -120
- package/lib/model-menu.js.map +1 -1
- package/lib/model-settings-client.d.ts +47 -0
- package/lib/model-settings-client.js +143 -0
- package/lib/model-settings-client.js.map +1 -0
- package/lib/model-settings.d.ts +55 -0
- package/lib/model-settings.js +60 -0
- package/lib/model-settings.js.map +1 -0
- package/lib/official-ui.js +1 -1
- package/lib/official-ui.js.map +1 -1
- package/package.json +57 -8
package/lib/domain.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
//#region src/domain.ts
|
|
2
|
+
/**
|
|
3
|
+
* Open table-only plugin state using the host's lossless legacy bootstrap.
|
|
4
|
+
* The JSON backend ignores compatibleVersions in single layout, and treats
|
|
5
|
+
* unsupported/malformed legacy files as absent in per-record layout. Probe
|
|
6
|
+
* an empty result so persistent settings cannot silently become defaults.
|
|
7
|
+
* A populated per-record tree is authoritative; the retained legacy source
|
|
8
|
+
* is an initial backup, not a current-data rollback after subsequent writes.
|
|
9
|
+
*/
|
|
10
|
+
async function openCompatibleDomain(open, spec) {
|
|
11
|
+
const perRecord = {
|
|
12
|
+
...spec,
|
|
13
|
+
layout: "per-record"
|
|
14
|
+
};
|
|
15
|
+
const domain = await open(perRecord);
|
|
16
|
+
let populated = false;
|
|
17
|
+
try {
|
|
18
|
+
populated = Object.keys(spec.tables).some((table) => !domain.table(table).entries().next().done);
|
|
19
|
+
} finally {
|
|
20
|
+
if (!populated) await domain.close();
|
|
21
|
+
}
|
|
22
|
+
if (populated) return domain;
|
|
23
|
+
let mismatch;
|
|
24
|
+
for (const version of /* @__PURE__ */ new Set([spec.version, ...spec.compatibleVersions ?? []])) try {
|
|
25
|
+
await (await open({
|
|
26
|
+
...spec,
|
|
27
|
+
layout: "single",
|
|
28
|
+
version,
|
|
29
|
+
compatibleVersions: []
|
|
30
|
+
})).close();
|
|
31
|
+
return open(perRecord);
|
|
32
|
+
} catch (error) {
|
|
33
|
+
if (!error || typeof error !== "object" || !("code" in error) || error.code !== "version-mismatch") throw error;
|
|
34
|
+
mismatch = error;
|
|
35
|
+
}
|
|
36
|
+
throw mismatch;
|
|
37
|
+
}
|
|
38
|
+
//#endregion
|
|
39
|
+
export { openCompatibleDomain };
|
|
40
|
+
|
|
41
|
+
//# sourceMappingURL=domain.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"domain.js","names":[],"sources":["../src/domain.ts"],"sourcesContent":["type PluginDomainSpec = {\n version: number\n compatibleVersions?: readonly number[]\n layout?: 'single' | 'per-record'\n tables: Record<string, unknown>\n global?: never\n}\n\ntype PluginDomainHandle = {\n table(name: string): { entries(): IterableIterator<[unknown, unknown]> }\n close(): Promise<void>\n}\n\n/**\n * Open table-only plugin state using the host's lossless legacy bootstrap.\n * The JSON backend ignores compatibleVersions in single layout, and treats\n * unsupported/malformed legacy files as absent in per-record layout. Probe\n * an empty result so persistent settings cannot silently become defaults.\n * A populated per-record tree is authoritative; the retained legacy source\n * is an initial backup, not a current-data rollback after subsequent writes.\n */\nexport async function openCompatibleDomain<S extends PluginDomainSpec, D extends PluginDomainHandle>(\n open: (spec: S) => Promise<D>, spec: S,\n): Promise<D> {\n const perRecord = { ...spec, layout: 'per-record' } as S\n const domain = await open(perRecord)\n let populated = false\n try {\n populated = Object.keys(spec.tables).some(table => !domain.table(table).entries().next().done)\n } finally {\n if (!populated) await domain.close()\n }\n if (populated) return domain\n\n let mismatch: unknown\n for (const version of new Set([spec.version, ...spec.compatibleVersions ?? []])) {\n try {\n const legacy = await open({ ...spec, layout: 'single', version, compatibleVersions: [] } as S)\n await legacy.close()\n return open(perRecord)\n } catch (error) {\n if (!error || typeof error !== 'object' || !('code' in error) || error.code !== 'version-mismatch') throw error\n mismatch = error\n }\n }\n throw mismatch\n}\n"],"mappings":";;;;;;;;;AAqBA,eAAsB,qBACpB,MAA+B,MACnB;CACZ,MAAM,YAAY;EAAE,GAAG;EAAM,QAAQ;CAAa;CAClD,MAAM,SAAS,MAAM,KAAK,SAAS;CACnC,IAAI,YAAY;CAChB,IAAI;EACF,YAAY,OAAO,KAAK,KAAK,MAAM,CAAC,CAAC,MAAK,UAAS,CAAC,OAAO,MAAM,KAAK,CAAC,CAAC,QAAQ,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI;CAC/F,UAAU;EACR,IAAI,CAAC,WAAW,MAAM,OAAO,MAAM;CACrC;CACA,IAAI,WAAW,OAAO;CAEtB,IAAI;CACJ,KAAK,MAAM,2BAAW,IAAI,IAAI,CAAC,KAAK,SAAS,GAAG,KAAK,sBAAsB,CAAC,CAAC,CAAC,GAC5E,IAAI;EAEF,OAAM,MADe,KAAK;GAAE,GAAG;GAAM,QAAQ;GAAU;GAAS,oBAAoB,CAAC;EAAE,CAAM,EAAA,CAChF,MAAM;EACnB,OAAO,KAAK,SAAS;CACvB,SAAS,OAAO;EACd,IAAI,CAAC,SAAS,OAAO,UAAU,YAAY,EAAE,UAAU,UAAU,MAAM,SAAS,oBAAoB,MAAM;EAC1G,WAAW;CACb;CAEF,MAAM;AACR"}
|
package/lib/host-rpc.d.ts
CHANGED
|
@@ -13,8 +13,27 @@ interface HostRpcContext {
|
|
|
13
13
|
requestRejection?: (request: IncomingMessage) => number | undefined;
|
|
14
14
|
};
|
|
15
15
|
}
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
type Rejection = {
|
|
17
|
+
status: number;
|
|
18
|
+
body?: string;
|
|
19
|
+
};
|
|
20
|
+
interface HostRpcTransportOptions {
|
|
21
|
+
maxBodyBytes?: number;
|
|
22
|
+
isEndpoint?: (endpoint: string) => boolean;
|
|
23
|
+
caseInsensitiveContentType?: boolean;
|
|
24
|
+
emptyBodyAsNull?: boolean;
|
|
25
|
+
beforeBody?: (request: IncomingMessage, endpoint: string) => Rejection | undefined;
|
|
26
|
+
errorBody?: (status: number, error?: unknown) => string | undefined;
|
|
27
|
+
}
|
|
28
|
+
interface HostRpcOptions extends HostRpcTransportOptions {
|
|
29
|
+
requirePayload?: boolean;
|
|
30
|
+
}
|
|
31
|
+
/** The shared authenticated HTTP boundary. Each plugin still owns its RPC envelope semantics. */
|
|
32
|
+
declare function registerHostRpcTransport(ctx: HostRpcContext, channel: string, dispatch: (endpoint: string, body: unknown, signal: AbortSignal, response: ServerResponse) => void | Promise<void>, options?: HostRpcTransportOptions): () => void;
|
|
33
|
+
/** Write the native Connection response envelope after a plugin has handled a request. */
|
|
34
|
+
declare function sendHostRpcResponse(response: ServerResponse, rpcId: string, result: unknown, noStore?: boolean): void;
|
|
35
|
+
/** Safe host RPC registration. Missing requestRejection fails closed. */
|
|
36
|
+
declare function registerHostRpc<R extends RpcResult = RpcResult>(ctx: HostRpcContext, channel: string, call: (endpoint: string, payload: unknown, signal: AbortSignal) => Promise<R>, options?: HostRpcOptions): () => void;
|
|
18
37
|
//#endregion
|
|
19
|
-
export { HostRpcContext, registerHostRpc };
|
|
38
|
+
export { HostRpcContext, HostRpcOptions, HostRpcTransportOptions, registerHostRpc, registerHostRpcTransport, sendHostRpcResponse };
|
|
20
39
|
//# sourceMappingURL=host-rpc.d.ts.map
|
package/lib/host-rpc.js
CHANGED
|
@@ -1,95 +1,114 @@
|
|
|
1
1
|
//#region src/host-rpc.ts
|
|
2
|
-
/**
|
|
3
|
-
function
|
|
2
|
+
/** The shared authenticated HTTP boundary. Each plugin still owns its RPC envelope semantics. */
|
|
3
|
+
function registerHostRpcTransport(ctx, channel, dispatch, options = {}) {
|
|
4
|
+
const bodyLimit = options.maxBodyBytes ?? 65536;
|
|
5
|
+
if (!Number.isSafeInteger(bodyLimit) || bodyLimit <= 0) throw new Error("Invalid RPC body limit");
|
|
6
|
+
const isEndpoint = options.isEndpoint ?? ((endpoint) => /^[a-z.]+$/.test(endpoint));
|
|
4
7
|
return ctx.webServer.register({
|
|
5
8
|
kind: "prefix",
|
|
6
9
|
path: channel,
|
|
7
10
|
handler: async (req, res) => {
|
|
8
|
-
|
|
9
|
-
res.
|
|
10
|
-
res.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
if (rejection !== void 0) {
|
|
15
|
-
res.writeHead(rejection);
|
|
16
|
-
res.end();
|
|
17
|
-
return;
|
|
18
|
-
}
|
|
19
|
-
const pathname = new URL(req.url ?? "/", "http://dsh.internal").pathname;
|
|
20
|
-
const endpoint = pathname.startsWith(`${channel}/`) ? pathname.slice(channel.length + 1) : "";
|
|
21
|
-
if (req.method !== "POST" || !/^[a-z.]+$/.test(endpoint)) {
|
|
22
|
-
res.writeHead(404);
|
|
23
|
-
res.end();
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
if (String(req.headers["content-type"] ?? "").split(";")[0]?.trim() !== "application/json") {
|
|
27
|
-
res.writeHead(415);
|
|
28
|
-
res.end();
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
const controller = new AbortController();
|
|
32
|
-
res.on("close", () => {
|
|
33
|
-
if (!res.writableEnded) controller.abort();
|
|
34
|
-
});
|
|
11
|
+
const reject = (status, error, override) => {
|
|
12
|
+
if (res.destroyed) return;
|
|
13
|
+
if (status === 413) res.writeHead(status, { connection: "close" });
|
|
14
|
+
else res.writeHead(status);
|
|
15
|
+
res.end(override ?? options.errorBody?.(status, error) ?? (status === 500 ? "rpc request failed" : ""));
|
|
16
|
+
};
|
|
35
17
|
try {
|
|
18
|
+
if (typeof ctx.connection.requestRejection !== "function") {
|
|
19
|
+
reject(503);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const rejection = ctx.connection.requestRejection(req);
|
|
23
|
+
if (rejection !== void 0) {
|
|
24
|
+
reject(rejection);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const pathname = new URL(req.url ?? "/", "http://dsh.internal").pathname;
|
|
28
|
+
const endpoint = pathname.startsWith(`${channel}/`) ? pathname.slice(channel.length + 1) : "";
|
|
29
|
+
if (req.method !== "POST" || !isEndpoint(endpoint)) {
|
|
30
|
+
reject(404);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const contentType = String(req.headers["content-type"] ?? "").split(";", 1)[0]?.trim();
|
|
34
|
+
if ((options.caseInsensitiveContentType ? contentType.toLowerCase() : contentType) !== "application/json") {
|
|
35
|
+
reject(415);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const guarded = options.beforeBody?.(req, endpoint);
|
|
39
|
+
if (guarded) {
|
|
40
|
+
reject(guarded.status, void 0, guarded.body);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const controller = new AbortController();
|
|
44
|
+
res.on("close", () => {
|
|
45
|
+
if (!res.writableEnded) controller.abort();
|
|
46
|
+
});
|
|
36
47
|
const chunks = [];
|
|
37
48
|
let size = 0;
|
|
38
49
|
for await (const chunk of req) {
|
|
39
50
|
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
40
51
|
size += buffer.length;
|
|
41
|
-
if (size >
|
|
42
|
-
|
|
43
|
-
res.end();
|
|
52
|
+
if (size > bodyLimit) {
|
|
53
|
+
reject(413);
|
|
44
54
|
req.destroy();
|
|
45
55
|
return;
|
|
46
56
|
}
|
|
47
57
|
chunks.push(buffer);
|
|
48
58
|
}
|
|
49
|
-
let
|
|
59
|
+
let body;
|
|
50
60
|
try {
|
|
51
|
-
|
|
52
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error();
|
|
53
|
-
request = parsed;
|
|
61
|
+
body = JSON.parse(Buffer.concat(chunks).toString("utf8") || (options.emptyBodyAsNull ? "null" : ""));
|
|
54
62
|
} catch {
|
|
55
|
-
|
|
56
|
-
res.end();
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
if (request.type !== "client-request" || request.method !== endpoint || typeof request.rpcId !== "string") {
|
|
60
|
-
res.writeHead(400);
|
|
61
|
-
res.end();
|
|
63
|
+
reject(400);
|
|
62
64
|
return;
|
|
63
65
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
error: {
|
|
68
|
-
details: {},
|
|
69
|
-
...raw.error
|
|
70
|
-
}
|
|
71
|
-
};
|
|
72
|
-
if (!res.destroyed) {
|
|
73
|
-
res.writeHead(200, {
|
|
74
|
-
"content-type": "application/json",
|
|
75
|
-
"cache-control": "no-store"
|
|
76
|
-
});
|
|
77
|
-
res.end(JSON.stringify({
|
|
78
|
-
type: "server-response",
|
|
79
|
-
rpcId: request.rpcId,
|
|
80
|
-
result
|
|
81
|
-
}));
|
|
82
|
-
}
|
|
83
|
-
} catch {
|
|
84
|
-
if (!res.destroyed) {
|
|
85
|
-
res.writeHead(500);
|
|
86
|
-
res.end("rpc request failed");
|
|
87
|
-
}
|
|
66
|
+
await dispatch(endpoint, body, controller.signal, res);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
reject(500, error);
|
|
88
69
|
}
|
|
89
70
|
}
|
|
90
71
|
});
|
|
91
72
|
}
|
|
73
|
+
/** Write the native Connection response envelope after a plugin has handled a request. */
|
|
74
|
+
function sendHostRpcResponse(response, rpcId, result, noStore = true) {
|
|
75
|
+
if (response.destroyed) return;
|
|
76
|
+
response.writeHead(200, {
|
|
77
|
+
"content-type": "application/json",
|
|
78
|
+
...noStore ? { "cache-control": "no-store" } : {}
|
|
79
|
+
});
|
|
80
|
+
response.end(JSON.stringify({
|
|
81
|
+
type: "server-response",
|
|
82
|
+
rpcId,
|
|
83
|
+
result
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
/** Safe host RPC registration. Missing requestRejection fails closed. */
|
|
87
|
+
function registerHostRpc(ctx, channel, call, options = {}) {
|
|
88
|
+
return registerHostRpcTransport(ctx, channel, async (endpoint, body, signal, res) => {
|
|
89
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
90
|
+
res.writeHead(400);
|
|
91
|
+
res.end();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const request = body;
|
|
95
|
+
if (request.type !== "client-request" || request.method !== endpoint || typeof request.rpcId !== "string" || options.requirePayload && !Object.hasOwn(request, "payload")) {
|
|
96
|
+
res.writeHead(400);
|
|
97
|
+
res.end();
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const raw = await call(endpoint, request.payload, signal);
|
|
101
|
+
const result = raw.ok ? raw : {
|
|
102
|
+
...raw,
|
|
103
|
+
error: {
|
|
104
|
+
details: {},
|
|
105
|
+
...raw.error
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
sendHostRpcResponse(res, request.rpcId, result);
|
|
109
|
+
}, options);
|
|
110
|
+
}
|
|
92
111
|
//#endregion
|
|
93
|
-
export { registerHostRpc };
|
|
112
|
+
export { registerHostRpc, registerHostRpcTransport, sendHostRpcResponse };
|
|
94
113
|
|
|
95
114
|
//# sourceMappingURL=host-rpc.js.map
|
package/lib/host-rpc.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"host-rpc.js","names":[],"sources":["../src/host-rpc.ts"],"sourcesContent":["import type { IncomingMessage, ServerResponse } from 'node:http'\nimport type { RpcResult } from './contracts.ts'\n\nexport interface HostRpcContext {\n webServer: { register(route: { kind: 'prefix'; path: string; handler(req: IncomingMessage, res: ServerResponse): void | Promise<void> }): () => void }\n connection: { requestRejection?: (request: IncomingMessage) => number | undefined }\n}\n\n
|
|
1
|
+
{"version":3,"file":"host-rpc.js","names":[],"sources":["../src/host-rpc.ts"],"sourcesContent":["import type { IncomingMessage, ServerResponse } from 'node:http'\nimport type { RpcResult } from './contracts.ts'\n\nexport interface HostRpcContext {\n webServer: { register(route: { kind: 'prefix'; path: string; handler(req: IncomingMessage, res: ServerResponse): void | Promise<void> }): () => void }\n connection: { requestRejection?: (request: IncomingMessage) => number | undefined }\n}\n\ntype Rejection = { status: number; body?: string }\n\nexport interface HostRpcTransportOptions {\n maxBodyBytes?: number\n isEndpoint?: (endpoint: string) => boolean\n caseInsensitiveContentType?: boolean\n emptyBodyAsNull?: boolean\n beforeBody?: (request: IncomingMessage, endpoint: string) => Rejection | undefined\n errorBody?: (status: number, error?: unknown) => string | undefined\n}\n\nexport interface HostRpcOptions extends HostRpcTransportOptions {\n requirePayload?: boolean\n}\n\n/** The shared authenticated HTTP boundary. Each plugin still owns its RPC envelope semantics. */\nexport function registerHostRpcTransport(\n ctx: HostRpcContext,\n channel: string,\n dispatch: (endpoint: string, body: unknown, signal: AbortSignal, response: ServerResponse) => void | Promise<void>,\n options: HostRpcTransportOptions = {},\n): () => void {\n const bodyLimit = options.maxBodyBytes ?? 64 * 1024\n if (!Number.isSafeInteger(bodyLimit) || bodyLimit <= 0) throw new Error('Invalid RPC body limit')\n const isEndpoint = options.isEndpoint ?? ((endpoint: string) => /^[a-z.]+$/.test(endpoint))\n return ctx.webServer.register({ kind: 'prefix', path: channel, handler: async (req, res) => {\n const reject = (status: number, error?: unknown, override?: string) => {\n if (res.destroyed) return\n if (status === 413) res.writeHead(status, { connection: 'close' })\n else res.writeHead(status)\n res.end(override ?? options.errorBody?.(status, error) ?? (status === 500 ? 'rpc request failed' : ''))\n }\n try {\n // Host admission always runs before plugin-specific checks or body reads.\n if (typeof ctx.connection.requestRejection !== 'function') { reject(503); return }\n const rejection = ctx.connection.requestRejection(req)\n if (rejection !== undefined) { reject(rejection); return }\n const pathname = new URL(req.url ?? '/', 'http://dsh.internal').pathname\n const endpoint = pathname.startsWith(`${channel}/`) ? pathname.slice(channel.length + 1) : ''\n if (req.method !== 'POST' || !isEndpoint(endpoint)) { reject(404); return }\n const contentType = String(req.headers['content-type'] ?? '').split(';', 1)[0]?.trim()\n if ((options.caseInsensitiveContentType ? contentType.toLowerCase() : contentType) !== 'application/json') {\n reject(415); return\n }\n const guarded = options.beforeBody?.(req, endpoint)\n if (guarded) { reject(guarded.status, undefined, guarded.body); return }\n\n const controller = new AbortController()\n res.on('close', () => { if (!res.writableEnded) controller.abort() })\n const chunks: Buffer[] = []\n let size = 0\n for await (const chunk of req) {\n const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)\n size += buffer.length\n if (size > bodyLimit) { reject(413); req.destroy(); return }\n chunks.push(buffer)\n }\n let body: unknown\n try { body = JSON.parse(Buffer.concat(chunks).toString('utf8') || (options.emptyBodyAsNull ? 'null' : '')) }\n catch { reject(400); return }\n await dispatch(endpoint, body, controller.signal, res)\n } catch (error) {\n reject(500, error)\n }\n } })\n}\n\n/** Write the native Connection response envelope after a plugin has handled a request. */\nexport function sendHostRpcResponse(response: ServerResponse, rpcId: string, result: unknown, noStore = true): void {\n if (response.destroyed) return\n response.writeHead(200, { 'content-type': 'application/json', ...(noStore ? { 'cache-control': 'no-store' } : {}) })\n response.end(JSON.stringify({ type: 'server-response', rpcId, result }))\n}\n\n/** Safe host RPC registration. Missing requestRejection fails closed. */\nexport function registerHostRpc<R extends RpcResult = RpcResult>(\n ctx: HostRpcContext,\n channel: string,\n call: (endpoint: string, payload: unknown, signal: AbortSignal) => Promise<R>,\n options: HostRpcOptions = {},\n): () => void {\n return registerHostRpcTransport(ctx, channel, async (endpoint, body, signal, res) => {\n if (!body || typeof body !== 'object' || Array.isArray(body)) { res.writeHead(400); res.end(); return }\n const request = body as Record<string, unknown>\n if (request.type !== 'client-request' || request.method !== endpoint || typeof request.rpcId !== 'string'\n || (options.requirePayload && !Object.hasOwn(request, 'payload'))) {\n res.writeHead(400); res.end(); return\n }\n const raw = await call(endpoint, request.payload, signal)\n const result = raw.ok ? raw : { ...raw, error: { details: {}, ...raw.error } }\n sendHostRpcResponse(res, request.rpcId, result)\n }, options)\n}\n"],"mappings":"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"}
|
package/lib/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { CHAT_EVENTS_SLOT, CREATOR_PRESET_ID, ContextKnowledge, DshWebManagementDescriptor, EvidenceRef, KnowledgeKind, KnowledgeScope, MODEL_SETTINGS_CHANGES_REMOTE, MemoryMutationOptions, MemoryQuery, MemoryRecord, MemoryService, ModelRoute, NewMemoryRecord, ProcedureKnowledge, ProducerMessageSource, ProducerSourceKind, RpcResult, SubagentSessionLike, TaskCheckpoint, TaskContract, WEB_PROVIDER_AVAILABILITY_EVENT, isSubagentSession, producerMessageSource, projectIdFromCwd } from "./contracts.js";
|
|
2
|
+
import { openCompatibleDomain } from "./domain.js";
|
|
2
3
|
import { HostRpcContext, registerHostRpc } from "./host-rpc.js";
|
|
3
4
|
import { FeatureModelRoute, LlmTextCaller, callLlmText, resolveFeatureModel } from "./llm-call.js";
|
|
4
|
-
export {
|
|
5
|
+
export { CHAT_EVENTS_SLOT, type CREATOR_PRESET_ID, type ContextKnowledge, type DshWebManagementDescriptor, type EvidenceRef, type FeatureModelRoute, type HostRpcContext, type KnowledgeKind, type KnowledgeScope, type LlmTextCaller, type MODEL_SETTINGS_CHANGES_REMOTE, type MemoryMutationOptions, type MemoryQuery, type MemoryRecord, type MemoryService, type ModelRoute, type NewMemoryRecord, type ProcedureKnowledge, type ProducerMessageSource, type ProducerSourceKind, type RpcResult, type SubagentSessionLike, type TaskCheckpoint, type TaskContract, type WEB_PROVIDER_AVAILABILITY_EVENT, callLlmText, type isSubagentSession, openCompatibleDomain, producerMessageSource, projectIdFromCwd, registerHostRpc, resolveFeatureModel };
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CHAT_EVENTS_SLOT, producerMessageSource, projectIdFromCwd } from "./contracts.js";
|
|
2
2
|
import { registerHostRpc } from "./host-rpc.js";
|
|
3
|
+
import { openCompatibleDomain } from "./domain.js";
|
|
3
4
|
import { callLlmText, resolveFeatureModel } from "./llm-call.js";
|
|
4
|
-
export { CHAT_EVENTS_SLOT, callLlmText, producerMessageSource, projectIdFromCwd, registerHostRpc, resolveFeatureModel };
|
|
5
|
+
export { CHAT_EVENTS_SLOT, callLlmText, openCompatibleDomain, producerMessageSource, projectIdFromCwd, registerHostRpc, resolveFeatureModel };
|
package/lib/llm-call.d.ts
CHANGED
|
@@ -27,7 +27,11 @@ interface LlmTextResult {
|
|
|
27
27
|
inputTokens?: number;
|
|
28
28
|
outputTokens?: number;
|
|
29
29
|
}
|
|
30
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* Plugin selection first; otherwise the live session model, then the host default.
|
|
32
|
+
* Cordis callers must declare agents, sessionProjections and agentDefaultModel
|
|
33
|
+
* in their inject list so the fallback reads use the caller's live services.
|
|
34
|
+
*/
|
|
31
35
|
declare function resolveFeatureModel(host: unknown, saved: FeatureModelRoute | undefined, sessionId?: string): FeatureModelRoute | undefined;
|
|
32
36
|
/** One native model call. No retry, timeout policy, or provider queue. */
|
|
33
37
|
declare function callLlmText(llm: LlmTextCaller, request: LlmTextRequest): Promise<LlmTextResult>;
|
package/lib/llm-call.js
CHANGED
|
@@ -14,7 +14,11 @@ function routeOf(value) {
|
|
|
14
14
|
model: value.model.trim()
|
|
15
15
|
};
|
|
16
16
|
}
|
|
17
|
-
/**
|
|
17
|
+
/**
|
|
18
|
+
* Plugin selection first; otherwise the live session model, then the host default.
|
|
19
|
+
* Cordis callers must declare agents, sessionProjections and agentDefaultModel
|
|
20
|
+
* in their inject list so the fallback reads use the caller's live services.
|
|
21
|
+
*/
|
|
18
22
|
function resolveFeatureModel(host, saved, sessionId) {
|
|
19
23
|
const explicit = routeOf(saved);
|
|
20
24
|
if (explicit) return explicit;
|
package/lib/llm-call.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"llm-call.js","names":[],"sources":["../src/llm-call.ts"],"sourcesContent":["import {\n BlockAssembler, ReasoningEffortId, createUserMessage,\n type GenerateOptions, type LlmRuntime, type StreamChunk,\n} from '@deepseek-ai/dsh-llm'\nimport { producerMessageSource, type ModelRoute } from './contracts.ts'\n\n/** A feature's own saved model. Empty means the host default chat model. */\nexport interface FeatureModelRoute {\n readonly provider: string\n readonly model: string\n readonly reasoningEffort?: string\n}\n\nexport type LlmTextCaller = Pick<LlmRuntime, 'prepareCall' | 'resolveCallConfig'>\n\nexport interface LlmTextRequest {\n plugin: string\n route: FeatureModelRoute\n system: string\n text: string\n maxTokens: number\n signal: AbortSignal\n sessionId?: string\n /** Auxiliary-call marker adapters may special-case (e.g. disable thinking for titles). */\n purpose?: GenerateOptions['purpose']\n /** Rechecked before the call returns text. A false result discards the output. */\n isCurrent?: () => boolean\n}\n\nexport interface LlmTextResult {\n text: string\n provider: string\n model: string\n inputTokens?: number\n outputTokens?: number\n}\n\ntype SessionModelsHost = {\n agents?: { get(sessionId: string): { session?: { requestHeader?: () => { config?: ModelRoute; adapterDefaults?: { reasoningEffort?: unknown } } | undefined } } | undefined }\n sessionProjections?: { stateOf(session: unknown, key: string): { pending?: ModelRoute | null } | undefined }\n agentDefaultModel?: { currentSelection(): ModelRoute | undefined }\n}\n\nfunction routeOf(value: { provider?: unknown; model?: unknown; reasoningEffort?: unknown } | undefined): FeatureModelRoute | undefined {\n if (typeof value?.provider !== 'string' || !value.provider.trim()) return undefined\n if (typeof value.model !== 'string' || !value.model.trim()) return undefined\n const effort = typeof value.reasoningEffort === 'string' ? value.reasoningEffort.trim() : ''\n return effort\n ? { provider: value.provider.trim(), model: value.model.trim(), reasoningEffort: effort }\n : { provider: value.provider.trim(), model: value.model.trim() }\n}\n\n/** Plugin selection first; otherwise the live session model, then the host default. */\nexport function resolveFeatureModel(host: unknown, saved: FeatureModelRoute | undefined, sessionId?: string): FeatureModelRoute | undefined {\n const explicit = routeOf(saved)\n if (explicit) return explicit\n const face = host as SessionModelsHost | undefined\n if (sessionId && face?.agents && face.sessionProjections) {\n const session = face.agents.get(sessionId)?.session\n if (session) {\n try {\n const pending = face.sessionProjections.stateOf(session, 'modelSelection')?.pending\n const selected = routeOf(pending ?? undefined)\n if (selected) return selected\n } catch { /* fall through to the recorded route */ }\n const header = session.requestHeader?.()\n const recorded = header?.config\n if (recorded) {\n const selected = header?.adapterDefaults?.reasoningEffort === true\n ? { provider: recorded.provider, model: recorded.model }\n : recorded\n const route = routeOf(selected)\n if (route) return route\n }\n }\n }\n return routeOf(face?.agentDefaultModel?.currentSelection())\n}\n\nfunction joinText(assembler: BlockAssembler): string {\n return assembler.blocks()\n .filter((block): block is { type: 'text'; text: string } => block.type === 'text')\n .map(block => block.text)\n .join('')\n}\n\nasync function readStream(stream: AsyncIterable<StreamChunk>, signal: AbortSignal, live: () => boolean): Promise<BlockAssembler> {\n const assembler = new BlockAssembler()\n const iterator = stream[Symbol.asyncIterator]()\n try {\n while (live()) {\n if (signal.aborted) break\n const item = await iterator.next()\n if (item.done) break\n assembler.push(item.value)\n }\n } finally {\n try { const closing = iterator.return?.(); if (closing) void closing.catch(() => {}) } catch { /* producer ignored abort */ }\n }\n return assembler\n}\n\n/** One native model call. No retry, timeout policy, or provider queue. */\nexport async function callLlmText(llm: LlmTextCaller, request: LlmTextRequest): Promise<LlmTextResult> {\n request.signal.throwIfAborted()\n const live = () => request.isCurrent?.() !== false && !request.signal.aborted\n if (!live()) throw Object.assign(new Error('调用已取消。'), { name: 'AbortError' })\n const effort = request.route.reasoningEffort?.trim()\n const prepared = await llm.prepareCall({\n provider: request.route.provider,\n model: request.route.model,\n ...(effort ? { reasoningEffort: ReasoningEffortId(effort) } : {}),\n maxTokens: request.maxTokens,\n }, request.signal)\n if (!live()) throw Object.assign(new Error('调用已取消。'), { name: 'AbortError' })\n const options: GenerateOptions = {\n provider: prepared.config.provider,\n model: prepared.config.model,\n ...(prepared.config.reasoningEffort ? { reasoningEffort: prepared.config.reasoningEffort } : {}),\n ...(prepared.config.maxTokens !== undefined ? { maxTokens: Math.min(request.maxTokens, prepared.config.maxTokens) } : { maxTokens: request.maxTokens }),\n ...(prepared.config.temperature !== undefined ? { temperature: prepared.config.temperature } : {}),\n ...(prepared.config.stop ? { stop: [...prepared.config.stop] } : {}),\n system: request.system,\n messages: [createUserMessage({\n source: producerMessageSource(request.plugin),\n content: [{ type: 'text', text: request.text }],\n })],\n signal: request.signal,\n ...(request.purpose ? { purpose: request.purpose } : {}),\n ...(request.sessionId ? { sessionId: request.sessionId as GenerateOptions['sessionId'] } : {}),\n }\n const assembler = await readStream(prepared.stream(options), request.signal, live)\n if (request.signal.aborted || assembler.finish.kind === 'aborted') {\n throw Object.assign(new Error('调用已取消。'), { name: 'AbortError' })\n }\n if (!live()) throw Object.assign(new Error('调用已取消。'), { name: 'AbortError' })\n if (assembler.finish.kind === 'error') {\n const code = assembler.finish.failure.code\n throw Object.assign(new Error(assembler.finish.failure.message || '模型调用失败。'), { code })\n }\n if (assembler.blocks().some(block => block.type === 'tool-call') || assembler.finish.kind === 'tool-calls') {\n throw new Error('这次调用不使用工具。')\n }\n if (assembler.finish.kind !== 'stop') throw new Error('模型调用未完成。')\n return {\n text: joinText(assembler),\n provider: prepared.config.provider,\n model: prepared.config.model,\n ...(assembler.usage ? { inputTokens: assembler.usage.inputTokens, outputTokens: assembler.usage.outputTokens } : {}),\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"llm-call.js","names":[],"sources":["../src/llm-call.ts"],"sourcesContent":["import {\n BlockAssembler, ReasoningEffortId, createUserMessage,\n type GenerateOptions, type LlmRuntime, type StreamChunk,\n} from '@deepseek-ai/dsh-llm'\nimport { producerMessageSource, type ModelRoute } from './contracts.ts'\n\n/** A feature's own saved model. Empty means the host default chat model. */\nexport interface FeatureModelRoute {\n readonly provider: string\n readonly model: string\n readonly reasoningEffort?: string\n}\n\nexport type LlmTextCaller = Pick<LlmRuntime, 'prepareCall' | 'resolveCallConfig'>\n\nexport interface LlmTextRequest {\n plugin: string\n route: FeatureModelRoute\n system: string\n text: string\n maxTokens: number\n signal: AbortSignal\n sessionId?: string\n /** Auxiliary-call marker adapters may special-case (e.g. disable thinking for titles). */\n purpose?: GenerateOptions['purpose']\n /** Rechecked before the call returns text. A false result discards the output. */\n isCurrent?: () => boolean\n}\n\nexport interface LlmTextResult {\n text: string\n provider: string\n model: string\n inputTokens?: number\n outputTokens?: number\n}\n\ntype SessionModelsHost = {\n agents?: { get(sessionId: string): { session?: { requestHeader?: () => { config?: ModelRoute; adapterDefaults?: { reasoningEffort?: unknown } } | undefined } } | undefined }\n sessionProjections?: { stateOf(session: unknown, key: string): { pending?: ModelRoute | null } | undefined }\n agentDefaultModel?: { currentSelection(): ModelRoute | undefined }\n}\n\nfunction routeOf(value: { provider?: unknown; model?: unknown; reasoningEffort?: unknown } | undefined): FeatureModelRoute | undefined {\n if (typeof value?.provider !== 'string' || !value.provider.trim()) return undefined\n if (typeof value.model !== 'string' || !value.model.trim()) return undefined\n const effort = typeof value.reasoningEffort === 'string' ? value.reasoningEffort.trim() : ''\n return effort\n ? { provider: value.provider.trim(), model: value.model.trim(), reasoningEffort: effort }\n : { provider: value.provider.trim(), model: value.model.trim() }\n}\n\n/**\n * Plugin selection first; otherwise the live session model, then the host default.\n * Cordis callers must declare agents, sessionProjections and agentDefaultModel\n * in their inject list so the fallback reads use the caller's live services.\n */\nexport function resolveFeatureModel(host: unknown, saved: FeatureModelRoute | undefined, sessionId?: string): FeatureModelRoute | undefined {\n const explicit = routeOf(saved)\n if (explicit) return explicit\n const face = host as SessionModelsHost | undefined\n if (sessionId && face?.agents && face.sessionProjections) {\n const session = face.agents.get(sessionId)?.session\n if (session) {\n try {\n const pending = face.sessionProjections.stateOf(session, 'modelSelection')?.pending\n const selected = routeOf(pending ?? undefined)\n if (selected) return selected\n } catch { /* fall through to the recorded route */ }\n const header = session.requestHeader?.()\n const recorded = header?.config\n if (recorded) {\n const selected = header?.adapterDefaults?.reasoningEffort === true\n ? { provider: recorded.provider, model: recorded.model }\n : recorded\n const route = routeOf(selected)\n if (route) return route\n }\n }\n }\n return routeOf(face?.agentDefaultModel?.currentSelection())\n}\n\nfunction joinText(assembler: BlockAssembler): string {\n return assembler.blocks()\n .filter((block): block is { type: 'text'; text: string } => block.type === 'text')\n .map(block => block.text)\n .join('')\n}\n\nasync function readStream(stream: AsyncIterable<StreamChunk>, signal: AbortSignal, live: () => boolean): Promise<BlockAssembler> {\n const assembler = new BlockAssembler()\n const iterator = stream[Symbol.asyncIterator]()\n try {\n while (live()) {\n if (signal.aborted) break\n const item = await iterator.next()\n if (item.done) break\n assembler.push(item.value)\n }\n } finally {\n try { const closing = iterator.return?.(); if (closing) void closing.catch(() => {}) } catch { /* producer ignored abort */ }\n }\n return assembler\n}\n\n/** One native model call. No retry, timeout policy, or provider queue. */\nexport async function callLlmText(llm: LlmTextCaller, request: LlmTextRequest): Promise<LlmTextResult> {\n request.signal.throwIfAborted()\n const live = () => request.isCurrent?.() !== false && !request.signal.aborted\n if (!live()) throw Object.assign(new Error('调用已取消。'), { name: 'AbortError' })\n const effort = request.route.reasoningEffort?.trim()\n const prepared = await llm.prepareCall({\n provider: request.route.provider,\n model: request.route.model,\n ...(effort ? { reasoningEffort: ReasoningEffortId(effort) } : {}),\n maxTokens: request.maxTokens,\n }, request.signal)\n if (!live()) throw Object.assign(new Error('调用已取消。'), { name: 'AbortError' })\n const options: GenerateOptions = {\n provider: prepared.config.provider,\n model: prepared.config.model,\n ...(prepared.config.reasoningEffort ? { reasoningEffort: prepared.config.reasoningEffort } : {}),\n ...(prepared.config.maxTokens !== undefined ? { maxTokens: Math.min(request.maxTokens, prepared.config.maxTokens) } : { maxTokens: request.maxTokens }),\n ...(prepared.config.temperature !== undefined ? { temperature: prepared.config.temperature } : {}),\n ...(prepared.config.stop ? { stop: [...prepared.config.stop] } : {}),\n system: request.system,\n messages: [createUserMessage({\n source: producerMessageSource(request.plugin),\n content: [{ type: 'text', text: request.text }],\n })],\n signal: request.signal,\n ...(request.purpose ? { purpose: request.purpose } : {}),\n ...(request.sessionId ? { sessionId: request.sessionId as GenerateOptions['sessionId'] } : {}),\n }\n const assembler = await readStream(prepared.stream(options), request.signal, live)\n if (request.signal.aborted || assembler.finish.kind === 'aborted') {\n throw Object.assign(new Error('调用已取消。'), { name: 'AbortError' })\n }\n if (!live()) throw Object.assign(new Error('调用已取消。'), { name: 'AbortError' })\n if (assembler.finish.kind === 'error') {\n const code = assembler.finish.failure.code\n throw Object.assign(new Error(assembler.finish.failure.message || '模型调用失败。'), { code })\n }\n if (assembler.blocks().some(block => block.type === 'tool-call') || assembler.finish.kind === 'tool-calls') {\n throw new Error('这次调用不使用工具。')\n }\n if (assembler.finish.kind !== 'stop') throw new Error('模型调用未完成。')\n return {\n text: joinText(assembler),\n provider: prepared.config.provider,\n model: prepared.config.model,\n ...(assembler.usage ? { inputTokens: assembler.usage.inputTokens, outputTokens: assembler.usage.outputTokens } : {}),\n }\n}\n"],"mappings":"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"}
|
package/lib/model-menu.d.ts
CHANGED
|
@@ -1,42 +1,9 @@
|
|
|
1
|
-
import { ModelRoute } from "
|
|
1
|
+
import { ModelCatalogClient, ModelCatalogClient as ModelCatalogClient$1, ModelMenuChoice, ModelMenuChoice as ModelMenuChoice$1, ModelMenuGroup, ModelRoute, groupModelMenuChoices, knownModelFromChoices, modelMenuEffortLabel, modelMenuEffortOptions, modelMenuShortName, modelRouteKey as modelMenuChoiceKey, modelRouteOverride as modelMenuOverride, normalizeModelRoute as normalizeModelMenuRoute, parseModelMenuChoices, parseModelRouteKey as parseModelMenuChoiceKey } from "@klarkxy/dsh-model-route";
|
|
2
2
|
//#region src/model-menu.d.ts
|
|
3
|
-
/** One selectable model. `efforts` are the advertised reasoning efforts. */
|
|
4
|
-
interface ModelMenuChoice {
|
|
5
|
-
readonly provider: string;
|
|
6
|
-
readonly model: string;
|
|
7
|
-
readonly label: string;
|
|
8
|
-
readonly efforts: ReadonlyArray<{
|
|
9
|
-
id: string;
|
|
10
|
-
name: string;
|
|
11
|
-
}>;
|
|
12
|
-
}
|
|
13
3
|
/** The stored selection. Empty provider/model means "follow the default". */
|
|
14
4
|
type ModelMenuRoute = ModelRoute;
|
|
15
|
-
/** The catalog face this helper needs; stays structural so hosts may add more. */
|
|
16
|
-
interface ModelCatalogClient {
|
|
17
|
-
readonly remote?: {
|
|
18
|
-
readonly session?: {
|
|
19
|
-
readonly modelCatalog?: () => Promise<unknown>;
|
|
20
|
-
};
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
declare function modelMenuChoiceKey(provider: string, model: string): string;
|
|
24
|
-
declare function parseModelMenuChoiceKey(value: string): {
|
|
25
|
-
provider: string;
|
|
26
|
-
model: string;
|
|
27
|
-
} | undefined;
|
|
28
|
-
/** Parse the host session catalog into flat choices, keeping a saved route selectable. */
|
|
29
|
-
declare function parseModelMenuChoices(value: unknown, bound?: ModelMenuRoute): ModelMenuChoice[];
|
|
30
|
-
/** An empty saved route means no explicit model. */
|
|
31
|
-
declare function modelMenuOverride(route: ModelMenuRoute | undefined): ModelRoute | undefined;
|
|
32
|
-
declare function normalizeModelMenuRoute(value: unknown): ModelMenuRoute;
|
|
33
5
|
/** Load the host catalog once per mount and republish whenever it answers. */
|
|
34
|
-
declare function useModelMenuChoices(client: ModelCatalogClient, bound?: ModelMenuRoute): ModelMenuChoice[];
|
|
35
|
-
/** Reasoning-effort options for one choice; the saved value stays selectable. */
|
|
36
|
-
declare function modelMenuEffortOptions(choice: ModelMenuChoice | undefined, current?: string): Array<{
|
|
37
|
-
id: string;
|
|
38
|
-
name: string;
|
|
39
|
-
}>;
|
|
6
|
+
declare function useModelMenuChoices(client: ModelCatalogClient$1, bound?: ModelMenuRoute): ModelMenuChoice$1[];
|
|
40
7
|
//#endregion
|
|
41
|
-
export { ModelCatalogClient, ModelMenuChoice, ModelMenuRoute, modelMenuChoiceKey, modelMenuEffortOptions, modelMenuOverride, normalizeModelMenuRoute, parseModelMenuChoiceKey, parseModelMenuChoices, useModelMenuChoices };
|
|
8
|
+
export { type ModelCatalogClient, type ModelMenuChoice, type ModelMenuGroup, ModelMenuRoute, groupModelMenuChoices, knownModelFromChoices, modelMenuChoiceKey, modelMenuEffortLabel, modelMenuEffortOptions, modelMenuOverride, modelMenuShortName, normalizeModelMenuRoute, parseModelMenuChoiceKey, parseModelMenuChoices, useModelMenuChoices };
|
|
42
9
|
//# sourceMappingURL=model-menu.d.ts.map
|
package/lib/model-menu.js
CHANGED
|
@@ -1,118 +1,18 @@
|
|
|
1
1
|
import { useEffect, useState } from "react";
|
|
2
|
+
import { groupModelMenuChoices, knownModelFromChoices, modelMenuEffortLabel, modelMenuEffortOptions, modelMenuShortName, modelRouteKey as modelMenuChoiceKey, modelRouteOverride as modelMenuOverride, normalizeModelRoute as normalizeModelMenuRoute, parseModelMenuChoices, parseModelMenuChoices as parseModelMenuChoices$1, parseModelRouteKey as parseModelMenuChoiceKey } from "@klarkxy/dsh-model-route";
|
|
2
3
|
//#region src/model-menu.ts
|
|
3
4
|
/**
|
|
4
5
|
* Shared plugin-page model menu.
|
|
5
6
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* Everything lives in @klarkxy/dsh-model-route: the pure contract (route
|
|
8
|
+
* values, schema marker, catalog parsing, menu grouping) in its core entry,
|
|
9
|
+
* and the ModelMenu popover component in its `./ui` browser entry. This entry
|
|
10
|
+
* re-exports the core so existing plugin-kit consumers keep working, and adds
|
|
11
|
+
* the React catalog hook on top. It must stay free of any ui-primitives
|
|
12
|
+
* import so host-side and node-test consumers never load the component
|
|
13
|
+
* bundle — the popover component and its styles live in
|
|
14
|
+
* `@klarkxy/dsh-model-route/ui`.
|
|
11
15
|
*/
|
|
12
|
-
const KEY_SEPARATOR = "";
|
|
13
|
-
function modelMenuChoiceKey(provider, model) {
|
|
14
|
-
return provider && model ? `${provider}${KEY_SEPARATOR}${model}` : "";
|
|
15
|
-
}
|
|
16
|
-
function parseModelMenuChoiceKey(value) {
|
|
17
|
-
const index = value.indexOf(KEY_SEPARATOR);
|
|
18
|
-
if (index <= 0) return void 0;
|
|
19
|
-
const provider = value.slice(0, index);
|
|
20
|
-
const model = value.slice(index + 1);
|
|
21
|
-
if (!provider || !model) return void 0;
|
|
22
|
-
return {
|
|
23
|
-
provider,
|
|
24
|
-
model
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
function asRecord(value) {
|
|
28
|
-
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
29
|
-
}
|
|
30
|
-
function asText(value) {
|
|
31
|
-
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
32
|
-
}
|
|
33
|
-
function parseEffort(value) {
|
|
34
|
-
const row = asRecord(value);
|
|
35
|
-
const id = row ? asText(row.id) : void 0;
|
|
36
|
-
if (!id) return void 0;
|
|
37
|
-
return {
|
|
38
|
-
id,
|
|
39
|
-
name: row ? asText(row.name) ?? id : id
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
/** Parse the host session catalog into flat choices, keeping a saved route selectable. */
|
|
43
|
-
function parseModelMenuChoices(value, bound) {
|
|
44
|
-
const envelope = asRecord(value);
|
|
45
|
-
const root = (envelope && envelope.ok === true ? asRecord(envelope.value) : void 0) ?? envelope ?? {};
|
|
46
|
-
const groups = Array.isArray(root.groups) ? root.groups : [];
|
|
47
|
-
const choices = [];
|
|
48
|
-
for (const group of groups) {
|
|
49
|
-
const groupRow = asRecord(group);
|
|
50
|
-
const provider = groupRow ? asText(groupRow.id) : void 0;
|
|
51
|
-
if (!provider || !groupRow || !Array.isArray(groupRow.models)) continue;
|
|
52
|
-
const providerName = asText(groupRow.name) ?? provider;
|
|
53
|
-
for (const entry of groupRow.models) {
|
|
54
|
-
const modelRow = asRecord(entry);
|
|
55
|
-
const model = modelRow ? asText(modelRow.id) : void 0;
|
|
56
|
-
if (!model || !modelRow) continue;
|
|
57
|
-
const name = asText(modelRow.name) ?? model;
|
|
58
|
-
const reasoning = asRecord(modelRow.reasoning);
|
|
59
|
-
const efforts = reasoning && Array.isArray(reasoning.efforts) ? reasoning.efforts.flatMap((effort) => {
|
|
60
|
-
const parsed = parseEffort(effort);
|
|
61
|
-
return parsed ? [parsed] : [];
|
|
62
|
-
}) : [];
|
|
63
|
-
choices.push({
|
|
64
|
-
provider,
|
|
65
|
-
model,
|
|
66
|
-
label: `${providerName} / ${name}`,
|
|
67
|
-
efforts
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
if (bound?.provider && bound.model && !choices.some((item) => item.provider === bound.provider && item.model === bound.model)) choices.push({
|
|
72
|
-
provider: bound.provider,
|
|
73
|
-
model: bound.model,
|
|
74
|
-
label: `${bound.provider} / ${bound.model}`,
|
|
75
|
-
efforts: []
|
|
76
|
-
});
|
|
77
|
-
return choices;
|
|
78
|
-
}
|
|
79
|
-
/** An empty saved route means no explicit model. */
|
|
80
|
-
function modelMenuOverride(route) {
|
|
81
|
-
const provider = route?.provider?.trim();
|
|
82
|
-
const model = route?.model?.trim();
|
|
83
|
-
if (!provider || !model) return void 0;
|
|
84
|
-
const effort = route?.reasoningEffort?.trim();
|
|
85
|
-
return effort ? {
|
|
86
|
-
provider,
|
|
87
|
-
model,
|
|
88
|
-
reasoningEffort: effort
|
|
89
|
-
} : {
|
|
90
|
-
provider,
|
|
91
|
-
model
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
function normalizeModelMenuRoute(value) {
|
|
95
|
-
const row = asRecord(value);
|
|
96
|
-
if (!row) return {
|
|
97
|
-
provider: "",
|
|
98
|
-
model: ""
|
|
99
|
-
};
|
|
100
|
-
const provider = asText(row.provider) ?? "";
|
|
101
|
-
const model = asText(row.model) ?? "";
|
|
102
|
-
const effort = asText(row.reasoningEffort);
|
|
103
|
-
if (!provider || !model || provider.length > 250 || model.length > 250) return {
|
|
104
|
-
provider: "",
|
|
105
|
-
model: ""
|
|
106
|
-
};
|
|
107
|
-
return effort && effort.length <= 80 ? {
|
|
108
|
-
provider,
|
|
109
|
-
model,
|
|
110
|
-
reasoningEffort: effort
|
|
111
|
-
} : {
|
|
112
|
-
provider,
|
|
113
|
-
model
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
16
|
/** Load the host catalog once per mount and republish whenever it answers. */
|
|
117
17
|
function useModelMenuChoices(client, bound) {
|
|
118
18
|
const [choices, setChoices] = useState([]);
|
|
@@ -121,7 +21,7 @@ function useModelMenuChoices(client, bound) {
|
|
|
121
21
|
let live = true;
|
|
122
22
|
const load = () => {
|
|
123
23
|
client.remote?.session?.modelCatalog?.()?.then((value) => {
|
|
124
|
-
if (live) setChoices(parseModelMenuChoices(value, bound));
|
|
24
|
+
if (live) setChoices(parseModelMenuChoices$1(value, bound));
|
|
125
25
|
}).catch(() => {
|
|
126
26
|
if (live) setChoices([]);
|
|
127
27
|
});
|
|
@@ -137,16 +37,7 @@ function useModelMenuChoices(client, bound) {
|
|
|
137
37
|
]);
|
|
138
38
|
return choices;
|
|
139
39
|
}
|
|
140
|
-
/** Reasoning-effort options for one choice; the saved value stays selectable. */
|
|
141
|
-
function modelMenuEffortOptions(choice, current) {
|
|
142
|
-
const listed = (choice?.efforts ?? []).map((item) => ({ ...item }));
|
|
143
|
-
if (current && !listed.some((item) => item.id === current)) listed.push({
|
|
144
|
-
id: current,
|
|
145
|
-
name: current
|
|
146
|
-
});
|
|
147
|
-
return listed;
|
|
148
|
-
}
|
|
149
40
|
//#endregion
|
|
150
|
-
export { modelMenuChoiceKey, modelMenuEffortOptions, modelMenuOverride, normalizeModelMenuRoute, parseModelMenuChoiceKey, parseModelMenuChoices, useModelMenuChoices };
|
|
41
|
+
export { groupModelMenuChoices, knownModelFromChoices, modelMenuChoiceKey, modelMenuEffortLabel, modelMenuEffortOptions, modelMenuOverride, modelMenuShortName, normalizeModelMenuRoute, parseModelMenuChoiceKey, parseModelMenuChoices, useModelMenuChoices };
|
|
151
42
|
|
|
152
43
|
//# sourceMappingURL=model-menu.js.map
|