dsh-llm-workbuddy 0.1.7 → 0.1.9

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.
Files changed (3) hide show
  1. package/README.md +6 -3
  2. package/lib/index.js +59 -13
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -16,7 +16,7 @@
16
16
  1. **模型能力**:Web 界面的模型选择器(composer 模型菜单或 `/model` 命令)会多出
17
17
  一个 **WorkBuddy** 分组,模型(DeepSeek-V4、GLM-5.x、Kimi-K2.x、MiniMax-M3、
18
18
  Hy3、Hunyuan…)随账号可用列表实时同步,点一下即可切换;支持推理的模型还会
19
- 显示**推理等级**选择器(低 / / 高)。
19
+ 显示**推理等级**选择器(Low / Medium / High)。
20
20
  2. **Web 登录状态小组件**:在 Web GUI 右下角常驻一个状态胶囊,**实时显示登录/
21
21
  代理状态**,未登录时一键在新标签页打开 WorkBuddy 登录页,登录完成后自动变绿。
22
22
  无需再回到终端手动跑登录脚本。
@@ -302,9 +302,12 @@ DSH 的 `dsh.client` 机制只要求 `package.json` 里:
302
302
 
303
303
  ## 限制
304
304
 
305
- - 当前为纯文本适配器:图片输入会以 `UNSUPPORTED_CONTENT` 拒绝(后续可加)。
305
+ - 图片输入:声明支持图片的模型(`inputModalities: ["text", "image"]`,如 deepseek-v4-pro、
306
+ glm-5.2、kimi-k2.x、hy3 等)可附带图片,适配器会通过 DSH 的附件服务把图片编码为
307
+ `data:<mime>;base64,<bytes>` 以 OpenAI `image_url` 格式透传给代理。若附件服务不可用
308
+ (headless 等无附件场景),图片输入会以 `UNSUPPORTED_CONTENT` 稳定报错。
306
309
  - 推理等级(reasoning effort):支持推理的模型(如 DeepSeek-V4、GLM、Kimi、MiniMax、
307
- Hy3 等)会显示推理等级下拉(低 / / 高),默认值取平台默认强度。`reasoning_effort`
310
+ Hy3 等)会显示推理等级下拉(Low / Medium / High),默认值取平台默认强度。`reasoning_effort`
308
311
  会透传给代理;若某模型平台侧只接受平台默认、忽略该参数,则退化为平台默认强度,不影响出字。
309
312
  - 代理未运行时,模型请求会以 `TRANSPORT` 错误快速失败(连接被拒绝);但状态
310
313
  小组件本身不依赖代理——代理挂了它仍能显示「代理未运行」并允许触发登录。
