@trim21/personal-pi-extensions 0.1.647 → 0.1.650
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/package.json +1 -1
- package/src/lib/lsp/client.ts +1 -1
- package/src/lib/model-call.ts +90 -0
- package/src/session-name.ts +10 -38
- package/src/vision-agent.ts +11 -45
package/package.json
CHANGED
package/src/lib/lsp/client.ts
CHANGED
|
@@ -680,7 +680,7 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
680
680
|
const initialized = await withTimeout(
|
|
681
681
|
connection.sendRequest(InitializeRequest.type, {
|
|
682
682
|
rootUri: pathToFileURL(input.root).href,
|
|
683
|
-
processId:
|
|
683
|
+
processId: process.pid,
|
|
684
684
|
workspaceFolders: [{ name: "workspace", uri: pathToFileURL(input.root).href }],
|
|
685
685
|
initializationOptions: {
|
|
686
686
|
...input.server.initialization,
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 经 pi 的模型注册表发起一次文本生成调用(非流式)。
|
|
3
|
+
*
|
|
4
|
+
* 需要「让某个模型把一段内容变成一段文本」的扩展都走这里,因为有几件事
|
|
5
|
+
* 只有在同一处才对:
|
|
6
|
+
* - registry 的契约(下面 `ModelRegistryLike` 是鸭子类型,只声明用到的方法);
|
|
7
|
+
* - user 消息的封装(`content` + `timestamp`,模型侧看到的形状每次一致);
|
|
8
|
+
* - 本地超时兜底:`ctx.signal` 在 agent 空闲时为 undefined,只依赖它就没上限,
|
|
9
|
+
* 所以这里把调用方 signal 与 `timeoutMs` 合并;
|
|
10
|
+
* - 正文提取与空正文判定:只看 `contentText`(推理模型的 thinking 不算正文),
|
|
11
|
+
* 空正文是失败而不是空结果。
|
|
12
|
+
*
|
|
13
|
+
* 不在这里的:取消语义(调用方各自决定 `AbortError` 意味着什么)、流式、
|
|
14
|
+
* 重试(registry 自己负责)、以及各扩展自己的 prompt 与后处理。
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
type Api,
|
|
19
|
+
type ApiStreamOptions,
|
|
20
|
+
type AssistantMessage,
|
|
21
|
+
contentText,
|
|
22
|
+
type Context,
|
|
23
|
+
type Model,
|
|
24
|
+
type Usage,
|
|
25
|
+
type UserMessage,
|
|
26
|
+
} from "@earendil-works/pi-ai";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 模型注册表操作:扩展传 `ctx.modelRegistry`,测试传 mock。
|
|
30
|
+
* 结构化类型(duck typing),只声明用到的两个方法。
|
|
31
|
+
*/
|
|
32
|
+
export interface ModelRegistryLike {
|
|
33
|
+
find(provider: string, modelId: string): Model<Api> | undefined;
|
|
34
|
+
complete(
|
|
35
|
+
model: Model<Api>,
|
|
36
|
+
context: Context,
|
|
37
|
+
options?: ApiStreamOptions<Api> & { signal?: AbortSignal },
|
|
38
|
+
): Promise<AssistantMessage>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface CompleteTextOptions {
|
|
42
|
+
registry: ModelRegistryLike;
|
|
43
|
+
model: Model<Api>;
|
|
44
|
+
systemPrompt: string;
|
|
45
|
+
/** user 消息内容:纯文本,或文本 / 图片分片数组。 */
|
|
46
|
+
content: UserMessage["content"];
|
|
47
|
+
/** 输出上限;缺省用 `model.maxTokens`。 */
|
|
48
|
+
maxTokens?: number;
|
|
49
|
+
/** 本地超时兜底:与 `signal` 合并,调用方不传 signal 时也仍然有上限。 */
|
|
50
|
+
timeoutMs: number;
|
|
51
|
+
/** 调用方的取消信号(例如 `ctx.signal`,agent 空闲时为 undefined)。 */
|
|
52
|
+
signal?: AbortSignal;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface CompleteTextResult {
|
|
56
|
+
/** 模型输出的正文(已 trim;thinking 块不计入)。 */
|
|
57
|
+
text: string;
|
|
58
|
+
usage: Usage;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** 合并调用方 signal 与本地超时;调用方未传时仍然有超时兜底 */
|
|
62
|
+
function withTimeout(signal: AbortSignal | undefined, ms: number): AbortSignal {
|
|
63
|
+
const timeout = AbortSignal.timeout(ms);
|
|
64
|
+
return signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 用给定模型生成正文。`registry.complete` 抛出的错误原样向上抛(含
|
|
69
|
+
* `AbortError`),取消与失败的区分由调用方决定。
|
|
70
|
+
*/
|
|
71
|
+
export async function completeText(options: CompleteTextOptions): Promise<CompleteTextResult> {
|
|
72
|
+
const result = await options.registry.complete(
|
|
73
|
+
options.model,
|
|
74
|
+
{
|
|
75
|
+
systemPrompt: options.systemPrompt,
|
|
76
|
+
messages: [
|
|
77
|
+
{ role: "user", content: options.content, timestamp: Date.now() } satisfies UserMessage,
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
maxTokens: options.maxTokens ?? options.model.maxTokens,
|
|
82
|
+
signal: withTimeout(options.signal, options.timeoutMs),
|
|
83
|
+
},
|
|
84
|
+
);
|
|
85
|
+
const text = contentText(result.content).trim();
|
|
86
|
+
if (!text) {
|
|
87
|
+
throw new Error("API 未返回内容");
|
|
88
|
+
}
|
|
89
|
+
return { text, usage: result.usage };
|
|
90
|
+
}
|
package/src/session-name.ts
CHANGED
|
@@ -24,18 +24,12 @@ import { readFileSync } from "node:fs";
|
|
|
24
24
|
import { homedir } from "node:os";
|
|
25
25
|
import { join } from "node:path";
|
|
26
26
|
|
|
27
|
-
import {
|
|
28
|
-
type Api,
|
|
29
|
-
type ApiStreamOptions,
|
|
30
|
-
type AssistantMessage,
|
|
31
|
-
contentText,
|
|
32
|
-
type Context,
|
|
33
|
-
type Model,
|
|
34
|
-
} from "@earendil-works/pi-ai";
|
|
27
|
+
import { type Api, type Model } from "@earendil-works/pi-ai";
|
|
35
28
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
36
29
|
import { type Static, Type } from "typebox";
|
|
37
30
|
import { Value } from "typebox/value";
|
|
38
31
|
|
|
32
|
+
import { completeText, type ModelRegistryLike } from "./lib/model-call.js";
|
|
39
33
|
import { isRecord } from "./lib/narrow.js";
|
|
40
34
|
|
|
41
35
|
// ── constants ────────────────────────────────────────────────────────────────
|
|
@@ -75,19 +69,6 @@ const sessionNameSettingsSchema = Type.Object({
|
|
|
75
69
|
|
|
76
70
|
type SessionNameSettings = Static<typeof sessionNameSettingsSchema>;
|
|
77
71
|
|
|
78
|
-
/**
|
|
79
|
-
* 命名所需的模型注册表操作:扩展传 ctx.modelRegistry,测试传 mock。
|
|
80
|
-
* 结构化类型(duck typing),只声明用到的两个方法。
|
|
81
|
-
*/
|
|
82
|
-
export interface ModelRegistryLike {
|
|
83
|
-
find(provider: string, modelId: string): Model<Api> | undefined;
|
|
84
|
-
complete(
|
|
85
|
-
model: Model<Api>,
|
|
86
|
-
context: Context,
|
|
87
|
-
options?: ApiStreamOptions<Api> & { signal?: AbortSignal },
|
|
88
|
-
): Promise<AssistantMessage>;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
72
|
/** 命名所需的 session 操作:扩展传 pi,测试传 mock */
|
|
92
73
|
export interface NamerAPI {
|
|
93
74
|
getSessionName(): string | undefined;
|
|
@@ -216,12 +197,6 @@ export function buildNamerPrompt(maxLength: number): string {
|
|
|
216
197
|
].join("\n");
|
|
217
198
|
}
|
|
218
199
|
|
|
219
|
-
/** 合并调用方 signal 与本地超时;调用方未传时仍然有超时兜底 */
|
|
220
|
-
function withTimeout(signal: AbortSignal | undefined, ms: number): AbortSignal {
|
|
221
|
-
const timeout = AbortSignal.timeout(ms);
|
|
222
|
-
return signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
200
|
/**
|
|
226
201
|
* 通过模型注册表调用命名模型,让模型把 prompt 概括成短名。
|
|
227
202
|
* 走 pi 的 AI SDK(modelRegistry.complete),复用 provider 解析与
|
|
@@ -239,18 +214,15 @@ export async function callNamer(
|
|
|
239
214
|
maxLength: number,
|
|
240
215
|
signal?: AbortSignal,
|
|
241
216
|
): Promise<string> {
|
|
242
|
-
const
|
|
217
|
+
const { text: output } = await completeText({
|
|
218
|
+
registry,
|
|
243
219
|
model,
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
);
|
|
250
|
-
const output = contentText(result.content).trim();
|
|
251
|
-
if (!output) {
|
|
252
|
-
throw new Error("API 未返回内容");
|
|
253
|
-
}
|
|
220
|
+
systemPrompt: buildNamerPrompt(maxLength),
|
|
221
|
+
content: text,
|
|
222
|
+
maxTokens: NAMER_MAX_TOKENS,
|
|
223
|
+
timeoutMs: REQUEST_TIMEOUT_MS,
|
|
224
|
+
signal,
|
|
225
|
+
});
|
|
254
226
|
return output;
|
|
255
227
|
}
|
|
256
228
|
|
package/src/vision-agent.ts
CHANGED
|
@@ -21,20 +21,12 @@ import { readFileSync, statSync } from "node:fs";
|
|
|
21
21
|
import { homedir } from "node:os";
|
|
22
22
|
import { basename, extname, join } from "node:path";
|
|
23
23
|
|
|
24
|
-
import {
|
|
25
|
-
type Api,
|
|
26
|
-
type ApiStreamOptions,
|
|
27
|
-
type AssistantMessage,
|
|
28
|
-
contentText,
|
|
29
|
-
type Context,
|
|
30
|
-
type ImageContent,
|
|
31
|
-
type Model,
|
|
32
|
-
type TextContent,
|
|
33
|
-
} from "@earendil-works/pi-ai";
|
|
24
|
+
import { type Api, type ImageContent, type Model, type TextContent } from "@earendil-works/pi-ai";
|
|
34
25
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
35
26
|
import { Type } from "typebox";
|
|
36
27
|
import { Value } from "typebox/value";
|
|
37
28
|
|
|
29
|
+
import { completeText, type CompleteTextResult, type ModelRegistryLike } from "./lib/model-call.js";
|
|
38
30
|
import { type ToolPendant } from "./lib/pendant.js";
|
|
39
31
|
|
|
40
32
|
// ── constants ────────────────────────────────────────────────────────────────
|
|
@@ -99,19 +91,6 @@ const visionSettingsSchema = Type.Object({
|
|
|
99
91
|
),
|
|
100
92
|
});
|
|
101
93
|
|
|
102
|
-
/**
|
|
103
|
-
* 视觉识别所需的模型注册表操作:扩展传 ctx.modelRegistry,测试传 mock。
|
|
104
|
-
* 结构化类型(duck typing),只声明用到的两个方法。
|
|
105
|
-
*/
|
|
106
|
-
export interface ModelRegistryLike {
|
|
107
|
-
find(provider: string, modelId: string): Model<Api> | undefined;
|
|
108
|
-
complete(
|
|
109
|
-
model: Model<Api>,
|
|
110
|
-
context: Context,
|
|
111
|
-
options?: ApiStreamOptions<Api> & { signal?: AbortSignal },
|
|
112
|
-
): Promise<AssistantMessage>;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
94
|
// ── 纯函数(可测试)──────────────────────────────────────────────────────────
|
|
116
95
|
|
|
117
96
|
/** 把 path(单个或数组)规整成非空字符串列表,顺序不变 */
|
|
@@ -270,12 +249,6 @@ function loadImageBytes(path: string): { base64: string; mimeType: string; label
|
|
|
270
249
|
return { base64: buffer.toString("base64"), mimeType, label: basename(path) };
|
|
271
250
|
}
|
|
272
251
|
|
|
273
|
-
/** 合并调用方 signal 与本地超时;调用方未传时仍然有超时兜底 */
|
|
274
|
-
function withTimeout(signal: AbortSignal | undefined, ms: number): AbortSignal {
|
|
275
|
-
const timeout = AbortSignal.timeout(ms);
|
|
276
|
-
return signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
252
|
/**
|
|
280
253
|
* 通过模型注册表调用视觉模型识别图片。所有图片文件直接以 base64 放进
|
|
281
254
|
* 同一个 user 消息 —— 不需要模型或 agent 先读取图片文件。走 pi 的 AI SDK
|
|
@@ -298,19 +271,16 @@ export async function callVision(
|
|
|
298
271
|
),
|
|
299
272
|
{ type: "text", text: prompt } satisfies TextContent,
|
|
300
273
|
];
|
|
301
|
-
let result:
|
|
274
|
+
let result: CompleteTextResult;
|
|
302
275
|
try {
|
|
303
|
-
result = await
|
|
276
|
+
result = await completeText({
|
|
277
|
+
registry,
|
|
304
278
|
model,
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
maxTokens: model.maxTokens,
|
|
311
|
-
signal: withTimeout(signal, REQUEST_TIMEOUT_MS),
|
|
312
|
-
},
|
|
313
|
-
);
|
|
279
|
+
systemPrompt: VISION_SYSTEM_PROMPT,
|
|
280
|
+
content,
|
|
281
|
+
timeoutMs: REQUEST_TIMEOUT_MS,
|
|
282
|
+
signal,
|
|
283
|
+
});
|
|
314
284
|
} catch (error) {
|
|
315
285
|
// 用户主动取消(signal abort)不是失败,转成 VisionAbortError 由调用方处理
|
|
316
286
|
if (error instanceof Error && error.name === "AbortError") {
|
|
@@ -318,14 +288,10 @@ export async function callVision(
|
|
|
318
288
|
}
|
|
319
289
|
throw error;
|
|
320
290
|
}
|
|
321
|
-
const text = contentText(result.content).trim();
|
|
322
|
-
if (!text) {
|
|
323
|
-
throw new Error("API 未返回内容");
|
|
324
|
-
}
|
|
325
291
|
const tokenStr =
|
|
326
292
|
typeof result.usage.totalTokens === "number" ? String(result.usage.totalTokens) : "?";
|
|
327
293
|
const labels = loaded.map((l) => l.label).join(", ");
|
|
328
|
-
return `[${labels}]\n${text}\n[模型: ${model.id}, tokens: ${tokenStr}]`;
|
|
294
|
+
return `[${labels}]\n${result.text}\n[模型: ${model.id}, tokens: ${tokenStr}]`;
|
|
329
295
|
}
|
|
330
296
|
|
|
331
297
|
// ── extension ────────────────────────────────────────────────────────────────
|