dsh-llm-codebuddy-power 1.0.0 → 1.2.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/lib/index.js CHANGED
@@ -1,5 +1,6 @@
1
+ import z from "@deepseek-ai/schemastery";
2
+ import { CONTEXT_WINDOW_EXCEEDED_CODE, EMPTY_RESPONSE_CODE, LlmAdapter, LlmError, ProviderRequestId, QUOTA_EXCEEDED_CODE, ReasoningEffortId, ToolCallId, contentHasImage, isContextWindowExceededError, isQuotaExceededError, resolveImageAttachmentAccess } from "@deepseek-ai/dsh-llm";
1
3
  import { gzipSync } from "node:zlib";
2
- import { CONTEXT_WINDOW_EXCEEDED_CODE, EMPTY_RESPONSE_CODE, LlmAdapter, LlmError, ProviderRequestId, QUOTA_EXCEEDED_CODE, ReasoningEffortId, ToolCallId, contentHasImage, isContextWindowExceededError, isQuotaExceededError } from "@deepseek-ai/dsh-llm";
3
4
  import { randomBytes, randomUUID } from "node:crypto";
4
5
  import { promises } from "node:fs";
5
6
  import { dshHomePath } from "@deepseek-ai/dsh-home-paths";
@@ -7,7 +8,6 @@ import { dirname } from "node:path";
7
8
  import { EventSourceParserStream } from "eventsource-parser/stream";
8
9
  import { defineTool } from "@deepseek-ai/dsh-tools";
9
10
  import { spawn } from "node:child_process";
10
- import z from "@deepseek-ai/schemastery";
11
11
  //#region lib/types/constants.js
12
12
  /**
13
13
  * CodeBuddy 服务的固定事实。
@@ -77,6 +77,14 @@ const REQUEST_IMAGE_DIMENSION_LADDER = [
77
77
  const LOGIN_TIMEOUT_MS = 600 * 1e3;
78
78
  /** 等待浏览器登录完成时的轮询间隔,单位 ms。 */
79
79
  const LOGIN_POLL_INTERVAL_MS = 1e3;
80
+ /**
81
+ * 握手结算后在宿主表里保留结果的时长,单位 ms。
82
+ *
83
+ * 逾期即回收,但**客户端是过一会儿才来问的**(它按自己的间隔轮询),所以结果不能
84
+ * 一结算就丢:一丢,那次本已成功的登录就再也答不出 `done: true`,客户端只能干等到
85
+ * 十分钟截止。保留窗口只需覆盖一个客户端轮询周期,取一个宽裕值。
86
+ */
87
+ const LOGIN_RESULT_RETENTION_MS = 60 * 1e3;
80
88
  /** 含义为“浏览器登录尚未完成”的服务码。 */
81
89
  const AUTH_PENDING_CODE = 11217;
82
90
  //#endregion
