@klarkxy/dsh-plugin-kit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +41 -0
- package/README.md +59 -0
- package/docs/README.zh-CN.md +43 -0
- package/lib/client-utils.d.ts +52 -0
- package/lib/client-utils.js +94 -0
- package/lib/client-utils.js.map +1 -0
- package/lib/contracts.d.ts +279 -0
- package/lib/contracts.js +33 -0
- package/lib/contracts.js.map +1 -0
- package/lib/host-rpc.d.ts +20 -0
- package/lib/host-rpc.js +95 -0
- package/lib/host-rpc.js.map +1 -0
- package/lib/index.d.ts +4 -0
- package/lib/index.js +4 -0
- package/lib/llm-call.d.ts +36 -0
- package/lib/llm-call.js +118 -0
- package/lib/llm-call.js.map +1 -0
- package/lib/model-menu.d.ts +42 -0
- package/lib/model-menu.js +152 -0
- package/lib/model-menu.js.map +1 -0
- package/lib/official-ui.d.ts +110 -0
- package/lib/official-ui.js +421 -0
- package/lib/official-ui.js.map +1 -0
- package/package.json +95 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { RpcResult } from "./contracts.js";
|
|
2
|
+
import { IncomingMessage, ServerResponse } from "node:http";
|
|
3
|
+
//#region src/host-rpc.d.ts
|
|
4
|
+
interface HostRpcContext {
|
|
5
|
+
webServer: {
|
|
6
|
+
register(route: {
|
|
7
|
+
kind: 'prefix';
|
|
8
|
+
path: string;
|
|
9
|
+
handler(req: IncomingMessage, res: ServerResponse): void | Promise<void>;
|
|
10
|
+
}): () => void;
|
|
11
|
+
};
|
|
12
|
+
connection: {
|
|
13
|
+
requestRejection?: (request: IncomingMessage) => number | undefined;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/** Safe host RPC registration. Missing requestRejection fails closed. Result type accepts any RpcResult shape. */
|
|
17
|
+
declare function registerHostRpc<R extends RpcResult = RpcResult>(ctx: HostRpcContext, channel: string, call: (endpoint: string, payload: unknown, signal: AbortSignal) => Promise<R>): () => void;
|
|
18
|
+
//#endregion
|
|
19
|
+
export { HostRpcContext, registerHostRpc };
|
|
20
|
+
//# sourceMappingURL=host-rpc.d.ts.map
|
package/lib/host-rpc.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
//#region src/host-rpc.ts
|
|
2
|
+
/** Safe host RPC registration. Missing requestRejection fails closed. Result type accepts any RpcResult shape. */
|
|
3
|
+
function registerHostRpc(ctx, channel, call) {
|
|
4
|
+
return ctx.webServer.register({
|
|
5
|
+
kind: "prefix",
|
|
6
|
+
path: channel,
|
|
7
|
+
handler: async (req, res) => {
|
|
8
|
+
if (!ctx.connection.requestRejection) {
|
|
9
|
+
res.writeHead(503);
|
|
10
|
+
res.end();
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const rejection = ctx.connection.requestRejection(req);
|
|
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
|
+
});
|
|
35
|
+
try {
|
|
36
|
+
const chunks = [];
|
|
37
|
+
let size = 0;
|
|
38
|
+
for await (const chunk of req) {
|
|
39
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
40
|
+
size += buffer.length;
|
|
41
|
+
if (size > 65536) {
|
|
42
|
+
res.writeHead(413, { connection: "close" });
|
|
43
|
+
res.end();
|
|
44
|
+
req.destroy();
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
chunks.push(buffer);
|
|
48
|
+
}
|
|
49
|
+
let request;
|
|
50
|
+
try {
|
|
51
|
+
const parsed = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
52
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error();
|
|
53
|
+
request = parsed;
|
|
54
|
+
} catch {
|
|
55
|
+
res.writeHead(400);
|
|
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();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const raw = await call(endpoint, request.payload, controller.signal);
|
|
65
|
+
const result = raw.ok ? raw : {
|
|
66
|
+
...raw,
|
|
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
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
//#endregion
|
|
93
|
+
export { registerHostRpc };
|
|
94
|
+
|
|
95
|
+
//# sourceMappingURL=host-rpc.js.map
|
|
@@ -0,0 +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/** Safe host RPC registration. Missing requestRejection fails closed. Result type accepts any RpcResult shape. */\nexport function registerHostRpc<R extends RpcResult = RpcResult>(\n ctx: HostRpcContext,\n channel: string,\n call: (endpoint: string, payload: unknown, signal: AbortSignal) => Promise<R>,\n): () => void {\n return ctx.webServer.register({ kind: 'prefix', path: channel, handler: async (req, res) => {\n // Require the host's origin/auth policy. Missing policy must not expose settings.\n if (!ctx.connection.requestRejection) { res.writeHead(503); res.end(); return }\n const rejection = ctx.connection.requestRejection(req)\n if (rejection !== undefined) { res.writeHead(rejection); res.end(); 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' || !/^[a-z.]+$/.test(endpoint)) { res.writeHead(404); res.end(); return }\n if (String(req.headers['content-type'] ?? '').split(';')[0]?.trim() !== 'application/json') {\n res.writeHead(415); res.end(); return\n }\n const controller = new AbortController()\n res.on('close', () => { if (!res.writableEnded) controller.abort() })\n try {\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 > 64 * 1024) { res.writeHead(413, { connection: 'close' }); res.end(); req.destroy(); return }\n chunks.push(buffer)\n }\n let request: Record<string, unknown>\n try {\n const parsed: unknown = JSON.parse(Buffer.concat(chunks).toString('utf8'))\n if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error()\n request = parsed as Record<string, unknown>\n } catch { res.writeHead(400); res.end(); return }\n if (request.type !== 'client-request' || request.method !== endpoint || typeof request.rpcId !== 'string') {\n res.writeHead(400); res.end(); return\n }\n const raw = await call(endpoint, request.payload, controller.signal)\n const result = raw.ok ? raw : { ...raw, error: { details: {}, ...raw.error } }\n if (!res.destroyed) {\n res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' })\n res.end(JSON.stringify({ type: 'server-response', rpcId: request.rpcId, result }))\n }\n } catch {\n if (!res.destroyed) { res.writeHead(500); res.end('rpc request failed') }\n }\n } })\n}\n"],"mappings":";;AASA,SAAgB,gBACd,KACA,SACA,MACY;CACZ,OAAO,IAAI,UAAU,SAAS;EAAE,MAAM;EAAU,MAAM;EAAS,SAAS,OAAO,KAAK,QAAQ;GAE1F,IAAI,CAAC,IAAI,WAAW,kBAAkB;IAAE,IAAI,UAAU,GAAG;IAAG,IAAI,IAAI;IAAG;GAAO;GAC9E,MAAM,YAAY,IAAI,WAAW,iBAAiB,GAAG;GACrD,IAAI,cAAc,KAAA,GAAW;IAAE,IAAI,UAAU,SAAS;IAAG,IAAI,IAAI;IAAG;GAAO;GAC3E,MAAM,WAAW,IAAI,IAAI,IAAI,OAAO,KAAK,qBAAqB,CAAC,CAAC;GAChE,MAAM,WAAW,SAAS,WAAW,GAAG,QAAQ,EAAE,IAAI,SAAS,MAAM,QAAQ,SAAS,CAAC,IAAI;GAC3F,IAAI,IAAI,WAAW,UAAU,CAAC,YAAY,KAAK,QAAQ,GAAG;IAAE,IAAI,UAAU,GAAG;IAAG,IAAI,IAAI;IAAG;GAAO;GAClG,IAAI,OAAO,IAAI,QAAQ,mBAAmB,EAAE,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,EAAE,EAAE,KAAK,MAAM,oBAAoB;IAC1F,IAAI,UAAU,GAAG;IAAG,IAAI,IAAI;IAAG;GACjC;GACA,MAAM,aAAa,IAAI,gBAAgB;GACvC,IAAI,GAAG,eAAe;IAAE,IAAI,CAAC,IAAI,eAAe,WAAW,MAAM;GAAE,CAAC;GACpE,IAAI;IACF,MAAM,SAAmB,CAAC;IAC1B,IAAI,OAAO;IACX,WAAW,MAAM,SAAS,KAAK;KAC7B,MAAM,SAAS,OAAO,SAAS,KAAK,IAAI,QAAQ,OAAO,KAAK,KAAK;KACjE,QAAQ,OAAO;KACf,IAAI,OAAO,OAAW;MAAE,IAAI,UAAU,KAAK,EAAE,YAAY,QAAQ,CAAC;MAAG,IAAI,IAAI;MAAG,IAAI,QAAQ;MAAG;KAAO;KACtG,OAAO,KAAK,MAAM;IACpB;IACA,IAAI;IACJ,IAAI;KACF,MAAM,SAAkB,KAAK,MAAM,OAAO,OAAO,MAAM,CAAC,CAAC,SAAS,MAAM,CAAC;KACzE,IAAI,CAAC,UAAU,OAAO,WAAW,YAAY,MAAM,QAAQ,MAAM,GAAG,MAAM,IAAI,MAAM;KACpF,UAAU;IACZ,QAAQ;KAAE,IAAI,UAAU,GAAG;KAAG,IAAI,IAAI;KAAG;IAAO;IAChD,IAAI,QAAQ,SAAS,oBAAoB,QAAQ,WAAW,YAAY,OAAO,QAAQ,UAAU,UAAU;KACzG,IAAI,UAAU,GAAG;KAAG,IAAI,IAAI;KAAG;IACjC;IACA,MAAM,MAAM,MAAM,KAAK,UAAU,QAAQ,SAAS,WAAW,MAAM;IACnE,MAAM,SAAS,IAAI,KAAK,MAAM;KAAE,GAAG;KAAK,OAAO;MAAE,SAAS,CAAC;MAAG,GAAG,IAAI;KAAM;IAAE;IAC7E,IAAI,CAAC,IAAI,WAAW;KAClB,IAAI,UAAU,KAAK;MAAE,gBAAgB;MAAoB,iBAAiB;KAAW,CAAC;KACtF,IAAI,IAAI,KAAK,UAAU;MAAE,MAAM;MAAmB,OAAO,QAAQ;MAAO;KAAO,CAAC,CAAC;IACnF;GACF,QAAQ;IACN,IAAI,CAAC,IAAI,WAAW;KAAE,IAAI,UAAU,GAAG;KAAG,IAAI,IAAI,oBAAoB;IAAE;GAC1E;EACF;CAAE,CAAC;AACL"}
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { AiFeatureScope, AiPolicy, AiServices, AuxiliaryRequest, AuxiliaryResult, CHAT_EVENTS_SLOT, ContextKnowledge, EvidenceRef, KnowledgeKind, KnowledgeScope, MemoryMutationOptions, MemoryQuery, MemoryRecord, MemoryService, ModelRole, ModelRoute, ModelTarget, NewMemoryRecord, ProcedureKnowledge, ProducerMessageSource, ProducerSourceKind, PurposeSpec, ResolvedRoute, RpcResult, SubagentSessionLike, TaskCheckpoint, TaskContract, UsageReceipt, isSubagentSession, producerMessageSource, projectIdFromCwd } from "./contracts.js";
|
|
2
|
+
import { HostRpcContext, registerHostRpc } from "./host-rpc.js";
|
|
3
|
+
import { FeatureModelRoute, LlmTextCaller, callLlmText, resolveFeatureModel } from "./llm-call.js";
|
|
4
|
+
export { type AiFeatureScope, type AiPolicy, type AiServices, type AuxiliaryRequest, type AuxiliaryResult, CHAT_EVENTS_SLOT, type ContextKnowledge, type EvidenceRef, type FeatureModelRoute, type HostRpcContext, type KnowledgeKind, type KnowledgeScope, type LlmTextCaller, type MemoryMutationOptions, type MemoryQuery, type MemoryRecord, type MemoryService, type ModelRole, type ModelRoute, type ModelTarget, type NewMemoryRecord, type ProcedureKnowledge, type ProducerMessageSource, type ProducerSourceKind, type PurposeSpec, type ResolvedRoute, type RpcResult, type SubagentSessionLike, type TaskCheckpoint, type TaskContract, type UsageReceipt, callLlmText, type isSubagentSession, producerMessageSource, projectIdFromCwd, registerHostRpc, resolveFeatureModel };
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { CHAT_EVENTS_SLOT, producerMessageSource, projectIdFromCwd } from "./contracts.js";
|
|
2
|
+
import { registerHostRpc } from "./host-rpc.js";
|
|
3
|
+
import { callLlmText, resolveFeatureModel } from "./llm-call.js";
|
|
4
|
+
export { CHAT_EVENTS_SLOT, callLlmText, producerMessageSource, projectIdFromCwd, registerHostRpc, resolveFeatureModel };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { GenerateOptions, LlmRuntime } from "@deepseek-ai/dsh-llm";
|
|
2
|
+
//#region src/llm-call.d.ts
|
|
3
|
+
/** A feature's own saved model. Empty means the host default chat model. */
|
|
4
|
+
interface FeatureModelRoute {
|
|
5
|
+
readonly provider: string;
|
|
6
|
+
readonly model: string;
|
|
7
|
+
readonly reasoningEffort?: string;
|
|
8
|
+
}
|
|
9
|
+
type LlmTextCaller = Pick<LlmRuntime, 'prepareCall' | 'resolveCallConfig'>;
|
|
10
|
+
interface LlmTextRequest {
|
|
11
|
+
plugin: string;
|
|
12
|
+
route: FeatureModelRoute;
|
|
13
|
+
system: string;
|
|
14
|
+
text: string;
|
|
15
|
+
maxTokens: number;
|
|
16
|
+
signal: AbortSignal;
|
|
17
|
+
sessionId?: string;
|
|
18
|
+
/** Auxiliary-call marker adapters may special-case (e.g. disable thinking for titles). */
|
|
19
|
+
purpose?: GenerateOptions['purpose'];
|
|
20
|
+
/** Rechecked before the call returns text. A false result discards the output. */
|
|
21
|
+
isCurrent?: () => boolean;
|
|
22
|
+
}
|
|
23
|
+
interface LlmTextResult {
|
|
24
|
+
text: string;
|
|
25
|
+
provider: string;
|
|
26
|
+
model: string;
|
|
27
|
+
inputTokens?: number;
|
|
28
|
+
outputTokens?: number;
|
|
29
|
+
}
|
|
30
|
+
/** Plugin selection first; otherwise the live session model, then the host default. */
|
|
31
|
+
declare function resolveFeatureModel(host: unknown, saved: FeatureModelRoute | undefined, sessionId?: string): FeatureModelRoute | undefined;
|
|
32
|
+
/** One native model call. No retry, timeout policy, or provider queue. */
|
|
33
|
+
declare function callLlmText(llm: LlmTextCaller, request: LlmTextRequest): Promise<LlmTextResult>;
|
|
34
|
+
//#endregion
|
|
35
|
+
export { FeatureModelRoute, LlmTextCaller, LlmTextRequest, LlmTextResult, callLlmText, resolveFeatureModel };
|
|
36
|
+
//# sourceMappingURL=llm-call.d.ts.map
|
package/lib/llm-call.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { producerMessageSource } from "./contracts.js";
|
|
2
|
+
import { BlockAssembler, ReasoningEffortId, createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
3
|
+
//#region src/llm-call.ts
|
|
4
|
+
function routeOf(value) {
|
|
5
|
+
if (typeof value?.provider !== "string" || !value.provider.trim()) return void 0;
|
|
6
|
+
if (typeof value.model !== "string" || !value.model.trim()) return void 0;
|
|
7
|
+
const effort = typeof value.reasoningEffort === "string" ? value.reasoningEffort.trim() : "";
|
|
8
|
+
return effort ? {
|
|
9
|
+
provider: value.provider.trim(),
|
|
10
|
+
model: value.model.trim(),
|
|
11
|
+
reasoningEffort: effort
|
|
12
|
+
} : {
|
|
13
|
+
provider: value.provider.trim(),
|
|
14
|
+
model: value.model.trim()
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/** Plugin selection first; otherwise the live session model, then the host default. */
|
|
18
|
+
function resolveFeatureModel(host, saved, sessionId) {
|
|
19
|
+
const explicit = routeOf(saved);
|
|
20
|
+
if (explicit) return explicit;
|
|
21
|
+
const face = host;
|
|
22
|
+
if (sessionId && face?.agents && face.sessionProjections) {
|
|
23
|
+
const session = face.agents.get(sessionId)?.session;
|
|
24
|
+
if (session) {
|
|
25
|
+
try {
|
|
26
|
+
const pending = face.sessionProjections.stateOf(session, "modelSelection")?.pending;
|
|
27
|
+
const selected = routeOf(pending ?? void 0);
|
|
28
|
+
if (selected) return selected;
|
|
29
|
+
} catch {}
|
|
30
|
+
const header = session.requestHeader?.();
|
|
31
|
+
const recorded = header?.config;
|
|
32
|
+
if (recorded) {
|
|
33
|
+
const route = routeOf(header?.adapterDefaults?.reasoningEffort === true ? {
|
|
34
|
+
provider: recorded.provider,
|
|
35
|
+
model: recorded.model
|
|
36
|
+
} : recorded);
|
|
37
|
+
if (route) return route;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return routeOf(face?.agentDefaultModel?.currentSelection());
|
|
42
|
+
}
|
|
43
|
+
function joinText(assembler) {
|
|
44
|
+
return assembler.blocks().filter((block) => block.type === "text").map((block) => block.text).join("");
|
|
45
|
+
}
|
|
46
|
+
async function readStream(stream, signal, live) {
|
|
47
|
+
const assembler = new BlockAssembler();
|
|
48
|
+
const iterator = stream[Symbol.asyncIterator]();
|
|
49
|
+
try {
|
|
50
|
+
while (live()) {
|
|
51
|
+
if (signal.aborted) break;
|
|
52
|
+
const item = await iterator.next();
|
|
53
|
+
if (item.done) break;
|
|
54
|
+
assembler.push(item.value);
|
|
55
|
+
}
|
|
56
|
+
} finally {
|
|
57
|
+
try {
|
|
58
|
+
const closing = iterator.return?.();
|
|
59
|
+
if (closing) closing.catch(() => {});
|
|
60
|
+
} catch {}
|
|
61
|
+
}
|
|
62
|
+
return assembler;
|
|
63
|
+
}
|
|
64
|
+
/** One native model call. No retry, timeout policy, or provider queue. */
|
|
65
|
+
async function callLlmText(llm, request) {
|
|
66
|
+
request.signal.throwIfAborted();
|
|
67
|
+
const live = () => request.isCurrent?.() !== false && !request.signal.aborted;
|
|
68
|
+
if (!live()) throw Object.assign(/* @__PURE__ */ new Error("调用已取消。"), { name: "AbortError" });
|
|
69
|
+
const effort = request.route.reasoningEffort?.trim();
|
|
70
|
+
const prepared = await llm.prepareCall({
|
|
71
|
+
provider: request.route.provider,
|
|
72
|
+
model: request.route.model,
|
|
73
|
+
...effort ? { reasoningEffort: ReasoningEffortId(effort) } : {},
|
|
74
|
+
maxTokens: request.maxTokens
|
|
75
|
+
}, request.signal);
|
|
76
|
+
if (!live()) throw Object.assign(/* @__PURE__ */ new Error("调用已取消。"), { name: "AbortError" });
|
|
77
|
+
const options = {
|
|
78
|
+
provider: prepared.config.provider,
|
|
79
|
+
model: prepared.config.model,
|
|
80
|
+
...prepared.config.reasoningEffort ? { reasoningEffort: prepared.config.reasoningEffort } : {},
|
|
81
|
+
...prepared.config.maxTokens !== void 0 ? { maxTokens: Math.min(request.maxTokens, prepared.config.maxTokens) } : { maxTokens: request.maxTokens },
|
|
82
|
+
...prepared.config.temperature !== void 0 ? { temperature: prepared.config.temperature } : {},
|
|
83
|
+
...prepared.config.stop ? { stop: [...prepared.config.stop] } : {},
|
|
84
|
+
system: request.system,
|
|
85
|
+
messages: [createUserMessage({
|
|
86
|
+
source: producerMessageSource(request.plugin),
|
|
87
|
+
content: [{
|
|
88
|
+
type: "text",
|
|
89
|
+
text: request.text
|
|
90
|
+
}]
|
|
91
|
+
})],
|
|
92
|
+
signal: request.signal,
|
|
93
|
+
...request.purpose ? { purpose: request.purpose } : {},
|
|
94
|
+
...request.sessionId ? { sessionId: request.sessionId } : {}
|
|
95
|
+
};
|
|
96
|
+
const assembler = await readStream(prepared.stream(options), request.signal, live);
|
|
97
|
+
if (request.signal.aborted || assembler.finish.kind === "aborted") throw Object.assign(/* @__PURE__ */ new Error("调用已取消。"), { name: "AbortError" });
|
|
98
|
+
if (!live()) throw Object.assign(/* @__PURE__ */ new Error("调用已取消。"), { name: "AbortError" });
|
|
99
|
+
if (assembler.finish.kind === "error") {
|
|
100
|
+
const code = assembler.finish.failure.code;
|
|
101
|
+
throw Object.assign(new Error(assembler.finish.failure.message || "模型调用失败。"), { code });
|
|
102
|
+
}
|
|
103
|
+
if (assembler.blocks().some((block) => block.type === "tool-call") || assembler.finish.kind === "tool-calls") throw new Error("这次调用不使用工具。");
|
|
104
|
+
if (assembler.finish.kind !== "stop") throw new Error("模型调用未完成。");
|
|
105
|
+
return {
|
|
106
|
+
text: joinText(assembler),
|
|
107
|
+
provider: prepared.config.provider,
|
|
108
|
+
model: prepared.config.model,
|
|
109
|
+
...assembler.usage ? {
|
|
110
|
+
inputTokens: assembler.usage.inputTokens,
|
|
111
|
+
outputTokens: assembler.usage.outputTokens
|
|
112
|
+
} : {}
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
//#endregion
|
|
116
|
+
export { callLlmText, resolveFeatureModel };
|
|
117
|
+
|
|
118
|
+
//# sourceMappingURL=llm-call.js.map
|
|
@@ -0,0 +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":";;;AA2CA,SAAS,QAAQ,OAAsH;CACrI,IAAI,OAAO,OAAO,aAAa,YAAY,CAAC,MAAM,SAAS,KAAK,GAAG,OAAO,KAAA;CAC1E,IAAI,OAAO,MAAM,UAAU,YAAY,CAAC,MAAM,MAAM,KAAK,GAAG,OAAO,KAAA;CACnE,MAAM,SAAS,OAAO,MAAM,oBAAoB,WAAW,MAAM,gBAAgB,KAAK,IAAI;CAC1F,OAAO,SACH;EAAE,UAAU,MAAM,SAAS,KAAK;EAAG,OAAO,MAAM,MAAM,KAAK;EAAG,iBAAiB;CAAO,IACtF;EAAE,UAAU,MAAM,SAAS,KAAK;EAAG,OAAO,MAAM,MAAM,KAAK;CAAE;AACnE;;AAGA,SAAgB,oBAAoB,MAAe,OAAsC,WAAmD;CAC1I,MAAM,WAAW,QAAQ,KAAK;CAC9B,IAAI,UAAU,OAAO;CACrB,MAAM,OAAO;CACb,IAAI,aAAa,MAAM,UAAU,KAAK,oBAAoB;EACxD,MAAM,UAAU,KAAK,OAAO,IAAI,SAAS,CAAC,EAAE;EAC5C,IAAI,SAAS;GACX,IAAI;IACF,MAAM,UAAU,KAAK,mBAAmB,QAAQ,SAAS,gBAAgB,CAAC,EAAE;IAC5E,MAAM,WAAW,QAAQ,WAAW,KAAA,CAAS;IAC7C,IAAI,UAAU,OAAO;GACvB,QAAQ,CAA2C;GACnD,MAAM,SAAS,QAAQ,gBAAgB;GACvC,MAAM,WAAW,QAAQ;GACzB,IAAI,UAAU;IAIZ,MAAM,QAAQ,QAHG,QAAQ,iBAAiB,oBAAoB,OAC1D;KAAE,UAAU,SAAS;KAAU,OAAO,SAAS;IAAM,IACrD,QAC0B;IAC9B,IAAI,OAAO,OAAO;GACpB;EACF;CACF;CACA,OAAO,QAAQ,MAAM,mBAAmB,iBAAiB,CAAC;AAC5D;AAEA,SAAS,SAAS,WAAmC;CACnD,OAAO,UAAU,OAAO,CAAC,CACtB,QAAQ,UAAmD,MAAM,SAAS,MAAM,CAAC,CACjF,KAAI,UAAS,MAAM,IAAI,CAAC,CACxB,KAAK,EAAE;AACZ;AAEA,eAAe,WAAW,QAAoC,QAAqB,MAA8C;CAC/H,MAAM,YAAY,IAAI,eAAe;CACrC,MAAM,WAAW,OAAO,OAAO,cAAc,CAAC;CAC9C,IAAI;EACF,OAAO,KAAK,GAAG;GACb,IAAI,OAAO,SAAS;GACpB,MAAM,OAAO,MAAM,SAAS,KAAK;GACjC,IAAI,KAAK,MAAM;GACf,UAAU,KAAK,KAAK,KAAK;EAC3B;CACF,UAAU;EACR,IAAI;GAAE,MAAM,UAAU,SAAS,SAAS;GAAG,IAAI,SAAS,QAAa,YAAY,CAAC,CAAC;EAAE,QAAQ,CAA+B;CAC9H;CACA,OAAO;AACT;;AAGA,eAAsB,YAAY,KAAoB,SAAiD;CACrG,QAAQ,OAAO,eAAe;CAC9B,MAAM,aAAa,QAAQ,YAAY,MAAM,SAAS,CAAC,QAAQ,OAAO;CACtE,IAAI,CAAC,KAAK,GAAG,MAAM,OAAO,uBAAO,IAAI,MAAM,QAAQ,GAAG,EAAE,MAAM,aAAa,CAAC;CAC5E,MAAM,SAAS,QAAQ,MAAM,iBAAiB,KAAK;CACnD,MAAM,WAAW,MAAM,IAAI,YAAY;EACrC,UAAU,QAAQ,MAAM;EACxB,OAAO,QAAQ,MAAM;EACrB,GAAI,SAAS,EAAE,iBAAiB,kBAAkB,MAAM,EAAE,IAAI,CAAC;EAC/D,WAAW,QAAQ;CACrB,GAAG,QAAQ,MAAM;CACjB,IAAI,CAAC,KAAK,GAAG,MAAM,OAAO,uBAAO,IAAI,MAAM,QAAQ,GAAG,EAAE,MAAM,aAAa,CAAC;CAC5E,MAAM,UAA2B;EAC/B,UAAU,SAAS,OAAO;EAC1B,OAAO,SAAS,OAAO;EACvB,GAAI,SAAS,OAAO,kBAAkB,EAAE,iBAAiB,SAAS,OAAO,gBAAgB,IAAI,CAAC;EAC9F,GAAI,SAAS,OAAO,cAAc,KAAA,IAAY,EAAE,WAAW,KAAK,IAAI,QAAQ,WAAW,SAAS,OAAO,SAAS,EAAE,IAAI,EAAE,WAAW,QAAQ,UAAU;EACrJ,GAAI,SAAS,OAAO,gBAAgB,KAAA,IAAY,EAAE,aAAa,SAAS,OAAO,YAAY,IAAI,CAAC;EAChG,GAAI,SAAS,OAAO,OAAO,EAAE,MAAM,CAAC,GAAG,SAAS,OAAO,IAAI,EAAE,IAAI,CAAC;EAClE,QAAQ,QAAQ;EAChB,UAAU,CAAC,kBAAkB;GAC3B,QAAQ,sBAAsB,QAAQ,MAAM;GAC5C,SAAS,CAAC;IAAE,MAAM;IAAQ,MAAM,QAAQ;GAAK,CAAC;EAChD,CAAC,CAAC;EACF,QAAQ,QAAQ;EAChB,GAAI,QAAQ,UAAU,EAAE,SAAS,QAAQ,QAAQ,IAAI,CAAC;EACtD,GAAI,QAAQ,YAAY,EAAE,WAAW,QAAQ,UAA0C,IAAI,CAAC;CAC9F;CACA,MAAM,YAAY,MAAM,WAAW,SAAS,OAAO,OAAO,GAAG,QAAQ,QAAQ,IAAI;CACjF,IAAI,QAAQ,OAAO,WAAW,UAAU,OAAO,SAAS,WACtD,MAAM,OAAO,uBAAO,IAAI,MAAM,QAAQ,GAAG,EAAE,MAAM,aAAa,CAAC;CAEjE,IAAI,CAAC,KAAK,GAAG,MAAM,OAAO,uBAAO,IAAI,MAAM,QAAQ,GAAG,EAAE,MAAM,aAAa,CAAC;CAC5E,IAAI,UAAU,OAAO,SAAS,SAAS;EACrC,MAAM,OAAO,UAAU,OAAO,QAAQ;EACtC,MAAM,OAAO,OAAO,IAAI,MAAM,UAAU,OAAO,QAAQ,WAAW,SAAS,GAAG,EAAE,KAAK,CAAC;CACxF;CACA,IAAI,UAAU,OAAO,CAAC,CAAC,MAAK,UAAS,MAAM,SAAS,WAAW,KAAK,UAAU,OAAO,SAAS,cAC5F,MAAM,IAAI,MAAM,YAAY;CAE9B,IAAI,UAAU,OAAO,SAAS,QAAQ,MAAM,IAAI,MAAM,UAAU;CAChE,OAAO;EACL,MAAM,SAAS,SAAS;EACxB,UAAU,SAAS,OAAO;EAC1B,OAAO,SAAS,OAAO;EACvB,GAAI,UAAU,QAAQ;GAAE,aAAa,UAAU,MAAM;GAAa,cAAc,UAAU,MAAM;EAAa,IAAI,CAAC;CACpH;AACF"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { ModelRoute } from "./contracts.js";
|
|
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
|
+
/** The stored selection. Empty provider/model means "follow the default". */
|
|
14
|
+
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
|
+
/** 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
|
+
}>;
|
|
40
|
+
//#endregion
|
|
41
|
+
export { ModelCatalogClient, ModelMenuChoice, ModelMenuRoute, modelMenuChoiceKey, modelMenuEffortOptions, modelMenuOverride, normalizeModelMenuRoute, parseModelMenuChoiceKey, parseModelMenuChoices, useModelMenuChoices };
|
|
42
|
+
//# sourceMappingURL=model-menu.d.ts.map
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
//#region src/model-menu.ts
|
|
3
|
+
/**
|
|
4
|
+
* Shared plugin-page model menu.
|
|
5
|
+
*
|
|
6
|
+
* A feature plugin's own settings row exposes a model select. An empty
|
|
7
|
+
* selection means the host default chat model for that feature's own call.
|
|
8
|
+
*
|
|
9
|
+
* Browser-safe and React-optional: pure helpers live here so hosts and clients
|
|
10
|
+
* share one catalog contract without bundling React into the host build.
|
|
11
|
+
*/
|
|
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
|
+
/** Load the host catalog once per mount and republish whenever it answers. */
|
|
117
|
+
function useModelMenuChoices(client, bound) {
|
|
118
|
+
const [choices, setChoices] = useState([]);
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
if (typeof document === "undefined") return;
|
|
121
|
+
let live = true;
|
|
122
|
+
const load = () => {
|
|
123
|
+
client.remote?.session?.modelCatalog?.()?.then((value) => {
|
|
124
|
+
if (live) setChoices(parseModelMenuChoices(value, bound));
|
|
125
|
+
}).catch(() => {
|
|
126
|
+
if (live) setChoices([]);
|
|
127
|
+
});
|
|
128
|
+
};
|
|
129
|
+
load();
|
|
130
|
+
return () => {
|
|
131
|
+
live = false;
|
|
132
|
+
};
|
|
133
|
+
}, [
|
|
134
|
+
client,
|
|
135
|
+
bound?.provider,
|
|
136
|
+
bound?.model
|
|
137
|
+
]);
|
|
138
|
+
return choices;
|
|
139
|
+
}
|
|
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
|
+
//#endregion
|
|
150
|
+
export { modelMenuChoiceKey, modelMenuEffortOptions, modelMenuOverride, normalizeModelMenuRoute, parseModelMenuChoiceKey, parseModelMenuChoices, useModelMenuChoices };
|
|
151
|
+
|
|
152
|
+
//# sourceMappingURL=model-menu.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"model-menu.js","names":[],"sources":["../src/model-menu.ts"],"sourcesContent":["/**\n * Shared plugin-page model menu.\n *\n * A feature plugin's own settings row exposes a model select. An empty\n * selection means the host default chat model for that feature's own call.\n *\n * Browser-safe and React-optional: pure helpers live here so hosts and clients\n * share one catalog contract without bundling React into the host build.\n */\nimport { useEffect, useState } from 'react'\nimport type { ModelRoute } from './contracts.ts'\n\n/** One selectable model. `efforts` are the advertised reasoning efforts. */\nexport interface ModelMenuChoice {\n readonly provider: string\n readonly model: string\n readonly label: string\n readonly efforts: ReadonlyArray<{ id: string; name: string }>\n}\n\n/** The stored selection. Empty provider/model means \"follow the default\". */\nexport type ModelMenuRoute = ModelRoute\n\n/** The catalog face this helper needs; stays structural so hosts may add more. */\nexport interface ModelCatalogClient {\n readonly remote?: {\n readonly session?: { readonly modelCatalog?: () => Promise<unknown> }\n }\n}\n\nconst KEY_SEPARATOR = '\\u001f'\n\nexport function modelMenuChoiceKey(provider: string, model: string): string {\n return provider && model ? `${provider}${KEY_SEPARATOR}${model}` : ''\n}\n\nexport function parseModelMenuChoiceKey(value: string): { provider: string; model: string } | undefined {\n const index = value.indexOf(KEY_SEPARATOR)\n if (index <= 0) return undefined\n const provider = value.slice(0, index)\n const model = value.slice(index + KEY_SEPARATOR.length)\n if (!provider || !model) return undefined\n return { provider, model }\n}\n\nfunction asRecord(value: unknown): Record<string, unknown> | undefined {\n return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : undefined\n}\n\nfunction asText(value: unknown): string | undefined {\n return typeof value === 'string' && value.trim() ? value.trim() : undefined\n}\n\nfunction parseEffort(value: unknown): { id: string; name: string } | undefined {\n const row = asRecord(value)\n const id = row ? asText(row.id) : undefined\n if (!id) return undefined\n return { id, name: row ? asText(row.name) ?? id : id }\n}\n\n/** Parse the host session catalog into flat choices, keeping a saved route selectable. */\nexport function parseModelMenuChoices(value: unknown, bound?: ModelMenuRoute): ModelMenuChoice[] {\n const envelope = asRecord(value)\n // Host remote calls resolve to a result envelope ({ ok, value }); older hosts\n // and unit tests hand over the bare catalog. Accept both.\n const root = (envelope && envelope.ok === true ? asRecord(envelope.value) : undefined) ?? envelope ?? {}\n const groups = Array.isArray(root.groups) ? root.groups : []\n const choices: ModelMenuChoice[] = []\n for (const group of groups) {\n const groupRow = asRecord(group)\n const provider = groupRow ? asText(groupRow.id) : undefined\n if (!provider || !groupRow || !Array.isArray(groupRow.models)) continue\n const providerName = asText(groupRow.name) ?? provider\n for (const entry of groupRow.models) {\n const modelRow = asRecord(entry)\n const model = modelRow ? asText(modelRow.id) : undefined\n if (!model || !modelRow) continue\n const name = asText(modelRow.name) ?? model\n const reasoning = asRecord(modelRow.reasoning)\n const efforts = reasoning && Array.isArray(reasoning.efforts)\n ? reasoning.efforts.flatMap(effort => { const parsed = parseEffort(effort); return parsed ? [parsed] : [] })\n : []\n choices.push({ provider, model, label: `${providerName} / ${name}`, efforts })\n }\n }\n if (bound?.provider && bound.model && !choices.some(item => item.provider === bound.provider && item.model === bound.model)) {\n choices.push({ provider: bound.provider, model: bound.model, label: `${bound.provider} / ${bound.model}`, efforts: [] })\n }\n return choices\n}\n\n/** An empty saved route means no explicit model. */\nexport function modelMenuOverride(route: ModelMenuRoute | undefined): ModelRoute | undefined {\n const provider = route?.provider?.trim()\n const model = route?.model?.trim()\n if (!provider || !model) return undefined\n const effort = route?.reasoningEffort?.trim()\n return effort ? { provider, model, reasoningEffort: effort } : { provider, model }\n}\n\nexport function normalizeModelMenuRoute(value: unknown): ModelMenuRoute {\n const row = asRecord(value)\n if (!row) return { provider: '', model: '' }\n const provider = asText(row.provider) ?? ''\n const model = asText(row.model) ?? ''\n const effort = asText(row.reasoningEffort)\n if (!provider || !model || provider.length > 250 || model.length > 250) return { provider: '', model: '' }\n return effort && effort.length <= 80 ? { provider, model, reasoningEffort: effort } : { provider, model }\n}\n\n/** Load the host catalog once per mount and republish whenever it answers. */\nexport function useModelMenuChoices(client: ModelCatalogClient, bound?: ModelMenuRoute): ModelMenuChoice[] {\n const [choices, setChoices] = useState<ModelMenuChoice[]>([])\n useEffect(() => {\n if (typeof document === 'undefined') return\n let live = true\n const load = () => {\n void client.remote?.session?.modelCatalog?.()\n ?.then(value => { if (live) setChoices(parseModelMenuChoices(value, bound)) })\n .catch(() => { if (live) setChoices([]) })\n }\n load()\n return () => { live = false }\n }, [client, bound?.provider, bound?.model])\n return choices\n}\n\n/** Reasoning-effort options for one choice; the saved value stays selectable. */\nexport function modelMenuEffortOptions(\n choice: ModelMenuChoice | undefined,\n current?: string,\n): Array<{ id: string; name: string }> {\n const listed = (choice?.efforts ?? []).map(item => ({ ...item }))\n if (current && !listed.some(item => item.id === current)) listed.push({ id: current, name: current })\n return listed\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
//#region src/official-ui.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Shared contract that keeps every plugin's browser-side UI on the official
|
|
4
|
+
* DeepSeek Harness Web design language.
|
|
5
|
+
*
|
|
6
|
+
* Two rules produce that language, and both are load-bearing here:
|
|
7
|
+
*
|
|
8
|
+
* 1. Colour, radius, elevation and focus come from the host theme's real
|
|
9
|
+
* `--dsw-*` tokens, written under their own names. Plugins never invent a
|
|
10
|
+
* token, never alias the official ones behind a private palette (a private
|
|
11
|
+
* alias is just a second, diverging theme), and never carries a literal
|
|
12
|
+
* fallback — the host publishes light and dark, and a literal would pin one
|
|
13
|
+
* of them. The only non-colour custom properties used are the host's own
|
|
14
|
+
* content-font axis and frame clearance.
|
|
15
|
+
*
|
|
16
|
+
* 2. Anything the host already ships is used, not reimplemented. Controls come
|
|
17
|
+
* from `@deepseek-ai/dsh-client-ui-primitives` (`Button`, `Input`,
|
|
18
|
+
* `Checkbox`, `Switch`, `Tag`, `Pill`, `SegmentedControl`, `Menu`, `Modal`,
|
|
19
|
+
* `Tooltip`, `Toast`, `DisclosureRow`, `StateDot`, `PathLabel`, the settings
|
|
20
|
+
* form suite). This module only styles what primitives do not cover: page
|
|
21
|
+
* scaffolding, field layout, banners, empty states and scroll regions.
|
|
22
|
+
*
|
|
23
|
+
* The metrics below are transcribed from the primitives' own CSS modules so a
|
|
24
|
+
* feature panel lines up pixel-for-pixel with a native one: the 14/22 body and
|
|
25
|
+
* 13/20 compact type tiers, the 0.5px hairline scale, and the elevation family
|
|
26
|
+
* that draws a surface's edge in its shadow (`border: 0`) instead of a border.
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* Every `--dsw-*` token this repo is allowed to reference, grouped by role.
|
|
30
|
+
*
|
|
31
|
+
* Plugins may only colour through this list. It is exported so the guard test
|
|
32
|
+
* can fail a build when a stylesheet reaches for anything else — the failure
|
|
33
|
+
* mode this prevents is silent: an unknown token resolves to nothing, so a
|
|
34
|
+
* border disappears or a label inherits the wrong colour with no error anywhere.
|
|
35
|
+
*/
|
|
36
|
+
declare const OFFICIAL_THEME_TOKENS: {
|
|
37
|
+
/** Surfaces, from the page up to the topmost nested card. */
|
|
38
|
+
readonly surface: readonly ["--dsw-alias-bg-base", "--dsw-alias-bg-layer-1", "--dsw-alias-bg-layer-2", "--dsw-alias-bg-layer-3", "--dsw-alias-bg-layer-4", "--dsw-alias-bg-module-platform", "--dsw-alias-bg-overlay"];
|
|
39
|
+
/** Hairlines. l1 is near-invisible on a menu surface, so menus use l2+. */
|
|
40
|
+
readonly border: readonly ["--dsw-alias-border-l1", "--dsw-alias-border-l2", "--dsw-alias-border-l3", "--dsw-alias-border-l4"];
|
|
41
|
+
/** Text. `caption` is menu shortcut text, `dimmed` is placeholder text. */
|
|
42
|
+
readonly label: readonly ["--dsw-alias-label-primary", "--dsw-alias-label-secondary", "--dsw-alias-label-tertiary", "--dsw-alias-label-caption", "--dsw-alias-label-dimmed", "--dsw-alias-label-error", "--dsw-alias-label-primary-foreground"];
|
|
43
|
+
/** Interactive fills. The `-danger` hover belongs to destructive rows. */
|
|
44
|
+
readonly interactive: readonly ["--dsw-alias-interactive-bg-hover", "--dsw-alias-interactive-bg-active", "--dsw-alias-interactive-bg-hover-danger"];
|
|
45
|
+
/** Accent and status. `business` is the focus ring's default colour. */
|
|
46
|
+
readonly accent: readonly ["--dsw-alias-brand-primary", "--dsw-alias-button-primary-fill", "--dsw-alias-button-primary-hover", "--dsw-alias-button-tool-bar-fill", "--dsw-alias-button-tool-bar-hover", "--dsw-alias-button-ghost-active-fill", "--dsw-alias-button-ghost-active-border"];
|
|
47
|
+
readonly state: readonly ["--dsw-alias-state-business-primary", "--dsw-alias-state-success-primary", "--dsw-alias-state-warn-primary", "--dsw-alias-state-error-primary", "--dsw-alias-state-idle-primary"];
|
|
48
|
+
/** Corner scale: sm 8, md 12, lg the large card, panel the modal. */
|
|
49
|
+
readonly radius: readonly ["--dsw-radius-sm", "--dsw-radius-md", "--dsw-radius-lg", "--dsw-radius-panel"];
|
|
50
|
+
/** An elevated surface draws its edge in the shadow, so it keeps border: 0. */
|
|
51
|
+
readonly elevation: readonly ["--dsw-elevation-soft", "--dsw-elevation-stroke", "--dsw-elevation-stroke-color", "--dsw-elevation-panel", "--dsw-elevation-prominent"];
|
|
52
|
+
/** Floats, masks and scrollbars. */
|
|
53
|
+
readonly overlay: readonly ["--dsw-menu-surface-fill", "--dsw-menu-backdrop-filter", "--dsw-mask-blur", "--dsw-alias-bg-mask-1", "--dsw-alias-menu-icon", "--dsw-alias-scrollbar-bg-l2", "--dsw-alias-scrollbar-hover-l2", "--dsw-shadow-lv3"];
|
|
54
|
+
/** Motion, focus and type axis, published by the host rather than by a component. */
|
|
55
|
+
readonly system: readonly ["--dsw-focus-ring-width", "--dsw-focus-ring-color", "--dsw-font-markdown-code-font-family", "--ds-transition-duration", "--ds-ease-in-out", "--ds-font-family-code", "--dsh-scrollbar-thumb", "--dsh-scrollbar-thumb-hover", "--dsh-content-font-size-secondary", "--dsh-content-font-delta", "--dsh-frame-top-clearance"];
|
|
56
|
+
};
|
|
57
|
+
/** Every official token name, flat. Used by the token guard test. */
|
|
58
|
+
declare const OFFICIAL_THEME_TOKEN_NAMES: readonly string[];
|
|
59
|
+
/** The elevation steps a surface can be raised by, softest first. */
|
|
60
|
+
type OfficialElevation = 'soft' | 'stroke' | 'panel' | 'prominent';
|
|
61
|
+
/**
|
|
62
|
+
* The host's focus ring shorthand, verbatim. Writing this per control is how
|
|
63
|
+
* rings drift apart; a stylesheet that draws its own focus style should use
|
|
64
|
+
* {@link officialFocus} instead. The colour fallback is the host's own default.
|
|
65
|
+
*/
|
|
66
|
+
declare const OFFICIAL_FOCUS_RING = "var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary))";
|
|
67
|
+
/**
|
|
68
|
+
* A complete `outline` declaration at the official geometry. `offset` defaults
|
|
69
|
+
* to the 2px the native controls use; the host's inline field rules sit at 1px
|
|
70
|
+
* and a segmented tab pulls the ring inside itself at -2px.
|
|
71
|
+
*/
|
|
72
|
+
declare function officialFocus(offset?: string): string;
|
|
73
|
+
/**
|
|
74
|
+
* An elevated surface. The host draws a raised surface's edge inside its
|
|
75
|
+
* shadow rather than as a border, so a card sets `border: 0` and takes the edge
|
|
76
|
+
* from the elevation — which is also why its border stays off the alias-border
|
|
77
|
+
* set that a flat, non-elevated rule would use.
|
|
78
|
+
*/
|
|
79
|
+
declare function officialElevation(level: OfficialElevation): string;
|
|
80
|
+
/**
|
|
81
|
+
* An elevated surface whose edge should read as a specific hairline. Rebinding
|
|
82
|
+
* the stroke colour is how a menu card picks l1 while a panel picks l3, without
|
|
83
|
+
* the two growing apart by hand.
|
|
84
|
+
*/
|
|
85
|
+
declare function officialStrokedElevation(level: OfficialElevation, color: string): string;
|
|
86
|
+
/**
|
|
87
|
+
* The shared stylesheet for a plugin UI, scoped under that plugin's root class.
|
|
88
|
+
*
|
|
89
|
+
* Scoping is what keeps this safe to always-bundle into every client: the rules
|
|
90
|
+
* can only match inside the plugin's own subtree, so two plugins can both mount
|
|
91
|
+
* `dsh-ui-card` without either styling the other's markup.
|
|
92
|
+
*
|
|
93
|
+
* Each recipe matches the root element *and* its descendants, because the
|
|
94
|
+
* commonest arrangement is one element that is both — a surface that carries
|
|
95
|
+
* the root class and `dsh-ui-card`, or a portaled dialog that is the root and
|
|
96
|
+
* the `dsh-ui-surface`. A descendant-only scope would silently leave those
|
|
97
|
+
* unstyled, with no error anywhere.
|
|
98
|
+
*
|
|
99
|
+
* A caveat worth knowing: `Modal` and `Menu` portal to `document.body`, so their
|
|
100
|
+
* content leaves the plugin's subtree entirely. A portaled surface therefore
|
|
101
|
+
* needs its own root class on an element inside the portal.
|
|
102
|
+
*
|
|
103
|
+
* Only patterns that primitives do not provide live here. If a control appears
|
|
104
|
+
* below that `Button`/`Input`/`Switch`/`Tag`/`Menu` already covers, that is a
|
|
105
|
+
* bug in this file: use the primitive and delete the recipe.
|
|
106
|
+
*/
|
|
107
|
+
declare function officialUiCss(roots: string | readonly string[]): string;
|
|
108
|
+
//#endregion
|
|
109
|
+
export { OFFICIAL_FOCUS_RING, OFFICIAL_THEME_TOKENS, OFFICIAL_THEME_TOKEN_NAMES, OfficialElevation, officialElevation, officialFocus, officialStrokedElevation, officialUiCss };
|
|
110
|
+
//# sourceMappingURL=official-ui.d.ts.map
|