@trim21/personal-pi-extensions 0.1.503 → 0.1.505
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 +3 -2
- package/src/lib/lsp/lsp.ts +83 -26
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trim21/personal-pi-extensions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.505",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
|
|
6
6
|
"keywords": [
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"@types/lodash-es": "^4.17.12",
|
|
44
44
|
"@types/node": "^24.13.3",
|
|
45
45
|
"@types/turndown": "^5.0.6",
|
|
46
|
+
"@typescript/native": "npm:typescript@^7.0.2",
|
|
46
47
|
"@vitest/coverage-v8": "^4.1.10",
|
|
47
48
|
"citty": "^0.2.2",
|
|
48
49
|
"esbuild": "^0.28.2",
|
|
@@ -60,7 +61,7 @@
|
|
|
60
61
|
"shlex": "^3.0.0",
|
|
61
62
|
"tsx": "^4.23.12",
|
|
62
63
|
"typebox": "1.3.1",
|
|
63
|
-
"typescript": "
|
|
64
|
+
"typescript": "npm:@typescript/typescript6@^6.0.2",
|
|
64
65
|
"typescript-eslint": "^8.67.0",
|
|
65
66
|
"vitest": "^4.1.10"
|
|
66
67
|
},
|
package/src/lib/lsp/lsp.ts
CHANGED
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* 是配置错误:全局配置在扩展加载(createLspService)时抛错,本地配置在
|
|
14
14
|
* session 开始预加载时通知,工具调用时校验抛错兜底。disabled 中未注册
|
|
15
15
|
* 的 id 直接忽略;
|
|
16
|
-
* - client 按 (root, serverID) 缓存,并发 spawn 去重,启动失败记入
|
|
17
|
-
*
|
|
16
|
+
* - client 按 (root, serverID) 缓存,并发 spawn 去重,启动失败记入
|
|
17
|
+
* broken(冷却期内跳过,冷却过后下次触碰自动重试)并主动 notify;
|
|
18
18
|
* - 工具只与 touchFile / notifyFile / diagnostics / lspDiagnosticsForFile 四个方法打交道;通知回调按请求传入。
|
|
19
19
|
*/
|
|
20
20
|
|
|
@@ -109,6 +109,11 @@ export const configDefaults = {
|
|
|
109
109
|
maxOpenDocuments: clientDefaults.maxOpenDocuments,
|
|
110
110
|
} as const;
|
|
111
111
|
|
|
112
|
+
/** 服务器启动失败后自动重试的冷却(ms);冷却内跳过,之后下次触碰自动重试。 */
|
|
113
|
+
const RETRY_COOLDOWN_MS = 60_000;
|
|
114
|
+
/** 同一服务器启动失败错误通知的最小间隔(ms),冷却重试反复失败时不刷屏。 */
|
|
115
|
+
const NOTIFY_INTERVAL_MS = 5 * 60_000;
|
|
116
|
+
|
|
112
117
|
/** 生效的工作区监听配置(缺省值已应用)。 */
|
|
113
118
|
export interface EffectiveWatchConfig {
|
|
114
119
|
enabled: boolean;
|
|
@@ -292,7 +297,10 @@ export function filterAdapters(
|
|
|
292
297
|
|
|
293
298
|
interface LspState {
|
|
294
299
|
clients: LspClient[];
|
|
295
|
-
|
|
300
|
+
/** root+serverID → 最近一次启动失败时间;冷却期过后允许自动重试。 */
|
|
301
|
+
brokenFailAt: Map<string, number>;
|
|
302
|
+
/** root+serverID → 最近一次启动失败错误通知时间;节流避免反复刷屏。 */
|
|
303
|
+
brokenNotifiedAt: Map<string, number>;
|
|
296
304
|
spawning: Map<string, Promise<LspClient | undefined>>;
|
|
297
305
|
closing: boolean;
|
|
298
306
|
/** /lsp-stop 置 true:所有工具调用不再 spawn 服务器,直到 start/reload。 */
|
|
@@ -380,10 +388,10 @@ export interface LspService {
|
|
|
380
388
|
shutdownAll(): Promise<void>;
|
|
381
389
|
/** 停止全部服务器并禁用 LSP:之后工具调用不再 spawn,直到 start/reload。 */
|
|
382
390
|
stop(): Promise<void>;
|
|
383
|
-
/**
|
|
391
|
+
/** 解除禁用;服务器在下次工具调用时惰性启动(启动失败会按冷却自动重试)。 */
|
|
384
392
|
start(): void;
|
|
385
393
|
/**
|
|
386
|
-
* 重启指定服务器:关闭其全部 client
|
|
394
|
+
* 重启指定服务器:关闭其全部 client、清除对应失败记录并解除禁用;
|
|
387
395
|
* 其余服务器不受影响。配置在下次工具调用时重新读取。
|
|
388
396
|
*/
|
|
389
397
|
reload(serverID: string): Promise<void>;
|
|
@@ -408,10 +416,26 @@ function containsPath(file: string, cwd: string): boolean {
|
|
|
408
416
|
* 内置默认服务器合并构建。globalConfigPath 供测试注入固定的全局配置
|
|
409
417
|
* 路径,避免被本机 ~/.pi/agent/lsp.json 影响。
|
|
410
418
|
*/
|
|
419
|
+
export interface LspServiceStartupOptions {
|
|
420
|
+
/**
|
|
421
|
+
* 服务器启动失败后到允许自动重试的冷却时长(ms)。冷却期内该服务器被
|
|
422
|
+
* 跳过,冷却过后下次触碰匹配文件时自动重试,无需 /lsp-reload。
|
|
423
|
+
*/
|
|
424
|
+
retryCooldownMs?: number;
|
|
425
|
+
/** 同一服务器的启动失败错误通知最小间隔(ms),防止反复重试刷屏。 */
|
|
426
|
+
notifyIntervalMs?: number;
|
|
427
|
+
/** 会话级通知:启动失败时主动上报,不依赖触发请求恰好携带 notify。 */
|
|
428
|
+
notify?: ExtensionUIContext["notify"];
|
|
429
|
+
}
|
|
430
|
+
|
|
411
431
|
export function createLspService(
|
|
412
432
|
adapters?: LspServerAdapter[],
|
|
413
433
|
globalConfigPath?: string,
|
|
434
|
+
startupOptions?: LspServiceStartupOptions,
|
|
414
435
|
): LspService {
|
|
436
|
+
const retryCooldownMs = startupOptions?.retryCooldownMs ?? RETRY_COOLDOWN_MS;
|
|
437
|
+
const notifyIntervalMs = startupOptions?.notifyIntervalMs ?? NOTIFY_INTERVAL_MS;
|
|
438
|
+
const sessionNotify = startupOptions?.notify;
|
|
415
439
|
// 扩展加载时校验全局配置(本地配置在 session_start 预加载时校验)
|
|
416
440
|
validateConfig(
|
|
417
441
|
resolveConfig(
|
|
@@ -421,7 +445,8 @@ export function createLspService(
|
|
|
421
445
|
);
|
|
422
446
|
const state: LspState = {
|
|
423
447
|
clients: [],
|
|
424
|
-
|
|
448
|
+
brokenFailAt: new Map(),
|
|
449
|
+
brokenNotifiedAt: new Map(),
|
|
425
450
|
spawning: new Map(),
|
|
426
451
|
closing: false,
|
|
427
452
|
disabled: false,
|
|
@@ -558,6 +583,33 @@ export function createLspService(
|
|
|
558
583
|
notify?: ExtensionUIContext["notify"],
|
|
559
584
|
adapterFilter?: (adapter: LspServerAdapter) => boolean,
|
|
560
585
|
): Promise<LspClient[]> {
|
|
586
|
+
/**
|
|
587
|
+
* 记录一次启动失败:进入 broken(冷却期内跳过)、渲染 status,并按节流
|
|
588
|
+
* 间隔主动 notify。错误上报优先走会话级 sessionNotify——不依赖触发请求
|
|
589
|
+
* 恰好携带 notify(否则 Read warm-up 等静默通道会把失败吞掉);未注入
|
|
590
|
+
* 会话通知时退回请求级 notify 兜底。
|
|
591
|
+
*/
|
|
592
|
+
function reportStartupFailure(
|
|
593
|
+
key: string,
|
|
594
|
+
serverID: string,
|
|
595
|
+
root: string,
|
|
596
|
+
cause: string,
|
|
597
|
+
): void {
|
|
598
|
+
const now = Date.now();
|
|
599
|
+
state.brokenFailAt.set(key, now);
|
|
600
|
+
state.servers.set(key, { serverID, root, state: "broken" });
|
|
601
|
+
updateStatusText();
|
|
602
|
+
const reporter = sessionNotify ?? notify;
|
|
603
|
+
const lastNotified = state.brokenNotifiedAt.get(key);
|
|
604
|
+
if (reporter && (lastNotified === undefined || now - lastNotified >= notifyIntervalMs)) {
|
|
605
|
+
state.brokenNotifiedAt.set(key, now);
|
|
606
|
+
reporter(
|
|
607
|
+
`LSP server "${serverID}" failed to start for ${root}: ${cause}. ` +
|
|
608
|
+
`Fix the issue or run /lsp-reload ${serverID} to retry now.`,
|
|
609
|
+
"error",
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
561
613
|
if (state.closing || state.disabled) return [];
|
|
562
614
|
if (!containsPath(file, cwd)) return [];
|
|
563
615
|
const config = await loadLspConfig(cwd, globalConfigPath);
|
|
@@ -584,7 +636,12 @@ export function createLspService(
|
|
|
584
636
|
const root = await adapter.findRoot(file, cwd);
|
|
585
637
|
if (!root) continue;
|
|
586
638
|
const key = root + adapter.id;
|
|
587
|
-
|
|
639
|
+
const failedAt = state.brokenFailAt.get(key);
|
|
640
|
+
if (failedAt !== undefined) {
|
|
641
|
+
if (Date.now() - failedAt < retryCooldownMs) continue;
|
|
642
|
+
// 冷却已过:允许重试;重试成功后下面会清除 broken 记录
|
|
643
|
+
state.brokenFailAt.delete(key);
|
|
644
|
+
}
|
|
588
645
|
|
|
589
646
|
const existing = state.clients.find((c) => c.root === root && c.serverID === adapter.id);
|
|
590
647
|
if (existing) {
|
|
@@ -603,13 +660,7 @@ export function createLspService(
|
|
|
603
660
|
try {
|
|
604
661
|
const handle = await adapter.spawn(root, cwd);
|
|
605
662
|
if (!handle) {
|
|
606
|
-
|
|
607
|
-
state.servers.set(key, { serverID: adapter.id, root, state: "broken" });
|
|
608
|
-
updateStatusText();
|
|
609
|
-
notify?.(
|
|
610
|
-
`LSP server "${adapter.id}" is not available for ${root} (binary not found)`,
|
|
611
|
-
"error",
|
|
612
|
-
);
|
|
663
|
+
reportStartupFailure(key, adapter.id, root, "binary not found");
|
|
613
664
|
return;
|
|
614
665
|
}
|
|
615
666
|
const client = await create({
|
|
@@ -635,18 +686,18 @@ export function createLspService(
|
|
|
635
686
|
state.clients.push(client);
|
|
636
687
|
state.clientExtensions.set(client, adapter.extensions);
|
|
637
688
|
state.servers.set(key, { serverID: adapter.id, root, state: "running" });
|
|
689
|
+
// 启动成功:清除失败记录,之后若再次失败会立即重新上报
|
|
690
|
+
state.brokenFailAt.delete(key);
|
|
691
|
+
state.brokenNotifiedAt.delete(key);
|
|
638
692
|
updateStatusText();
|
|
639
693
|
void ensureWatcher(cwd, notify);
|
|
640
694
|
return client;
|
|
641
695
|
} catch (error) {
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
error instanceof Error ? error.message : String(error)
|
|
648
|
-
}`,
|
|
649
|
-
"error",
|
|
696
|
+
reportStartupFailure(
|
|
697
|
+
key,
|
|
698
|
+
adapter.id,
|
|
699
|
+
root,
|
|
700
|
+
error instanceof Error ? error.message : String(error),
|
|
650
701
|
);
|
|
651
702
|
return;
|
|
652
703
|
}
|
|
@@ -845,7 +896,8 @@ export function createLspService(
|
|
|
845
896
|
});
|
|
846
897
|
state.clients = [];
|
|
847
898
|
state.clientExtensions.clear();
|
|
848
|
-
state.
|
|
899
|
+
state.brokenFailAt.clear();
|
|
900
|
+
state.brokenNotifiedAt.clear();
|
|
849
901
|
state.servers.clear();
|
|
850
902
|
await stopWatcher();
|
|
851
903
|
updateStatusText();
|
|
@@ -866,7 +918,8 @@ export function createLspService(
|
|
|
866
918
|
function start(): void {
|
|
867
919
|
state.closing = false;
|
|
868
920
|
state.disabled = false;
|
|
869
|
-
state.
|
|
921
|
+
state.brokenFailAt.clear();
|
|
922
|
+
state.brokenNotifiedAt.clear();
|
|
870
923
|
updateStatusText();
|
|
871
924
|
}
|
|
872
925
|
|
|
@@ -880,7 +933,8 @@ export function createLspService(
|
|
|
880
933
|
for (const client of targets) state.clientExtensions.delete(client);
|
|
881
934
|
for (const [key, server] of state.servers) {
|
|
882
935
|
if (server.serverID !== serverID) continue;
|
|
883
|
-
state.
|
|
936
|
+
state.brokenFailAt.delete(key);
|
|
937
|
+
state.brokenNotifiedAt.delete(key);
|
|
884
938
|
state.servers.delete(key);
|
|
885
939
|
}
|
|
886
940
|
if (state.clients.length === 0) await stopWatcher();
|
|
@@ -985,7 +1039,10 @@ export function createLspManager(
|
|
|
985
1039
|
const config = await loadLspConfig(ctx.cwd, options?.globalConfigPath);
|
|
986
1040
|
validateConfig(config, options?.adapters);
|
|
987
1041
|
if (enabledServerCount(config, options?.adapters) === 0) return;
|
|
988
|
-
const next = createLspService(options?.adapters, options?.globalConfigPath
|
|
1042
|
+
const next = createLspService(options?.adapters, options?.globalConfigPath, {
|
|
1043
|
+
// 会话级通知:任何通道触发的启动失败都主动上报(不只依赖请求方 notify)
|
|
1044
|
+
notify: (message, level) => ctx.ui.notify(message, level),
|
|
1045
|
+
});
|
|
989
1046
|
// footer status 显示当前所有 LSP server 状态(无 UI 时不显示)
|
|
990
1047
|
next.attachStatus((text) =>
|
|
991
1048
|
ctx.ui.setStatus("lsp", text ? ctx.ui.theme.fg("accent", text) : undefined),
|