package/lib/index.js CHANGED
@@ -97,9 +97,9 @@ const DEFAULT_MODELS = [
97
97
 
98
98
  /** Selectable reasoning efforts exposed to the harness UI, in display order. */
99
99
  const REASONING_EFFORTS = [
100
- { id: "low", name: "" },
101
- { id: "medium", name: "" },
102
- { id: "high", name: "" },
100
+ { id: "low", name: "Low" },
101
+ { id: "medium", name: "Medium" },
102
+ { id: "high", name: "High" },
103
103
  ];
104
104
 
105
105
  /**
@@ -169,14 +169,29 @@ function serializeAssistant(message) {
169
169
  /**
170
170
  * Serialize the harness conversation into OpenAI chat-completions wire
171
171
  * messages. `tool-result` blocks become standalone `{role: 'tool'}` messages;
172
- * image content is rejected (the initial version is text-only).
172
+ * image blocks are read through the durable attachment service and emitted as
173
+ * OpenAI `image_url` parts (`data:<mime>;base64,<bytes>`), which the
174
+ * workbuddy2api proxy passes through to the upstream platform. When the
175
+ * attachment service is unavailable, image input degrades to the stable
176
+ * `UNSUPPORTED_CONTENT` error.
173
177
  */
174
- function serializeMessages(messages) {
178
+ async function serializeMessages(messages, attachments, signal) {
179
+ const refs = new Map();
180
+ for (const message of messages) collectImageRefs(message.content, refs);
181
+ const requestImages = new Map();
182
+ if (refs.size > 0) {
183
+ if (attachments === undefined) {
184
+ throw new LlmError("WorkBuddy image input requires the durable attachment service", "UNSUPPORTED_CONTENT");
185
+ }
186
+ const policy = { maxPixels: 4_000_000, maxBytes: 20_000_000 };
187
+ const ordered = [...refs.values()];
188
+ const prepared = await Promise.all(ordered.map((ref) => attachments.readImageRequest(ref, policy, signal)));
189
+ for (let index = 0; index < ordered.length; index += 1) {
190
+ requestImages.set(ordered[index].attachmentId, prepared[index]);
191
+ }
192
+ }
175
193
  const wire = [];
176
194
  for (const message of messages) {
177
- if (message.content.some((block) => block.type === "image")) {
178
- throw new LlmError("The WorkBuddy adapter does not support image content yet.", "UNSUPPORTED_CONTENT");
179
- }
180
195
  if (message.role === "system") {
181
196
  wire.push({ role: "system", content: flattenText(message.content) });
182
197
  continue;
@@ -187,7 +202,26 @@ function serializeMessages(messages) {
187
202
  }
188
203
  const toolResults = message.content.filter((block) => block.type === "tool-result");
189
204
  const text = flattenText(message.content);
190
- if (text.length > 0 || toolResults.length === 0) wire.push({ role: "user", content: text });
205
+ const images = message.content.filter((block) => block.type === "image");
206
+ if (text.length > 0 || toolResults.length === 0) {
207
+ if (images.length > 0) {
208
+ const parts = [];
209
+ if (text.length > 0) parts.push({ type: "text", text });
210
+ for (const block of images) {
211
+ const version = requestImages.get(block.attachment.attachmentId);
212
+ if (version === undefined) {
213
+ throw new LlmError("WorkBuddy image input missing attachment bytes", "UNSUPPORTED_CONTENT");
214
+ }
215
+ parts.push({
216
+ type: "image_url",
217
+ image_url: { url: `data:${version.mediaType};base64,${Buffer.from(version.data).toString("base64")}` },
218
+ });
219
+ }
220
+ wire.push({ role: "user", content: parts });
221
+ } else {
222
+ wire.push({ role: "user", content: text });
223
+ }
224
+ }
191
225
  for (const result of toolResults) {
192
226
  wire.push({ role: "tool", tool_call_id: result.toolCallId, content: flattenText(result.content) || "(no output)" });
193
227
  }
@@ -195,11 +229,19 @@ function serializeMessages(messages) {
195
229
  return wire;
196
230
  }
197
231
 
232
+ /** Collect image attachment refs from a content block list (recursing into tool results). */
233
+ function collectImageRefs(blocks, refs) {
234
+ for (const block of blocks) {
235
+ if (block.type === "image") refs.set(block.attachment.attachmentId, block.attachment);
236
+ else if (block.type === "tool-result") collectImageRefs(block.content, refs);
237
+ }
238
+ }
239
+
198
240
  /** Build the full wire request. Always streaming with usage reporting on. */
199
- function serializeRequest(options) {
241
+ async function serializeRequest(options, attachments, signal) {
200
242
  const messages = [];
201
243
  if (options.system !== undefined) messages.push({ role: "system", content: options.system });
202
- messages.push(...serializeMessages(options.messages));
244
+ messages.push(...await serializeMessages(options.messages, attachments, signal));
203
245
  const tools = options.tools?.map((tool) => ({
204
246
  type: "function",
205
247
  function: { name: tool.name, description: tool.description, parameters: tool.parameters },
@@ -588,7 +630,8 @@ export class WorkBuddyAdapter extends LlmAdapter {
588
630
  }
589
631
 
590
632
  async *request(options, signal, connection, onComment) {
591
- const body = serializeRequest(options);
633
+ const attachments = this.config.resolveAttachments?.();
634
+ const body = await serializeRequest(options, attachments, signal);
592
635
  const headers = {
593
636
  "content-type": "application/json",
594
637
  "accept": "text/event-stream",
@@ -1053,7 +1096,10 @@ export function apply(ctx, config) {
1053
1096
  }
1054
1097
  };
1055
1098
  options();
1056
- const adapter = new WorkBuddyAdapter({ options });
1099
+ const adapter = new WorkBuddyAdapter({
1100
+ options,
1101
+ resolveAttachments: () => ctx.get("attachments"),
1102
+ });
1057
1103
  ctx.llm.registerConfigurableProviders([{
1058
1104
  provider: PROVIDER,
1059
1105
  displayName: "WorkBuddy",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-llm-workbuddy",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "WorkBuddy (via the local workbuddy2api proxy) LLM provider adapter for DeepSeek Harness, with a Web login-status widget",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",