@@ -1005,7 +1013,7 @@ async function updateSessionInfo(sessionId, change) {
1005
1013
  * (`$DSH_HOME/data/plugins/llm-codebuddy-power/`,通过 harness 所用的同一个
1006
1014
  * `@deepseek-ai/dsh-home-paths` 解析),而不放在插件包内,因此重装不会让
1007
1015
  * 用户退出登录。写入经 [atomic-file.ts](./atomic-file.ts) 做原子替换并按路径串行化:
1008
- * 多个账号的令牌刷新、设置页的增删与 CLI 的调用都在改同一份文件,不串行化时
1016
+ * 多个账号的令牌刷新、配置页的增删与 CLI 的调用都在改同一份文件,不串行化时
1009
1017
  * 两个并发写入会各自基于自己读到的旧内容覆盖对方。残缺文件会让用户手里只有
1010
1018
  * 一份读不出的凭据,还无法与“从未登录过”区分开。
1011
1019
  *
@@ -1153,7 +1161,7 @@ async function updateStorage(change) {
1153
1161
  * 插入一个账号,或替换同 uid 账号的凭据。
1154
1162
  *
1155
1163
  * 同 uid 视为重新登录同一个账号:令牌被替换而不是追加成第二个条目,否则
1156
- * 设置页会出现两个同名徽章,退出其中一个还会留下另一个。
1164
+ * 配置页会出现两个同名徽章,退出其中一个还会留下另一个。
1157
1165
  * @param entry - 刚登录得到的账号。
1158
1166
  */
1159
1167
  async function upsertAccount(entry) {
@@ -1774,7 +1782,7 @@ var CodeBuddySession = class {
1774
1782
  return (this.storage?.accounts.length ?? 0) > 0;
1775
1783
  }
1776
1784
  /**
1777
- * 已存账号及其默认账号,供设置页与会话切换按钮展示。
1785
+ * 已存账号及其默认账号,供配置页与会话切换按钮展示。
1778
1786
  * @returns 账号列表(顺序与磁盘一致)与默认账号 uid;未登录时列表为空。
1779
1787
  */
1780
1788
  async accountList() {
@@ -1922,7 +1930,7 @@ var CodeBuddySession = class {
1922
1930
  /**
1923
1931
  * 目录;读不到时返回空列表。
1924
1932
  *
1925
- * 列出模型是设置页上的浏览动作,所以失败必须降级为“没有可显示的内容”,
1933
+ * 列出模型是配置页上的浏览动作,所以失败必须降级为“没有可显示的内容”,
1926
1934
  * 而不是让页面崩掉。请求路径直接使用 {@link models} 并保留真实失败。
1927
1935
  * @param signal - 可选取消信号。
1928
1936
  * @param sessionId - 用哪个会话的账号;缺省时用默认账号。
@@ -1998,12 +2006,12 @@ async function* parseSse(stream, onComment) {
1998
2006
  * 把 harness 消息序列化为 CodeBuddy(OpenAI 兼容)chat 请求。
1999
2007
  *
2000
2008
  * 用户文本被拼接,助手文本成为 `content`,工具调用成为
2001
- * `tool_calls`,每个工具结果成为其自身的 `role: 'tool'` 消息 →
2002
- * harness 把工具结果放在用户消息内部,而这条 wire 路由
2003
- * 不接受这种形式。对于支持图像的模型,图像以 OpenAI 兼容的
2004
- * `image_url` 部件(`data:<type>;base64,...`)出现在用户或工具 content
2005
- * 数组中,与官方 CLI 的发送方式完全一致。工具结果的图只对 `read_image`
2006
- * 这类"看图"工具发送,生成图工具的结果仍走纯文本信封。
2009
+ * `tool_calls`,每个工具结果是一条 `role: 'tool'` 的 harness 消息,直接映射为
2010
+ * 同名 wire 条目。对于支持图像的模型,用户消息里的图**不内联**:改为给出一段
2011
+ * 引导模型自行用 `read_image` 读取的提示词(附件服务解析出的只读绝对路径);
2012
+ * 解析不出路径时退回内联,以免模型既拿不到图也拿不到路径。`read_image` 的工具
2013
+ * 结果仍内联图片本身 —— 那是像素交给模型的唯一途径。生成图工具的结果继续走
2014
+ * 纯文本信封。
2007
2015
  *
2008
2016
  * @module dsh-llm-codebuddy-power/serialize
2009
2017
  */
@@ -2015,6 +2023,77 @@ function flattenText(blocks) {
2015
2023
  function imageDataUrl(version) {
2016
2024
  return `data:${version.mediaType};base64,${Buffer.from(version.data).toString("base64")}`;
2017
2025
  }
2026
+ /** PTC 模式下模型只能直呼 `run_code`,其余工具经生成的 SDK 以 `tools.<name>` 调用。 */
2027
+ const RUN_CODE_TOOL = "run_code";
2028
+ /** PTC 模式 SDK 中读取图片的绑定名。 */
2029
+ const PTC_READ_IMAGE_BINDING = "tools.read_image";
2030
+ /** 产出需要模型看到的图的那个工具:`read_image`。 */
2031
+ const READ_IMAGE_TOOL = "read_image";
2032
+ /**
2033
+ * 本请求中模型能否按提示词自行读取一张图。
2034
+ *
2035
+ * 三种形态分开处理,因为"能不能读"取决于该会话实际暴露的工具,而不只是路径是否
2036
+ * 可解析:
2037
+ * - `'direct'` —— 工具目录里有 `read_image`,可直接调用;标准模式即此。
2038
+ * - `'ptc'` —— 目录里只有 `run_code`(PTC 模式把其余工具放进生成的 SDK,
2039
+ * `read_image` 只是 `tools.read_image` 绑定),提示词须指明经代码调用。
2040
+ * - `'none'` —— 没有可用的读图途径(极简模式只有持久 shell)。此形态不引导,
2041
+ * 因为那会让模型去调一个不存在的工具,比内联更糟;而极简模式的模型尚未形成
2042
+ * "自己不能看图"的想法(标准模式那个想法来自大段系统提示词与 `read_image`
2043
+ * 描述里的 `Requires the current model to accept image input.`),内联即可。
2044
+ * @param tools - 本次请求的 wire 工具目录。
2045
+ * @returns 本请求的读图形态。
2046
+ */
2047
+ function readImageRoute(tools) {
2048
+ const names = new Set((tools ?? []).map((tool) => tool.name));
2049
+ if (names.has(READ_IMAGE_TOOL)) return "direct";
2050
+ if (names.has(RUN_CODE_TOOL)) return "ptc";
2051
+ return "none";
2052
+ }
2053
+ /**
2054
+ * 一张用户消息里的图:有可用的读图途径时只给"自行读取"的提示词,不再内联图片;
2055
+ * 否则退回内联。
2056
+ *
2057
+ * 不内联的理由:网关对非原生多模态模型的兼容路径把图转成文字描述,同时在同一条
2058
+ * 链路上附一句"内容已过滤"的错误声明。图随用户消息到达时,模型会采信那句声明并
2059
+ * 否认看图;改由模型自己调用 `read_image` 取回后,图是它自己取回的返回内容,它
2060
+ * 不再采信那句声明。退回内联则是为了不让模型既拿不到图、又被指去一个不存在的工具。
2061
+ * @param ref - 该次出现的持久化附件引用。
2062
+ * @param version - 该图本次请求已解析的版本,仅在退回内联时使用。
2063
+ * @param resolveAccess - 解析该图在当前执行世界中的只读路径。
2064
+ * @param route - 本请求的读图形态。
2065
+ * @returns 该图的读取提示词,或退回内联时的 image_url 部件。
2066
+ */
2067
+ function userImageParts(ref, version, resolveAccess, route) {
2068
+ const access = resolveAccess?.(ref);
2069
+ if (route === "none" || access === void 0) return [inlineImagePart(version)];
2070
+ return [{
2071
+ type: "text",
2072
+ text: imageReadPrompt(ref, access, route)
2073
+ }];
2074
+ }
2075
+ /** 内联 base64 图部件。工具结果的像素只能这样交给模型。 */
2076
+ function inlineImagePart(version) {
2077
+ return {
2078
+ type: "image_url",
2079
+ image_url: { url: imageDataUrl(version) }
2080
+ };
2081
+ }
2082
+ /**
2083
+ * 组装引导模型自行读取该图的提示词。
2084
+ *
2085
+ * 只陈述路径与动作:图并未内联,任何形如"已发送的预览版本"的措辞都会让模型把
2086
+ * 这段话当成图的替代说明,而不是去读取它的指令。
2087
+ * @param ref - 该次出现的持久化附件引用,身份取自它。
2088
+ * @param access - 该图在当前执行世界中的只读路径。
2089
+ * @param route - 本请求的读图形态,决定用哪种调用方式。
2090
+ * @returns 面向模型的读取提示词。
2091
+ */
2092
+ function imageReadPrompt(ref, access, route) {
2093
+ const identity = ref.name === void 0 ? String(ref.attachmentId) : `${JSON.stringify(ref.name)} (${ref.attachmentId})`;
2094
+ const call = route === "ptc" ? `Call it from your code as ${PTC_READ_IMAGE_BINDING}({ file_path: <path> }).` : `Call the ${READ_IMAGE_TOOL} tool with that path to view it.`;
2095
+ return `[Image ${identity} is at the read-only path ${JSON.stringify(access.readonlyPath)}. ${call} This conversation delivers images through that tool, so read the image yourself before answering about it.]`;
2096
+ }
2018
2097
  /**
2019
2098
  * 在任何文本扁平化可能静默丢弃图像内容之前先拒绝它。
2020
2099
  * @param blocks - 消息内容。
@@ -2026,12 +2105,14 @@ function assertSupportedContent(blocks, supportsImages) {
2026
2105
  /**
2027
2106
  * 序列化一条用户消息中非工具结果的块。纯文本内容保持紧凑的
2028
2107
  * 字符串 wire 形式;含图像的内容变为 OpenAI 兼容的
2029
- * 部件数组,并保持块顺序。
2108
+ * 部件数组(图可按读图形态换成读取提示词),并保持块顺序。
2030
2109
  * @param blocks - 消息中非工具结果的块。
2031
2110
  * @param images - 请求中每个图像已解析的请求版本。
2111
+ * @param resolveAccess - 解析单张图在当前执行世界中的只读路径。
2112
+ * @param route - 本请求的读图形态。
2032
2113
  * @returns 用户角色的 wire 内容。
2033
2114
  */
2034
- function userContent(blocks, images) {
2115
+ function userContent(blocks, images, resolveAccess, route) {
2035
2116
  if (!contentHasImage(blocks)) return flattenText(blocks);
2036
2117
  const parts = [];
2037
2118
  let text = "";
@@ -2052,10 +2133,7 @@ function userContent(blocks, images) {
2052
2133
  const version = images.get(block.attachment.attachmentId);
2053
2134
  if (version === void 0) throw new LlmError(`CodeBuddy request image ${block.attachment.attachmentId} was not prepared.`, "INVALID_REQUEST");
2054
2135
  flush();
2055
- parts.push({
2056
- type: "image_url",
2057
- image_url: { url: imageDataUrl(version) }
2058
- });
2136
+ parts.push(...userImageParts(block.attachment, version, resolveAccess, route));
2059
2137
  }
2060
2138
  flush();
2061
2139
  return parts;
@@ -2081,16 +2159,24 @@ function serializeAssistant(message) {
2081
2159
  }
2082
2160
  /**
2083
2161
  * 按顺序序列化整个对话。
2162
+ *
2163
+ * 工具结果在 harness 里是**独立的 `role: 'tool'` 消息**(`ToolResultMessage`),
2164
+ * 因此这里逐条映射,而不是按块从 user 消息里筛选。
2165
+ * developer 消息由运行时在 `projectToolUpdates` 中剥离(本 adapter 未声明
2166
+ * `toolUpdate`),因此不会到达这里;万一到达即明确拒绝,而不是静默丢弃。
2084
2167
  * @param messages - harness 会话。
2085
2168
  * @param supportsImages - 所选模型是否声明支持图像输入。
2086
2169
  * @param images - 请求中每个图像已解析的请求版本。
2087
- * @returns wire 消息,每个工具结果展开为独立条目。
2170
+ * @param resolveAccess - 解析单张图在当前执行世界中的只读路径。
2171
+ * @param route - 本请求的读图形态。
2172
+ * @returns wire 消息,工具结果各自成为独立条目。
2088
2173
  */
2089
- function serializeMessages(messages, supportsImages, images = /* @__PURE__ */ new Map()) {
2174
+ function serializeMessages(messages, supportsImages, images = /* @__PURE__ */ new Map(), resolveAccess, route = "none") {
2090
2175
  const wire = [];
2091
2176
  const toolNames = collectToolNames(messages);
2092
2177
  for (const message of messages) {
2093
2178
  assertSupportedContent(message.content, supportsImages);
2179
+ if (message.role === "developer") throw new LlmError("CodeBuddy chat does not support developer messages.", "UNSUPPORTED_CONTENT");
2094
2180
  if (message.role === "system") {
2095
2181
  wire.push({
2096
2182
  role: "system",
@@ -2102,47 +2188,55 @@ function serializeMessages(messages, supportsImages, images = /* @__PURE__ */ ne
2102
2188
  wire.push(serializeAssistant(message));
2103
2189
  continue;
2104
2190
  }
2105
- const toolResults = message.content.filter((block) => block.type === "tool-result");
2106
- const content = userContent(message.content.filter((block) => block.type !== "tool-result"), images);
2107
- if (content.length > 0 || toolResults.length === 0) wire.push({
2191
+ if (message.role === "tool") {
2192
+ wire.push(serializeToolResult(message, supportsImages, images, toolNames));
2193
+ continue;
2194
+ }
2195
+ const routeForMessage = message.source?.kind === "user" ? route : "none";
2196
+ wire.push({
2108
2197
  role: "user",
2109
- content
2198
+ content: userContent(message.content, images, resolveAccess, routeForMessage)
2110
2199
  });
2111
- for (const result of toolResults) {
2112
- const text = flattenText(result.content);
2113
- const imageBlocks = result.content.filter((block) => block.type === "image");
2114
- if (!(supportsImages && imageBlocks.length > 0 && toolNames.get(result.toolCallId) === READ_IMAGE_TOOL)) {
2115
- wire.push({
2116
- role: "tool",
2117
- tool_call_id: result.toolCallId,
2118
- content: text.length === 0 ? "(no output)" : text
2119
- });
2120
- continue;
2121
- }
2122
- const parts = [];
2123
- if (text.length > 0) parts.push({
2124
- type: "text",
2125
- text
2126
- });
2127
- for (const block of imageBlocks) {
2128
- const version = images.get(block.attachment.attachmentId);
2129
- if (version === void 0) throw new LlmError(`CodeBuddy request image ${block.attachment.attachmentId} was not prepared.`, "INVALID_REQUEST");
2130
- parts.push({
2131
- type: "image_url",
2132
- image_url: { url: imageDataUrl(version) }
2133
- });
2134
- }
2135
- wire.push({
2136
- role: "tool",
2137
- tool_call_id: result.toolCallId,
2138
- content: parts
2139
- });
2140
- }
2141
2200
  }
2142
2201
  return wire;
2143
2202
  }
2144
- /** 产出需要模型看到的图的那个工具:`read_image`。 */
2145
- const READ_IMAGE_TOOL = "read_image";
2203
+ /**
2204
+ * 序列化一条工具结果消息。
2205
+ *
2206
+ * 工具结果的图是否发给模型取决于**产出它的工具**:`read_image` 的存在意义就是让
2207
+ * 模型看图,必须发送;`codebuddy-draw` 只需模型知道"图已生成"(与官方 image_gen
2208
+ * 的纯文本信封一致),发图纯属浪费 token。实测确认服务端接受 tool 消息里的
2209
+ * image_url,因此直接放进该条目的 content 数组。
2210
+ * @param message - 工具结果消息。
2211
+ * @param supportsImages - 所选模型是否声明支持图像输入。
2212
+ * @param images - 请求中每个图像已解析的请求版本。
2213
+ * @param toolNames - toolCallId → 工具名 的索引。
2214
+ * @returns 该工具结果的 wire 条目。
2215
+ */
2216
+ function serializeToolResult(message, supportsImages, images, toolNames) {
2217
+ const text = flattenText(message.content);
2218
+ const imageBlocks = message.content.filter((block) => block.type === "image");
2219
+ if (!(supportsImages && imageBlocks.length > 0 && toolNames.get(message.toolCallId) === READ_IMAGE_TOOL)) return {
2220
+ role: "tool",
2221
+ tool_call_id: message.toolCallId,
2222
+ content: text.length === 0 ? "(no output)" : text
2223
+ };
2224
+ const parts = [];
2225
+ if (text.length > 0) parts.push({
2226
+ type: "text",
2227
+ text
2228
+ });
2229
+ for (const block of imageBlocks) {
2230
+ const version = images.get(block.attachment.attachmentId);
2231
+ if (version === void 0) throw new LlmError(`CodeBuddy request image ${block.attachment.attachmentId} was not prepared.`, "INVALID_REQUEST");
2232
+ parts.push(inlineImagePart(version));
2233
+ }
2234
+ return {
2235
+ role: "tool",
2236
+ tool_call_id: message.toolCallId,
2237
+ content: parts
2238
+ };
2239
+ }
2146
2240
  /**
2147
2241
  * 建立 toolCallId → 工具名 的索引。
2148
2242
  *
@@ -2166,15 +2260,17 @@ function collectToolNames(messages) {
2166
2260
  * @param options - 组装好的 harness 请求。
2167
2261
  * @param supportsImages - 所选模型是否声明支持图像输入。
2168
2262
  * @param images - 请求中每个图像已解析的请求版本。
2263
+ * @param resolveAccess - 解析单张图在当前执行世界中的只读路径。
2264
+ * @param route - 本请求的读图形态,由工具目录决定;写入前由调用方计算一次。
2169
2265
  * @returns 请求体。
2170
2266
  */
2171
- function serializeRequest(options, supportsImages, images = /* @__PURE__ */ new Map()) {
2267
+ function serializeRequest(options, supportsImages, images = /* @__PURE__ */ new Map(), resolveAccess) {
2172
2268
  const messages = [];
2173
2269
  if (options.system !== void 0) messages.push({
2174
2270
  role: "system",
2175
2271
  content: options.system
2176
2272
  });
2177
- messages.push(...serializeMessages(options.messages, supportsImages, images));
2273
+ messages.push(...serializeMessages(options.messages, supportsImages, images, resolveAccess, readImageRoute(options.tools)));
2178
2274
  const tools = options.tools?.map((tool) => ({
2179
2275
  type: "function",
2180
2276
  function: {
@@ -2518,48 +2614,45 @@ function requestId(headers) {
2518
2614
  const value = headers.get("x-request-id") ?? headers.get("x-requestid");
2519
2615
  return value === null || value.length === 0 ? void 0 : ProviderRequestId(value);
2520
2616
  }
2521
- /** 收集单条消息内容中的全部图像引用,包括工具结果内的引用。 */
2617
+ /** 收集单条消息内容中的全部图像引用。 */
2522
2618
  function collectImageRefs(content, refs) {
2523
2619
  for (const block of content) if (block.type === "image") refs.set(block.attachment.attachmentId, block.attachment);
2524
- else if (block.type === "tool-result") collectImageRefs(block.content, refs);
2525
2620
  }
2526
2621
  /**
2527
- * 把"长边不超过 longEdge"折算为该图的总像素预算。
2528
- *
2529
- * 请求投影只接受总像素上限(`ImageRequestPolicy.maxPixels`),没有长边字段,
2530
- * 而总像素无法约束长边:8000×460 仅 368 万像素,长边却达 8000。投影按
2531
- * `sqrt(maxPixels / (width * height))` 等比缩放,因此令
2532
- * `maxPixels = longEdge² × (短边 / 长边)` 时缩放系数恰为 `longEdge / 长边`,
2533
- * 结果正好把长边压到该档。
2622
+ * 把"长边不超过 longEdge"折算为该图的目标尺寸。
2534
2623
  *
2535
- * 长边本就在档内时返回原图总像素。这**不是**防放大所必需的:投影自身以
2536
- * `min(1, …)` 截断缩放系数,且长边 ≤ longEdge 时恒有
2537
- * `longEdge² × (短边/长边) ≥ 原面积`,故照公式给值也不会被放大。返回原图
2538
- * 面积的作用是让**预算值在各档之间重复**,从而被逐档重试的去重跳过,避免
2539
- * 对同一尺寸重复编码。
2624
+ * 请求投影接受的是目标宽高(`ImageRequestTarget`),因此长边档位直接折算成一对
2625
+ * 宽高,而不必再换算成总像素预算。目标大于源图时投影保持源图尺寸(不放大),
2626
+ * 所以长边本就在档内时直接给源图宽高。
2540
2627
  *
2541
2628
  * 宽高由会话格式的载荷校验强制为正整数(`payload-validation.ts` 的
2542
2629
  * `imageAttachmentValue`),故此处不做防御检查。
2543
2630
  * @param width - 原图宽度(像素)。
2544
2631
  * @param height - 原图高度(像素)。
2545
2632
  * @param longEdge - 本档的长边上限(像素)。
2546
- * @returns 该图在该档下的 `maxPixels` 取值,恒为正整数。
2633
+ * @returns 该图在该档下的目标宽高。
2547
2634
  */
2548
- function pixelBudgetForLongEdge(width, height, longEdge) {
2635
+ function dimensionsForLongEdge(width, height, longEdge) {
2549
2636
  const source = Math.max(width, height);
2550
- if (source <= longEdge) return width * height;
2637
+ if (source <= longEdge) return {
2638
+ width,
2639
+ height
2640
+ };
2551
2641
  const scale = longEdge / source;
2552
- return Math.max(1, Math.round(width * scale)) * Math.max(1, Math.round(height * scale));
2642
+ return {
2643
+ width: Math.max(1, Math.round(width * scale)),
2644
+ height: Math.max(1, Math.round(height * scale))
2645
+ };
2553
2646
  }
2554
2647
  /**
2555
2648
  * 解析一张图的请求版本:源图未超字节目标时原样复用,超过时才逐档收紧长边。
2556
2649
  *
2557
- * 未超目标的一支不做任何转换:`maxPixels` 取原图总像素,投影因此不缩放,附件
2558
- * 服务命中其直通分支返回原始字节,既不重编码也不写请求缓存 —— 降到 1920 会
2559
- * 白白丢掉分辨率与画质,而模型侧的约束只有字节目标。
2650
+ * 未超目标的一支不做任何转换:目标取原图宽高,投影因此不缩放,附件服务命中其
2651
+ * 直通分支返回原始字节,既不重编码也不写请求缓存 —— 降到 1920 会白白丢掉分辨率
2652
+ * 与画质,而模型侧的约束只有字节目标。
2560
2653
  *
2561
2654
  * 超过目标的一支才逐档收紧长边。附件服务的编码阶梯只降质量、不降尺寸,单靠
2562
- * 一档无法保证落到字节目标,故需要换更小的长边重来;预算相同的档位会被跳过。
2655
+ * 一档无法保证落到字节目标,故需要换更小的长边重来;尺寸相同的档位会被跳过。
2563
2656
  * @param ref - 持久化附件引用。
2564
2657
  * @param attachments - 已挂载的附件服务。
2565
2658
  * @param signal - 本次生成的取消信号。
@@ -2568,17 +2661,19 @@ function pixelBudgetForLongEdge(width, height, longEdge) {
2568
2661
  async function requestImageVersion(ref, attachments, signal) {
2569
2662
  const maxBytes = REQUEST_IMAGE_MAX_BYTES;
2570
2663
  if (ref.bytes <= maxBytes) return attachments.readImageRequest(ref, {
2571
- maxPixels: ref.width * ref.height,
2664
+ width: ref.width,
2665
+ height: ref.height,
2572
2666
  maxBytes
2573
2667
  }, signal);
2574
2668
  const tried = /* @__PURE__ */ new Set();
2575
2669
  let smallest;
2576
2670
  for (const longEdge of REQUEST_IMAGE_DIMENSION_LADDER) {
2577
- const maxPixels = pixelBudgetForLongEdge(ref.width, ref.height, longEdge);
2578
- if (tried.has(maxPixels)) continue;
2579
- tried.add(maxPixels);
2671
+ const size = dimensionsForLongEdge(ref.width, ref.height, longEdge);
2672
+ const key = `${size.width}x${size.height}`;
2673
+ if (tried.has(key)) continue;
2674
+ tried.add(key);
2580
2675
  const version = await attachments.readImageRequest(ref, {
2581
- maxPixels,
2676
+ ...size,
2582
2677
  maxBytes
2583
2678
  }, signal);
2584
2679
  if (version.bytes <= maxBytes) return version;
@@ -2588,7 +2683,7 @@ async function requestImageVersion(ref, attachments, signal) {
2588
2683
  }
2589
2684
  /**
2590
2685
  * 在所选模型接受图像输入时,解析会话所携带每张图像的请求版本。持久化 harness 以引用
2591
- * 方式附加;请求字节由附件服务按逐图的像素与字节预算派生,与官方 DeepSeek 适配器
2686
+ * 方式附加;请求字节由附件服务按逐图的目标尺寸与字节预算派生,与官方 DeepSeek 适配器
2592
2687
  * 的准备方式一致。
2593
2688
  * @param messages - 等待序列化的 harness 消息。
2594
2689
  * @param attachments - 持久化附件服务;未挂载时为 undefined。
@@ -2738,7 +2833,8 @@ var CodeBuddyAdapter = class extends LlmAdapter {
2738
2833
  const entry = (await this.config.session.modelsOrEmpty(options.signal, sessionId)).find((candidate) => candidate.id === options.model);
2739
2834
  const supportsImages = entry?.supportsImages === true;
2740
2835
  if (options.tools !== void 0 && options.tools.length > 0 && entry?.supportsToolCall === false) throw new LlmError(`CodeBuddy model "${options.model}" does not support tool calls`, "UNSUPPORTED_OPTION");
2741
- const body = serializeRequest(options, supportsImages, supportsImages ? await prepareRequestImages(options.messages, this.config.resolveAttachments(), options.signal) : /* @__PURE__ */ new Map());
2836
+ const attachments = this.config.resolveAttachments();
2837
+ const body = serializeRequest(options, supportsImages, supportsImages ? await prepareRequestImages(options.messages, attachments, options.signal) : /* @__PURE__ */ new Map(), attachments === void 0 || this.config.resolveImageAccess === void 0 ? void 0 : (ref) => this.config.resolveImageAccess?.(attachments, ref));
2742
2838
  const payload = gzipSync(Buffer.from(JSON.stringify(body), "utf8"));
2743
2839
  const messageId = compactUuid();
2744
2840
  const conversationId = options.sessionId === void 0 ? compactUuid() : this.config.session.conversationIdOf(String(options.sessionId));
@@ -3026,8 +3122,9 @@ function endpointOf(channel, pathname) {
3026
3122
  * @param res - 本路由在整个生命周期内持有的响应。
3027
3123
  */
3028
3124
  async function serveChannel(rpcCtx, channel, handler, req, res) {
3029
- const rejection = rpcCtx.connection.requestRejection(req);
3030
- if (rejection !== void 0) {
3125
+ const admission = rpcCtx.connection.admit(req);
3126
+ if ("rejection" in admission) {
3127
+ const rejection = admission.rejection;
3031
3128
  res.writeHead(rejection);
3032
3129
  res.end(rejection === 401 ? "unauthorized" : "forbidden");
3033
3130
  return;
@@ -3085,7 +3182,7 @@ async function serveChannel(rpcCtx, channel, handler, req, res) {
3085
3182
  res.on("close", () => {
3086
3183
  controller.abort();
3087
3184
  });
3088
- const result = await handler(endpoint, message.payload, controller.signal);
3185
+ const result = await handler(endpoint, message.payload, controller.signal, admission.peer);
3089
3186
  writeJson(res, 200, serverResponse(message.rpcId, result));
3090
3187
  } catch (error) {
3091
3188
  res.writeHead(500);
@@ -3451,10 +3548,11 @@ function enrichModel(model, promotions, tiers, withOverrides) {
3451
3548
  /**
3452
3549
  * 通过私有 RPC 通道暴露给 Web 客户端的宿主侧 OAuth 服务。
3453
3550
  *
3454
- * 浏览器登录是长时操作(要等待用户完成登录),因此拆成两个 RPC endpoint:
3455
- * `startLogin` 生成握手并返回用户必须打开的 URL,`pollLogin` 检查该握手是否已完成;
3551
+ * 浏览器登录是长时操作(要等待用户完成登录),因此拆成三个 RPC endpoint:
3552
+ * `startLogin` 生成握手并返回用户必须打开的 URL,`pollLogin` 检查该握手是否已完成,
3553
+ * `pendingLogin` 报出仍在轮询的握手(浏览器刷新后据此接回);
3456
3554
  * 一次完成的登录会**追加**一个账号,而不是取代已有的。`accounts`、`activate`、
3457
- * `setDefault`、`logout` 是设置页与会话账号按钮其余时间驱动的读写组合。
3555
+ * `setDefault`、`logout` 是配置页与会话账号按钮其余时间驱动的读写组合。
3458
3556
  *
3459
3557
  * @module dsh-llm-codebuddy-power/auth-service
3460
3558
  */
@@ -3525,13 +3623,18 @@ function projectWindow(window) {
3525
3623
  /**
3526
3624
  * CodeBuddy 认证 RPC 服务。
3527
3625
  *
3528
- * 握手由 `startLogin` 发起,由 `pollLogin` 轮询至完成,其账号由适配器的
3529
- * `CodeBuddySession` 在下一个请求中取用 → 因此通过 UI 完成的登录无需重启
3530
- * 即可到达运行中的 harness。`logout` 移除指定账号并使会话缓存失效。
3626
+ * 握手由 `startLogin` 发起并**脱手**开跑,客户端经 `pollLogin` 反复查问它是否已结算,
3627
+ * 其账号由适配器的 `CodeBuddySession` 在下一个请求中取用 → 因此通过 UI 完成的登录
3628
+ * 无需重启即可到达运行中的 harness。`logout` 移除指定账号并使会话缓存失效。
3531
3629
  */
3532
3630
  var CodeBuddyAuthService = class {
3533
3631
  session;
3534
- /** 按 state id 索引的进行中握手。 */
3632
+ /**
3633
+ * 按 state id 索引的握手。
3634
+ *
3635
+ * 同时存放进行中的与**刚结算、尚在保留窗口内**的条目 —— 后者供客户端取回结果,
3636
+ * 由 `settle()` 安排回收,因此本表不是"进行中握手"的集合。
3637
+ */
3535
3638
  pending = /* @__PURE__ */ new Map();
3536
3639
  constructor(ctx, session) {
3537
3640
  this.session = session;
@@ -3542,7 +3645,8 @@ var CodeBuddyAuthService = class {
3542
3645
  switch (endpoint) {
3543
3646
  case "accounts": return ok(await this.accounts());
3544
3647
  case "startLogin": return ok(await this.startLogin());
3545
- case "pollLogin": return ok(await this.pollLogin(stringField(payload, "state")));
3648
+ case "pollLogin": return ok(this.pollLogin(stringField(payload, "state")));
3649
+ case "pendingLogin": return ok(this.pendingLogin());
3546
3650
  case "cancelLogin": return this.cancelLogin(stringField(payload, "state")) ? ok(void 0) : err("bad-request", "a login handshake id is required");
3547
3651
  case "sessionAccount": return ok(await this.sessionAccount(stringField(payload, "sessionId")));
3548
3652
  case "activate": return await this.activate(stringField(payload, "sessionId"), stringField(payload, "uid")) ? ok(void 0) : err("unknown-account", "the requested account is not stored");
@@ -3581,37 +3685,70 @@ var CodeBuddyAuthService = class {
3581
3685
  }
3582
3686
  /**
3583
3687
  * 发起一次浏览器登录握手。
3584
- * @returns 用户必须打开的 URL。
3688
+ * @returns 用户必须打开的 URL,以及该握手的 id 与发起时刻。
3585
3689
  */
3586
3690
  async startLogin() {
3587
3691
  const handshake = await requestAuthState();
3588
3692
  const controller = new AbortController();
3693
+ const startedAt = Date.now();
3589
3694
  const pending = {
3590
3695
  state: handshake.state,
3591
- promise: this.runLogin(handshake.state, controller.signal),
3696
+ startedAt,
3697
+ account: void 0,
3698
+ settled: false,
3592
3699
  controller
3593
3700
  };
3594
3701
  this.pending.set(handshake.state, pending);
3595
- pending.promise.finally(() => {
3596
- if (this.pending.get(handshake.state) === pending) this.pending.delete(handshake.state);
3597
- });
3702
+ this.runLogin(handshake.state, controller.signal).catch(() => {});
3598
3703
  return {
3599
3704
  authUrl: handshake.authUrl,
3600
- state: handshake.state
3705
+ state: handshake.state,
3706
+ startedAt
3707
+ };
3708
+ }
3709
+ /**
3710
+ * 仍在进行的握手。
3711
+ *
3712
+ * 客户端的「登录中」状态只活在浏览器内存里,刷新即丢,而本表里的握手仍在轮询,
3713
+ * 浏览器重新挂载后据此接回(否则界面显示可再次登录,而宿主那边已有一次握手在等,
3714
+ * 用户点了就会平白多出第二次)。
3715
+ *
3716
+ * **只报尚未结算的握手**:已结算的条目会在表里多留一会儿供客户端取结果,若把它们
3717
+ * 也算作"进行中",刷新后接回的就是一次已经结束(甚至已经失败)的登录,界面会重新
3718
+ * 显示「登录中…」并空转到截止时间。
3719
+ *
3720
+ * 同时存在多次时给出最近发起的那次:客户端的轮询、取消与超时都只针对一个握手,
3721
+ * 而最晚发起的那次正是用户此刻在等的那次。
3722
+ * @returns 该握手;没有进行中的登录时缺省。
3723
+ */
3724
+ pendingLogin() {
3725
+ let latest;
3726
+ for (const pending of this.pending.values()) {
3727
+ if (pending.settled) continue;
3728
+ if (latest === void 0 || pending.startedAt > latest.startedAt) latest = pending;
3729
+ }
3730
+ return latest === void 0 ? void 0 : {
3731
+ state: latest.state,
3732
+ startedAt: latest.startedAt
3601
3733
  };
3602
3734
  }
3603
3735
  /**
3604
3736
  * 检查已发起的握手是否完成。
3737
+ *
3738
+ * **立即返回**,不等待后台握手:`runLogin` 可能还要跑满十分钟,若在这里 await 它,
3739
+ * 浏览器的一次请求就会在整个登录窗口内停在待处理,占住一条连接不放;而客户端是
3740
+ * 按自己的间隔反复来问的,让它每次都立刻拿到"还没好"才是这条 endpoint 的职责。
3605
3741
  * @param state - 来自 `startLogin` 的握手 id。
3606
3742
  * @returns 登录是否完成、账号是否已持久化。
3607
3743
  */
3608
- async pollLogin(state) {
3744
+ pollLogin(state) {
3609
3745
  const pending = this.pending.get(state);
3610
3746
  if (pending === void 0) return { done: false };
3611
- const entry = await pending.promise;
3747
+ if (!pending.settled) return { done: false };
3748
+ const account = pending.account;
3612
3749
  return {
3613
- done: entry !== void 0,
3614
- ...entry === void 0 ? {} : { nickname: entry.account.nickname }
3750
+ done: account !== void 0,
3751
+ ...account === void 0 ? {} : { nickname: account.account.nickname }
3615
3752
  };
3616
3753
  }
3617
3754
  /**
@@ -3714,23 +3851,45 @@ var CodeBuddyAuthService = class {
3714
3851
  };
3715
3852
  }
3716
3853
  /**
3717
- * 把一次握手推进到持久化的账号。
3854
+ * 把一次握手推进到持久化的账号,并把结果写回 {@link pending} 里对应条目。
3718
3855
  *
3719
3856
  * 复用 `buildAccount`,使落盘结构与 CLI 登录完全一致。
3720
- * 任何失败都返回 `undefined`,让客户端的轮询得到 `done: false`,
3857
+ * 任何失败都记为"结算为失败",让客户端的轮询得到 `done: false`,
3721
3858
  * 从而可以从 `startLogin` 重试。
3859
+ *
3860
+ * 本方法由 `startLogin` **脱手**调用,`pollLogin` 不 await 它;因此它自己不返回
3861
+ * 结果,只负责把结果写进条目并安排回收。
3722
3862
  */
3723
3863
  async runLogin(state, signal) {
3864
+ let entry;
3724
3865
  try {
3725
3866
  const token = await pollAuthToken(state, signal);
3726
- if (token === void 0) return void 0;
3727
- const entry = buildAccount(token, await getLoginAccount(state, token.accessToken, token.domain));
3728
- await upsertAccount(entry);
3729
- this.session?.invalidate();
3730
- return entry;
3867
+ if (token !== void 0) {
3868
+ entry = buildAccount(token, await getLoginAccount(state, token.accessToken, token.domain));
3869
+ await upsertAccount(entry);
3870
+ this.session?.invalidate();
3871
+ }
3731
3872
  } catch {
3732
- return;
3873
+ entry = void 0;
3733
3874
  }
3875
+ this.settle(state, entry);
3876
+ }
3877
+ /**
3878
+ * 记录一次握手的结局,并在保留窗口之后回收该条目。
3879
+ *
3880
+ * 回收之所以要延迟:客户端是按自己的间隔来问结果的,一结算就删会让那次本已成功的
3881
+ * 登录再也答不出 `done: true`。取消过的握手已不在表里,这里的写入自然丢弃。
3882
+ * @param state - 握手 id。
3883
+ * @param account - 已落盘的账号;失败或取消时为 `undefined`。
3884
+ */
3885
+ settle(state, account) {
3886
+ const pending = this.pending.get(state);
3887
+ if (pending === void 0) return;
3888
+ pending.settled = true;
3889
+ pending.account = account;
3890
+ setTimeout(() => {
3891
+ if (this.pending.get(state) === pending) this.pending.delete(state);
3892
+ }, LOGIN_RESULT_RETENTION_MS).unref();
3734
3893
  }
3735
3894
  /**
3736
3895
  * 供客户端面板使用的增强版 CodeBuddy 目录:应用了促销/等级展示信息与覆盖配置的模型。
@@ -3774,21 +3933,6 @@ var CodeBuddyAuthService = class {
3774
3933
  };
3775
3934
  //#endregion
3776
3935
  //#region lib/types/prefs.js
3777
- /**
3778
- * CodeBuddy 界面偏好的命名空间契约,Host 与 Client 两面共享。
3779
- *
3780
- * 这些偏好是**插件自己的设置命名空间**里的一段:`Config` 提供部署级 base,
3781
- * 用户在 Web 设置页的写入落在 harness 的用户设置文档里,因此换浏览器、重启
3782
- * harness 后仍然生效 —— 其中「极简模式画图工具」还会被宿主读取来决定
3783
- * `codebuddy-draw` 对模型是否可见,存在浏览器里会让同一次部署因浏览器而异。
3784
- *
3785
- * 本文件必须保持零依赖:两面都会编译它,而 Client 面若因此引入 schemastery
3786
- * 就会把它打进浏览器产物(校验 schema 是宿主面的事,见 `./settings.ts`)。
3787
- *
3788
- * @module dsh-llm-codebuddy-power/prefs
3789
- */
3790
- /** 本插件拥有的设置命名空间,与插件名一致。 */
3791
- const CODEBUDDY_SETTINGS_NAMESPACE = "llm-codebuddy-power";
3792
3936
  /** 未覆盖任何一层时使用的偏好默认值。 */
3793
3937
  const DEFAULT_CODEBUDDY_SETTINGS = {
3794
3938
  showUsage: true,
@@ -3798,11 +3942,11 @@ const DEFAULT_CODEBUDDY_SETTINGS = {
3798
3942
  //#endregion
3799
3943
  //#region lib/types/settings.js
3800
3944
  /**
3801
- * 本插件设置命名空间的宿主 schema。
3945
+ * 本插件的 `Config` schema —— 连接事实与界面偏好合一。
3802
3946
  *
3803
- * 校验只在宿主发生:浏览器面若也引入 schemastery,它会连同校验器一起被打进
3804
- * `lib/client.js`,而客户端本来就把服务端返回的数据当作已校验结果接收。因此
3805
- * schema 只在此文件出现,浏览器面经 `./prefs.ts` 取同一份类型与默认值。
3947
+ * 设置表单只投影**入口自己的 `Config`** 里声明为 `.volatile()` 的字段(入口 id
3948
+ * 即命名空间),因此界面偏好字段与连接字段同在 `Config` 中,只有偏好那三个是
3949
+ * volatile。
3806
3950
  *
3807
3951
  * 字段的默认值一律取自 `DEFAULT_CODEBUDDY_SETTINGS`,不在此重复字面量 ——
3808
3952
  * 两处各写一份时,改了一处就会出现「宿主默认 90、界面显示 80」这种只有用户
@@ -3810,12 +3954,12 @@ const DEFAULT_CODEBUDDY_SETTINGS = {
3810
3954
  *
3811
3955
  * @module dsh-llm-codebuddy-power/settings
3812
3956
  */
3813
- /** 用户设置文档中本插件命名空间的 schema。 */
3814
- const CodeBuddySettingsSchema = z.object({
3815
- showUsage: z.boolean().default(DEFAULT_CODEBUDDY_SETTINGS.showUsage),
3816
- dangerPct: z.number().step(1).min(1).max(100).default(DEFAULT_CODEBUDDY_SETTINGS.dangerPct),
3817
- drawToolInMinimal: z.boolean().default(DEFAULT_CODEBUDDY_SETTINGS.drawToolInMinimal)
3818
- });
3957
+ /** `Config.showUsage` 的 schema。 */
3958
+ const ShowUsageField = z.boolean().default(DEFAULT_CODEBUDDY_SETTINGS.showUsage).volatile();
3959
+ /** `Config.dangerPct` 的 schema。 */
3960
+ const DangerPctField = z.number().step(1).min(1).max(100).default(DEFAULT_CODEBUDDY_SETTINGS.dangerPct).volatile();
3961
+ /** `Config.drawToolInMinimal` 的 schema。 */
3962
+ const DrawToolInMinimalField = z.boolean().default(DEFAULT_CODEBUDDY_SETTINGS.drawToolInMinimal).volatile();
3819
3963
  //#endregion
3820
3964
  //#region lib/types/index.js
3821
3965
  /**
@@ -3834,9 +3978,9 @@ const CodeBuddySettingsSchema = z.object({
3834
3978
  const name = "llm-codebuddy-power";
3835
3979
  /**
3836
3980
  * 本插件的激活服务。`llm` 承载适配器路由,`tools` 接受文生图工具注册,
3837
- * `agents` 用于极简模式会话中按 agent 限制画图工具可见性。生成图片经
3838
- * 标准附件通道(attachments.saveImage + uiConversation.imageUrl)持久化与
3839
- * 展示,工具注册在附件服务挂载后(`ctx.inject(['attachments'])`)。
3981
+ * `agents` 用于极简模式会话中按 agent 限制画图工具可见性。生成图片经标准附件通道
3982
+ * (attachments.saveImage + uiConversation.imageUrl)持久化与展示,工具注册在
3983
+ * 附件服务挂载后(`ctx.inject(['attachments'])`)。
3840
3984
  */
3841
3985
  const inject = [
3842
3986
  "llm",
@@ -3844,14 +3988,29 @@ const inject = [
3844
3988
  "agents"
3845
3989
  ];
3846
3990
  /**
3847
- * 校验并补全原始配置。
3991
+ * {@link Config} 的运行时 schema。
3848
3992
  *
3849
- * 编程式构造可以绕过任何 schema,因此边界在此判定,坏值在加载时就带字段名
3850
- * 失败,而不是等到请求中途。
3851
- * @param config - 原始入口配置。
3993
+ * 三个界面偏好字段必须是 volatile 才会被设置表单投影,而 volatile 只从
3994
+ * schemastery schema 产生;连接字段保持普通字段,因此不在表单里暴露 ——
3995
+ * 它们由 profile 的 cordis.patch.yml 配置。
3996
+ */
3997
+ const Config = z.object({
3998
+ baseURL: z.string(),
3999
+ defaultContextWindow: z.number().step(1).min(1),
4000
+ defaultMaxTokens: z.number().step(1).min(1),
4001
+ streamIdleTimeoutMs: z.number().min(Number.MIN_VALUE),
4002
+ showUsage: ShowUsageField,
4003
+ dangerPct: DangerPctField,
4004
+ drawToolInMinimal: DrawToolInMinimalField
4005
+ });
4006
+ /**
4007
+ * 从配置解析连接事实。
4008
+ *
4009
+ * 连接字段在 `Config` 里都是可选的,缺省值即公开的 CodeBuddy 服务。
4010
+ * @param config - 入口配置。
3852
4011
  * @returns 解析出的连接事实。
3853
4012
  */
3854
- function resolveConnectionOptions(config = {}) {
4013
+ function resolveConnectionOptions(config) {
3855
4014
  const positiveInteger = (value, field, fallback) => {
3856
4015
  if (value === void 0) return fallback;
3857
4016
  if (!Number.isInteger(value) || value <= 0) throw new Error(`dsh-llm-codebuddy-power: ${field} must be a positive integer`);
@@ -3869,34 +4028,23 @@ function resolveConnectionOptions(config = {}) {
3869
4028
  };
3870
4029
  }
3871
4030
  /**
3872
- * 解析界面偏好的部署级 base 层。用户设置文档中的写入覆盖它。
3873
- *
3874
- * 与 {@link resolveConnectionOptions} 同理,编程式构造可以绕过任何 schema,
3875
- * 因此坏值在此判定并在加载时失败。
3876
- * @param config - 原始入口配置。
3877
- * @returns 设置命名空间的 base 值。
4031
+ * 读取界面偏好的当前值。
4032
+ * @param config - 入口配置。
4033
+ * @returns 三个界面偏好。
3878
4034
  */
3879
- function resolvePrefsBase(config = {}) {
3880
- const dangerPct = config.dangerPct ?? DEFAULT_CODEBUDDY_SETTINGS.dangerPct;
3881
- if (!Number.isInteger(dangerPct) || dangerPct < 1 || dangerPct > 100) throw new Error(`dsh-llm-codebuddy-power: dangerPct must be an integer within 1..100`);
4035
+ function resolvePrefs(config) {
3882
4036
  return {
3883
- showUsage: config.showUsage ?? DEFAULT_CODEBUDDY_SETTINGS.showUsage,
3884
- dangerPct,
3885
- drawToolInMinimal: config.drawToolInMinimal ?? DEFAULT_CODEBUDDY_SETTINGS.drawToolInMinimal
4037
+ showUsage: config.showUsage.get(),
4038
+ dangerPct: config.dangerPct.get(),
4039
+ drawToolInMinimal: config.drawToolInMinimal.get()
3886
4040
  };
3887
4041
  }
3888
4042
  /** 挂载插件:解析配置,然后注册路由。 */
3889
- function apply(ctx, config = {}) {
4043
+ function apply(ctx, config) {
3890
4044
  const resolved = resolveConnectionOptions(config);
3891
- const prefsBase = resolvePrefsBase(config);
3892
- let prefsSource = () => prefsBase;
4045
+ const prefsSource = () => resolvePrefs(config);
3893
4046
  ctx.inject(["settings"], (settingsCtx) => {
3894
- settingsCtx.settings.installSection(ctx, CODEBUDDY_SETTINGS_NAMESPACE, CodeBuddySettingsSchema, prefsBase, {
3895
- setSource: (current) => {
3896
- prefsSource = current;
3897
- },
3898
- onChange: () => {}
3899
- });
4047
+ settingsCtx.effect(() => settingsCtx.settings.configure({ auto: false }, ctx.fiber));
3900
4048
  });
3901
4049
  const session = new CodeBuddySession(ctx.logger, () => {
3902
4050
  ctx.emit("llm/adapters-updated");
@@ -3905,6 +4053,7 @@ function apply(ctx, config = {}) {
3905
4053
  session,
3906
4054
  options: () => resolved,
3907
4055
  resolveAttachments: () => ctx.get("attachments"),
4056
+ resolveImageAccess: (attachments, ref) => resolveImageAttachmentAccess(attachments, (hostPath) => ctx.get("fs")?.processPathFromHostPath(hostPath), ref),
3908
4057
  resolveOverrides: async (modelId) => {
3909
4058
  return (await loadOverrides())[modelId];
3910
4059
  }
@@ -3950,8 +4099,8 @@ function apply(ctx, config = {}) {
3950
4099
  ctx.on("session/disposed", onSessionDisposed, { global: true });
3951
4100
  session.isLoggedIn().then((loggedIn) => {
3952
4101
  if (loggedIn) return;
3953
- ctx.logger.info("llm-codebuddy-power: no CodeBuddy session stored; sign in through the Settings → CodeBuddy page, or run `dsh plugin --profile web codebuddy-power` (no API key needed).");
4102
+ ctx.logger.info("llm-codebuddy-power: no CodeBuddy session stored; sign in through this plugin's card on the Web sidebar's Plugins page, or run `dsh plugin --profile web codebuddy-power` (no API key needed).");
3954
4103
  }).catch(() => {});
3955
4104
  }
3956
4105
  //#endregion
3957
- export { AUTH_PENDING_CODE, BLOCKED_MODEL_IDS, CODEBUDDY_AUTH_CHANNEL, CODEBUDDY_AXIOS_USER_AGENT, CODEBUDDY_CHAT_BASE, CODEBUDDY_DISPLAY_NAME, CODEBUDDY_ENDPOINT, CODEBUDDY_IDE_VERSION, CODEBUDDY_PRODUCT_NAME, CODEBUDDY_PROVIDER, CodeBuddyAdapter, CodeBuddyAuthService, CodeBuddySession, DEFAULT_CONTEXT_WINDOW, DEFAULT_MAX_TOKENS, DEFAULT_STREAM_IDLE_TIMEOUT_MS, LOGIN_POLL_INTERVAL_MS, LOGIN_TIMEOUT_MS, NotLoggedInError, REQUEST_IMAGE_DIMENSION_LADDER, REQUEST_IMAGE_MAX_BYTES, apply, fetchEnterpriseUsage, fetchPersonalUsage, fetchUsage, getStoragePath, hasDisclosedCapacity, httpErrorCode, inject, isBlockedModelId, loadSessionInfo, loadStorage, login, name, parseUsage, removeAccounts, resolveConnectionOptions, resolvePrefsBase, setDefaultAccount, setLastAccount, updateSessionInfo, updateStorage, upsertAccount };
4106
+ export { AUTH_PENDING_CODE, BLOCKED_MODEL_IDS, CODEBUDDY_AUTH_CHANNEL, CODEBUDDY_AXIOS_USER_AGENT, CODEBUDDY_CHAT_BASE, CODEBUDDY_DISPLAY_NAME, CODEBUDDY_ENDPOINT, CODEBUDDY_IDE_VERSION, CODEBUDDY_PRODUCT_NAME, CODEBUDDY_PROVIDER, CodeBuddyAdapter, CodeBuddyAuthService, CodeBuddySession, Config, DEFAULT_CONTEXT_WINDOW, DEFAULT_MAX_TOKENS, DEFAULT_STREAM_IDLE_TIMEOUT_MS, LOGIN_POLL_INTERVAL_MS, LOGIN_RESULT_RETENTION_MS, LOGIN_TIMEOUT_MS, NotLoggedInError, REQUEST_IMAGE_DIMENSION_LADDER, REQUEST_IMAGE_MAX_BYTES, apply, fetchEnterpriseUsage, fetchPersonalUsage, fetchUsage, getStoragePath, hasDisclosedCapacity, httpErrorCode, inject, isBlockedModelId, loadSessionInfo, loadStorage, login, name, parseUsage, removeAccounts, resolveConnectionOptions, resolvePrefs, setDefaultAccount, setLastAccount, updateSessionInfo, updateStorage, upsertAccount };