dsh-knowledge-library 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,42 @@
1
1
  const unavailable = '请先在 Harness 设置中配置默认模型;独立预览可通过环境变量配置模型。';
2
2
  class ModelResponseError extends Error {
3
3
  }
4
+ function outputLimitMessage() {
5
+ return '模型输出达到当前模型或服务的长度上限。插件沿用模型默认输出配置,未另设 token 上限;思考可能与正文共用额度,本次未自动重试。请检查模型配置,或分批整理后主动重试。';
6
+ }
7
+ async function boundedJson(response, limit = 512_000) {
8
+ const reader = response.body?.getReader();
9
+ if (!reader)
10
+ throw new Error('模型响应为空');
11
+ const chunks = [];
12
+ let length = 0;
13
+ try {
14
+ for (;;) {
15
+ const part = await reader.read();
16
+ if (part.done)
17
+ break;
18
+ length += part.value.length;
19
+ if (length > limit)
20
+ throw new Error('模型响应过大,请缩小整理范围');
21
+ chunks.push(part.value);
22
+ }
23
+ }
24
+ finally {
25
+ await reader.cancel().catch(() => { });
26
+ }
27
+ try {
28
+ return JSON.parse(Buffer.concat(chunks).toString('utf8'));
29
+ }
30
+ catch {
31
+ throw new Error('模型响应不是有效 JSON');
32
+ }
33
+ }
34
+ function checkImages(images) {
35
+ if (!images?.length)
36
+ return;
37
+ if (images.length > 10 || images.some(image => !image.data.byteLength || image.data.byteLength > 6 * 1024 * 1024 || !['image/png', 'image/jpeg', 'image/webp', 'image/gif'].includes(image.mediaType)) || images.reduce((sum, image) => sum + image.data.byteLength, 0) > 20 * 1024 * 1024)
38
+ throw new Error('图片请求超过数量、格式或字节限制');
39
+ }
4
40
  export function harnessKnowledgeModel(ctx) {
5
41
  function services() {
6
42
  const llm = ctx.get('llm');
@@ -10,7 +46,21 @@ export function harnessKnowledgeModel(ctx) {
10
46
  throw new Error(unavailable);
11
47
  return { llm, selection };
12
48
  }
49
+ async function visionStatus() {
50
+ try {
51
+ const { llm, selection } = services();
52
+ const model = `${selection.provider} / ${selection.model}`;
53
+ if (!ctx.get('attachments'))
54
+ return { available: false, model, reason: '当前宿主未提供图片附件服务,请升级或启用附件服务' };
55
+ const info = await llm.resolveModelInfo(selection.provider, selection.model, AbortSignal.timeout(5000));
56
+ return info.inputModalities?.includes('image') ? { available: true, model } : { available: false, model, reason: '当前默认模型未声明图片输入能力,请在 Harness 中选择支持视觉的模型(如 deepseek-flash)' };
57
+ }
58
+ catch {
59
+ return { available: false, reason: '无法确认默认模型的图片能力,请检查 Harness 模型设置' };
60
+ }
61
+ }
13
62
  return {
63
+ visionStatus,
14
64
  status() {
15
65
  try {
16
66
  const { selection } = services();
@@ -20,13 +70,31 @@ export function harnessKnowledgeModel(ctx) {
20
70
  return { available: false, reason: unavailable };
21
71
  }
22
72
  },
23
- async generate({ system, prompt, maxTokens, signal }) {
73
+ async generate({ system, prompt, signal, images, expectedModel }) {
74
+ checkImages(images);
24
75
  const { llm, selection } = services();
76
+ if (expectedModel !== undefined && expectedModel !== `${selection.provider} / ${selection.model}`)
77
+ throw new Error('模型已改变,请重新核对');
25
78
  signal.throwIfAborted();
79
+ const content = [{ type: 'text', text: prompt }];
80
+ if (images?.length) {
81
+ const status = await visionStatus();
82
+ signal.throwIfAborted();
83
+ if (!status.available || status.model !== `${selection.provider} / ${selection.model}`)
84
+ throw new Error(status.reason || '图片模型配置已改变');
85
+ const attachments = ctx.get('attachments');
86
+ const refs = await attachments.saveImages(images);
87
+ signal.throwIfAborted();
88
+ content.push(...refs.map(attachment => ({ type: 'image', attachment })));
89
+ }
90
+ const current = services().selection;
91
+ if (current.provider !== selection.provider || current.model !== selection.model || current.reasoningEffort !== selection.reasoningEffort)
92
+ throw new Error('模型配置已改变,请重新核对');
26
93
  let text = '';
27
94
  let finished = false;
95
+ // Omit maxTokens so the Harness runtime materializes the model's own defaults.
28
96
  try {
29
- for await (const chunk of llm.stream({ ...selection, system, messages: [{ role: 'user', content: [{ type: 'text', text: prompt }] }], maxTokens, signal })) {
97
+ for await (const chunk of llm.stream({ ...selection, system, messages: [{ role: 'user', content }], signal })) {
30
98
  signal.throwIfAborted();
31
99
  // Complete blocks are authoritative; deltas may include reasoning or retries.
32
100
  if (chunk.type === 'block-end' && chunk.block.type === 'text')
@@ -35,7 +103,7 @@ export function harnessKnowledgeModel(ctx) {
35
103
  throw new ModelResponseError('模型输出过长,请缩小整理范围');
36
104
  if (chunk.type === 'finish') {
37
105
  if (chunk.reason.kind !== 'stop')
38
- throw new ModelResponseError(chunk.reason.kind === 'max-tokens' ? '模型输出达到长度上限,请缩小整理范围后重试' : '模型调用未完成,请检查 Harness 模型设置后重试');
106
+ throw new ModelResponseError(chunk.reason.kind === 'max-tokens' ? outputLimitMessage() : '模型调用未完成,请检查 Harness 模型设置后重试');
39
107
  finished = true;
40
108
  }
41
109
  }
@@ -58,11 +126,45 @@ export function previewKnowledgeModel(env = process.env) {
58
126
  const key = env.KNOWLEDGE_LIBRARY_MODEL_API_KEY || env.DEEPSEEK_API_KEY;
59
127
  const model = env.KNOWLEDGE_LIBRARY_MODEL || 'deepseek-chat';
60
128
  const base = env.KNOWLEDGE_LIBRARY_MODEL_BASE_URL || 'https://api.deepseek.com';
129
+ let cached;
130
+ async function visionStatus() {
131
+ if (!key)
132
+ return { available: false, model, reason: unavailable };
133
+ if (cached && Date.now() - cached.time < 15_000)
134
+ return cached.value;
135
+ let value;
136
+ try {
137
+ const response = await fetch(`${base.replace(/\/$/, '')}/models`, { headers: { Authorization: `Bearer ${key}` }, redirect: 'error', signal: AbortSignal.timeout(5000) });
138
+ if (!response.ok) {
139
+ await response.body?.cancel();
140
+ throw new Error();
141
+ }
142
+ const data = await boundedJson(response);
143
+ const entry = Array.isArray(data.data) && data.data.find((item) => item.id === model);
144
+ value = entry?.input_modalities?.includes('image') ? { available: true, model } : { available: false, model, reason: '当前模型未声明图片输入能力,请设置支持视觉的 KNOWLEDGE_LIBRARY_MODEL(如 deepseek-flash)' };
145
+ }
146
+ catch {
147
+ value = { available: false, model, reason: '无法读取模型图片能力,请检查服务地址的 /models 接口和网络' };
148
+ }
149
+ cached = { time: Date.now(), value };
150
+ return value;
151
+ }
61
152
  return {
153
+ visionStatus,
62
154
  status: () => key ? { available: true, model } : { available: false, reason: unavailable },
63
- async generate({ system, prompt, maxTokens, signal }) {
155
+ async generate({ system, prompt, signal, images, expectedModel }) {
156
+ checkImages(images);
64
157
  if (!key)
65
158
  throw new Error(unavailable);
159
+ if (expectedModel !== undefined && expectedModel !== model)
160
+ throw new Error('模型已改变,请重新核对');
161
+ signal.throwIfAborted();
162
+ if (images?.length) {
163
+ const info = await visionStatus();
164
+ signal.throwIfAborted();
165
+ if (!info.available)
166
+ throw new Error(info.reason);
167
+ }
66
168
  let response;
67
169
  try {
68
170
  const url = new URL(`${base.replace(/\/$/, '')}/chat/completions`);
@@ -70,7 +172,7 @@ export function previewKnowledgeModel(env = process.env) {
70
172
  throw new Error();
71
173
  response = await fetch(url, { method: 'POST', redirect: 'error', signal,
72
174
  headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${key}` },
73
- body: JSON.stringify({ model, stream: false, max_tokens: maxTokens, messages: [{ role: 'system', content: system }, { role: 'user', content: prompt }] }) });
175
+ body: JSON.stringify({ model, stream: false, messages: [{ role: 'system', content: system }, { role: 'user', content: images?.length ? [{ type: 'text', text: prompt }, ...images.map(image => ({ type: 'image_url', image_url: { url: `data:${image.mediaType};base64,${Buffer.from(image.data).toString('base64')}`, detail: 'original' } }))] : prompt }] }) });
74
176
  }
75
177
  catch {
76
178
  signal.throwIfAborted();
@@ -80,34 +182,10 @@ export function previewKnowledgeModel(env = process.env) {
80
182
  await response.body?.cancel();
81
183
  throw new Error(`模型调用失败(HTTP ${response.status}),请检查模型配置或额度`);
82
184
  }
83
- // Bound bytes before JSON parsing, including chunked responses.
84
- const reader = response.body?.getReader();
85
- if (!reader)
86
- throw new Error('模型响应为空');
87
- const chunks = [];
88
- let length = 0;
89
- try {
90
- while (true) {
91
- const part = await reader.read();
92
- if (part.done)
93
- break;
94
- length += part.value.length;
95
- if (length > 512_000)
96
- throw new Error('模型响应过大,请缩小整理范围');
97
- chunks.push(part.value);
98
- }
99
- }
100
- finally {
101
- await reader.cancel().catch(() => { });
102
- }
103
- let data;
104
- try {
105
- data = JSON.parse(Buffer.concat(chunks).toString('utf8'));
106
- }
107
- catch {
108
- throw new Error('模型响应不是有效 JSON');
109
- }
185
+ const data = await boundedJson(response);
110
186
  const choice = data.choices?.[0];
187
+ if (choice?.finish_reason === 'length' || choice?.finish_reason === 'max_tokens')
188
+ throw new Error(outputLimitMessage());
111
189
  if (choice?.finish_reason !== 'stop' || typeof choice.message?.content !== 'string' || !choice.message.content.trim())
112
190
  throw new Error('模型未返回完整正文,可能达到输出上限,请缩小范围后重试');
113
191
  return choice.message.content;
@@ -327,6 +327,25 @@
327
327
  .kl-info .kl-proxy-option { display: flex; align-items: flex-start; gap: 8px; color: var(--kl-ink); font-size: 13px; line-height: 20px; cursor: pointer; }
328
328
  .kl-proxy-option input { flex: none; width: 16px; height: 16px; margin: 2px 0; accent-color: var(--kl-ink); }
329
329
  .kl-network-settings p { font-size: 12px; margin: 8px 0 12px; }
330
+ .kl-image-settings { margin-top: 20px; padding-top: 16px; border-top: .5px solid var(--kl-line); }
331
+ .kl-image-settings h3, .kl-page-images h3 { font-size: 13px; font-weight: 500; margin: 0 0 12px; }
332
+ .kl-image-settings p, .kl-image-record p { font-size: 12px; overflow-wrap: anywhere; }
333
+ .kl-info .kl-image-limit { display: flex; align-items: center; gap: 12px; margin: 14px 0; }
334
+ .kl-image-limit input { width: 68px; padding: 6px 8px; border: .5px solid var(--kl-line); border-radius: 8px; background: var(--kl-bg); }
335
+ .kl-image-policy { padding: 10px 12px; border-left: 2px solid var(--kl-link); background: var(--kl-sidebar); color: var(--kl-secondary); font-size: 12px; line-height: 1.8; margin: 0 0 16px; overflow-wrap: anywhere; }
336
+ .kl-page-images { margin: 24px 0; padding-block: 16px; border-block: .5px solid var(--kl-line); }
337
+ .kl-image-heading { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 8px; }
338
+ .kl-image-heading h3 { margin: 0; }
339
+ .kl-image-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(190px, 100%), 1fr)); gap: 16px; }
340
+ .kl-image-record { min-width: 0; display: flex; flex-direction: column; align-items: flex-start; gap: 6px; font-size: 12px; overflow-wrap: anywhere; }
341
+ .kl-image-record > a:has(img) { width: 100%; }
342
+ .kl-image-record img { display: block; width: 100%; height: 140px; object-fit: contain; border: .5px solid var(--kl-line); border-radius: 8px; background: var(--kl-sidebar); }
343
+ .kl-image-record small { color: var(--kl-muted); }
344
+ .kl-image-record a { color: var(--kl-link); }
345
+ .kl-image-record p { margin: 0; color: var(--kl-secondary); }
346
+ .kl-delete-dialog li { overflow-wrap: anywhere; }
347
+ .kl-job > p { color: var(--kl-secondary); }
348
+ .kl-job > p[role="alert"] { color: var(--kl-error); }
330
349
  .kl-error, .kl-form-error { background: var(--kl-sidebar); color: var(--kl-error); font-size: 13px; line-height: 1.7; padding: 10px 12px; border-radius: 8px; overflow-wrap: anywhere; }
331
350
  .kl-error { display: flex; align-items: center; gap: 12px; margin: 0 var(--kl-gutter) 14px; }
332
351
  .kl-error button { margin-left: auto; border: 0; background: transparent; color: inherit; display: flex; padding: 4px; }