pi-web-ui 0.94.1 → 0.96.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/CHANGELOG.md +91 -2
- package/README.md +5 -6
- package/README.zh-CN.md +4 -5
- package/bin/pi-web-ui.mjs +910 -237
- package/dist/server/agent-service.js +2655 -231
- package/dist/server/approval-rules.js +596 -0
- package/dist/server/attachment-store.js +129 -0
- package/dist/server/attachments.js +92 -174
- package/dist/server/bg-servers.js +45 -7
- package/dist/server/claim-files-tool.js +4 -8
- package/dist/server/claim-store.js +4 -2
- package/dist/server/client-state.js +67 -14
- package/dist/server/compact-context-tool.js +126 -0
- package/dist/server/composer-drafts.js +9 -0
- package/dist/server/context-budget.js +317 -0
- package/dist/server/control-socket.js +57 -28
- package/dist/server/conversation-read-tool.js +7 -17
- package/dist/server/dangling-tools.js +229 -0
- package/dist/server/delegate-task.js +25 -28
- package/dist/server/dsh/dsh-agent-service.js +69 -66
- package/dist/server/edit-soft-tool.js +84 -16
- package/dist/server/eval-tool.js +588 -0
- package/dist/server/file-archives.js +16 -5
- package/dist/server/files-service.js +65 -18
- package/dist/server/goal-service.js +371 -77
- package/dist/server/hashline-engine.js +703 -0
- package/dist/server/host-guard.js +94 -0
- package/dist/server/host-metrics.js +26 -2
- package/dist/server/i18n.js +3 -3
- package/dist/server/index.js +456 -80
- package/dist/server/lsp-tool.js +1371 -0
- package/dist/server/mcp-bridge.js +47 -3
- package/dist/server/model-admin.js +112 -32
- package/dist/server/office-parse.js +375 -0
- package/dist/server/patch-tool.js +85 -0
- package/dist/server/permission-preset.js +25 -0
- package/dist/server/plan-manager.js +114 -0
- package/dist/server/plugin-api-catalog.js +296 -0
- package/dist/server/plugin-catalog-sync.js +36 -19
- package/dist/server/plugin-catalog.js +11 -4
- package/dist/server/plugin-facilities.js +92 -21
- package/dist/server/plugin-install-spec.js +196 -0
- package/dist/server/plugin-installer.js +73 -0
- package/dist/server/plugin-llm.js +71 -65
- package/dist/server/plugin-manifest-validate.js +305 -0
- package/dist/server/plugin-project.js +117 -13
- package/dist/server/plugin-tool-guard.js +120 -0
- package/dist/server/plugin-updater.js +115 -12
- package/dist/server/plugins.js +950 -205
- package/dist/server/present-files-tool.js +9 -12
- package/dist/server/process-utils.js +16 -6
- package/dist/server/prompt-composer.js +9 -0
- package/dist/server/protocol-version.js +1 -1
- package/dist/server/read-tool.js +69 -30
- package/dist/server/resolve-global-sdk.js +30 -16
- package/dist/server/schedule-agent-tool.js +12 -15
- package/dist/server/scheduler-tasks.js +6 -0
- package/dist/server/sdk-origin.js +18 -2
- package/dist/server/serialize.js +111 -14
- package/dist/server/settings-service.js +112 -3
- package/dist/server/skill-tool.js +5 -7
- package/dist/server/subagent-templates.js +51 -0
- package/dist/server/subagents.js +83 -66
- package/dist/server/terminals.js +248 -81
- package/dist/server/tool-approval.js +84 -0
- package/dist/server/tool-manager.js +237 -12
- package/dist/server/tool-overrides.js +59 -0
- package/dist/server/update-check.js +28 -1
- package/dist/server/uploads.js +17 -2
- package/dist/server/wait-subscription-scan.js +18 -21
- package/dist/server/workspace-snapshot.js +113 -0
- package/dist/server/ws-client-id.js +29 -0
- package/dist/server/ws-pending-queue.js +60 -0
- package/extensions/webui.ts +60 -2
- package/package.json +5 -3
- package/plugin-sdk/README.md +25 -0
- package/plugin-sdk/index.d.ts +31 -38
- package/plugin-sdk/index.mjs +35 -19
- package/plugins/catalog.json +40 -0
- package/themes/aetheris.css +457 -0
- package/themes/ayu-light.css +6 -6
- package/themes/catppuccin-latte.css +6 -6
- package/themes/claude-code-dark.css +144 -0
- package/themes/codex.css +6 -6
- package/themes/everforest-light.css +6 -6
- package/themes/geist.css +6 -6
- package/themes/gruvbox-light.css +6 -6
- package/themes/kanagawa-lotus.css +6 -6
- package/themes/rose-pine-dawn.css +6 -6
- package/themes/solarized-light.css +6 -6
- package/themes/vs-code-dark.css +146 -0
- package/themes/zhupi-dark.css +658 -0
- package/themes/zhupi.css +711 -0
- package/web/dist/assets/{TerminalPanel-MVoxpJOA.js → TerminalPanel-DDChcYOh.js} +1 -1
- package/web/dist/assets/index-Do9RgJC3.js +366 -0
- package/web/dist/assets/index-DryOsILO.css +1 -0
- package/web/dist/assets/{markdown-eUQn_o9D.js → markdown-DXwnfD9T.js} +1 -1
- package/web/dist/index.html +3 -3
- package/web/dist/assets/index-B3S9MxnN.css +0 -1
- package/web/dist/assets/index-DVLrHI2E.js +0 -364
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* plugin-install-spec — 「安装前先读 spec」(DSH 对照清单 P0-3 / 引导式安装)。
|
|
3
|
+
*
|
|
4
|
+
* 背景:设置面板「添加到列表 / 从市场安装」原样把用户填的来源交给 CLI。打错字、
|
|
5
|
+
* 已装过、根本不是插件包,全都要等 pnpm/git 跑完、用户在输出里自己找原因。
|
|
6
|
+
* 这里在**动 CLI 之前**先做一次可解释的检查,把失败归到七种 problem 之一,
|
|
7
|
+
* 每种都给一句人话 +(能给的)修复建议。
|
|
8
|
+
*
|
|
9
|
+
* 纪律:
|
|
10
|
+
* - 本模块**不联网、不写盘**(纯分类 + 本地文件系统探测);真正的远端探测
|
|
11
|
+
* (raw.githubusercontent 读 manifest.json)在 installer 的 inspect 里做,
|
|
12
|
+
* 这样本文件可以离线单测、也可以被前端复用同一套文案 key。
|
|
13
|
+
* - 分类结果只是**建议**,不是硬门禁:installer 仍会在真正安装前再校验一次,
|
|
14
|
+
* 未知/疑似可用一律放行(宁可让 CLI 说话,也不要因为探测失败把能装的挡掉)。
|
|
15
|
+
*/
|
|
16
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
17
|
+
import { isAbsolute, join } from "node:path";
|
|
18
|
+
import { isValidSource } from "./plugin-catalog.js";
|
|
19
|
+
/** npm 包名形状(宽松):可带 @scope/ 前缀与 @version 后缀;不含 `/` 之外的路径段。 */
|
|
20
|
+
const NPM_RE = /^(?:@[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?\/)?[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?(?:@[^\s/]+)?$/i;
|
|
21
|
+
/**
|
|
22
|
+
* 解析并按形状分类一个安装来源。纯函数、不碰网络与磁盘(除 `path` 分支的
|
|
23
|
+
* 存在性判断——那是刻意让「本机路径 vs 形状不对」可区分)。
|
|
24
|
+
*/
|
|
25
|
+
export function parseInstallSpec(rawSpec) {
|
|
26
|
+
const raw = String(rawSpec ?? "").trim();
|
|
27
|
+
if (!raw)
|
|
28
|
+
return { kind: "invalid", normalized: "" };
|
|
29
|
+
// 1) 本机路径:绝对路径、file:// 前缀、或当前存在的相对目录(CLI 自己的判据)。
|
|
30
|
+
const localCandidate = raw.replace(/^file:\/\//i, "");
|
|
31
|
+
if (isAbsolute(localCandidate) || safeExists(localCandidate))
|
|
32
|
+
return { kind: "path", normalized: localCandidate };
|
|
33
|
+
// 2) 完整 URL(git 仓库 / 目录文档)。
|
|
34
|
+
if (/^https?:\/\//i.test(raw)) {
|
|
35
|
+
// GitHub 的网页 URL 仍归 github(能拿 owner/repo/ref/subpath,可做远端探测)。
|
|
36
|
+
const gh = parseGithubUrl(raw);
|
|
37
|
+
if (gh)
|
|
38
|
+
return gh;
|
|
39
|
+
return { kind: "url", normalized: raw };
|
|
40
|
+
}
|
|
41
|
+
// 3) GitHub 的 owner/repo[/sub][#ref] 简写。
|
|
42
|
+
const short = parseGithubShort(raw);
|
|
43
|
+
if (short)
|
|
44
|
+
return short;
|
|
45
|
+
// 4) npm 包名。
|
|
46
|
+
if (NPM_RE.test(raw))
|
|
47
|
+
return { kind: "npm", normalized: raw };
|
|
48
|
+
return { kind: "invalid", normalized: raw };
|
|
49
|
+
}
|
|
50
|
+
function safeExists(p) {
|
|
51
|
+
try {
|
|
52
|
+
return existsSync(p);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// 非法路径字符(Windows 上的 `<` `>` 等)→ 当不存在。
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** 解析 https://github.com/owner/repo[/tree/<ref>[/<sub>]] 或 `owner/repo` 简写。 */
|
|
60
|
+
function parseGithubUrl(raw) {
|
|
61
|
+
const m = raw.match(/^https?:\/\/(?:www\.)?github\.com\/([^/]+)\/([^/#?]+)(?:\/(?:tree|blob)\/([^/]+)((?:\/[^#?]+)*))?/i);
|
|
62
|
+
if (!m)
|
|
63
|
+
return null;
|
|
64
|
+
const [, owner, repoRaw, refRaw, rest] = m;
|
|
65
|
+
const repo = String(repoRaw ?? "").replace(/\.git$/i, "");
|
|
66
|
+
const subpath = String(rest ?? "")
|
|
67
|
+
.replace(/^\/+/, "")
|
|
68
|
+
.replace(/\/+$/, "");
|
|
69
|
+
const hashRef = raw.includes("#") ? raw.slice(raw.indexOf("#") + 1).trim() : "";
|
|
70
|
+
const ref = hashRef || String(refRaw ?? "");
|
|
71
|
+
return {
|
|
72
|
+
kind: "github",
|
|
73
|
+
normalized: [owner, repo, subpath].filter(Boolean).join("/") + (ref ? `#${ref}` : ""),
|
|
74
|
+
owner,
|
|
75
|
+
repo,
|
|
76
|
+
...(ref ? { ref } : {}),
|
|
77
|
+
...(subpath ? { subpath } : {}),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/** 解析 owner/repo[/sub][#ref](cli 与 marketplace 的主写法)。 */
|
|
81
|
+
function parseGithubShort(raw) {
|
|
82
|
+
// 带 @ 前缀 = npm scope(@scope/pkg),GitHub 的 owner 不允许 @ —— 先摘出去,
|
|
83
|
+
// 免得 `@scope/pkg` 被当成 owner=`@scope` 的仓库(分类错会让 UI 给错建议)。
|
|
84
|
+
if (raw.startsWith("@"))
|
|
85
|
+
return null;
|
|
86
|
+
const hash = raw.indexOf("#");
|
|
87
|
+
const ref = hash >= 0 ? raw.slice(hash + 1).trim() : "";
|
|
88
|
+
const body = (hash >= 0 ? raw.slice(0, hash) : raw).replace(/\/+$/, "");
|
|
89
|
+
if (!isValidSource(body))
|
|
90
|
+
return null;
|
|
91
|
+
const segs = body.split("/").filter(Boolean);
|
|
92
|
+
const [owner, repo, ...rest] = segs;
|
|
93
|
+
if (!owner || !repo)
|
|
94
|
+
return null;
|
|
95
|
+
// URL 的 tree/blob 写法也可简写过来(owner/repo/tree/<ref>/<sub>)。
|
|
96
|
+
let subpath = rest.join("/");
|
|
97
|
+
let effectiveRef = ref;
|
|
98
|
+
if (rest[0] === "tree" || rest[0] === "blob") {
|
|
99
|
+
effectiveRef = rest[1] ?? ref;
|
|
100
|
+
subpath = rest.slice(2).join("/");
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
kind: "github",
|
|
104
|
+
normalized: body + (effectiveRef ? `#${effectiveRef}` : ""),
|
|
105
|
+
owner,
|
|
106
|
+
repo,
|
|
107
|
+
...(effectiveRef ? { ref: effectiveRef } : {}),
|
|
108
|
+
...(subpath ? { subpath } : {}),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/** 粗略的 id 字符集(与 server/plugins.ts 的 ID_RE 一致)。 */
|
|
112
|
+
const ID_RE = /^[A-Za-z0-9_-]+$/;
|
|
113
|
+
/** 由来源推导落盘 id(与 CLI / plugin-catalog 同规则:子目录末段 > 仓库名 > 末段)。 */
|
|
114
|
+
export function suggestPluginId(spec, explicit) {
|
|
115
|
+
if (explicit && ID_RE.test(explicit))
|
|
116
|
+
return explicit;
|
|
117
|
+
const seg = spec.subpath
|
|
118
|
+
? spec.subpath.split("/").filter(Boolean).pop()
|
|
119
|
+
: (spec.repo ?? lastSegment(spec.normalized));
|
|
120
|
+
const cleaned = String(seg ?? "")
|
|
121
|
+
.replace(/[^A-Za-z0-9_-]/g, "-")
|
|
122
|
+
.replace(/^-+|-+$/g, "");
|
|
123
|
+
return cleaned || "plugin";
|
|
124
|
+
}
|
|
125
|
+
function lastSegment(s) {
|
|
126
|
+
const body = s.split("#")[0].replace(/\/+$/, "");
|
|
127
|
+
const segs = body.split("/").filter(Boolean);
|
|
128
|
+
return segs[segs.length - 1] ?? "";
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* 本地检查:形状分类 + 已装判定。**不联网** —— 远端 manifest 探测由 installer
|
|
132
|
+
* 的 `inspectRemote()` 补上(本函数是它的第一步,也可单独用于「添加到列表」)。
|
|
133
|
+
*/
|
|
134
|
+
export function inspectLocalInstallSpec(rawSpec, deps) {
|
|
135
|
+
const spec = parseInstallSpec(rawSpec);
|
|
136
|
+
const suggestedId = suggestPluginId(spec, deps.explicitId);
|
|
137
|
+
const installed = safeExists(join(deps.pluginsDir, suggestedId));
|
|
138
|
+
const base = { spec, installed, suggestedId };
|
|
139
|
+
if (spec.kind === "invalid")
|
|
140
|
+
return {
|
|
141
|
+
...base,
|
|
142
|
+
problem: "invalid-spec",
|
|
143
|
+
detail: "Unrecognized source — expected owner/repo, owner/repo/subdir[#ref], a GitHub URL, or an npm package name.",
|
|
144
|
+
};
|
|
145
|
+
if (spec.kind === "path" && !safeExists(spec.normalized))
|
|
146
|
+
return { ...base, problem: "not-found", detail: `Local path does not exist: ${spec.normalized}` };
|
|
147
|
+
if (installed && !deps.force)
|
|
148
|
+
return {
|
|
149
|
+
...base,
|
|
150
|
+
problem: "already-installed",
|
|
151
|
+
detail: `A plugin named "${suggestedId}" is already installed — use Update to replace it.`,
|
|
152
|
+
};
|
|
153
|
+
return base;
|
|
154
|
+
}
|
|
155
|
+
/** 读本地目录里的 manifest.json(本地路径源用;失败返回 null,不抛)。 */
|
|
156
|
+
export function readLocalManifest(dir) {
|
|
157
|
+
try {
|
|
158
|
+
const p = join(dir, "manifest.json");
|
|
159
|
+
if (!statSync(p).isFile())
|
|
160
|
+
return null;
|
|
161
|
+
const raw = JSON.parse(readFileSync(p, "utf8"));
|
|
162
|
+
return pickManifestFields(raw);
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/** 从任意 manifest 对象里摘出可展示字段(顺带校验它是插件)。 */
|
|
169
|
+
export function pickManifestFields(raw) {
|
|
170
|
+
if (!raw || typeof raw !== "object")
|
|
171
|
+
return null;
|
|
172
|
+
const o = raw;
|
|
173
|
+
if (typeof o.id !== "string" && typeof o.name !== "string")
|
|
174
|
+
return null;
|
|
175
|
+
const permissions = Array.isArray(o.permissions)
|
|
176
|
+
? o.permissions.filter((x) => typeof x === "string").slice(0, 32)
|
|
177
|
+
: undefined;
|
|
178
|
+
return {
|
|
179
|
+
...(typeof o.id === "string" ? { id: o.id } : {}),
|
|
180
|
+
...(typeof o.name === "string" ? { name: o.name } : {}),
|
|
181
|
+
...(typeof o.version === "string" ? { version: o.version } : {}),
|
|
182
|
+
...(typeof o.description === "string" ? { description: o.description } : {}),
|
|
183
|
+
...(permissions ? { permissions } : {}),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* 远端 manifest 的候选 raw URL(按 ref 有无/子目录有无各一条)。
|
|
188
|
+
* 只用 raw.githubusercontent.com —— 与 server/locales.ts 同一取用姿势,无需 API token。
|
|
189
|
+
*/
|
|
190
|
+
export function manifestCandidateUrls(spec) {
|
|
191
|
+
if (spec.kind !== "github" || !spec.owner || !spec.repo)
|
|
192
|
+
return [];
|
|
193
|
+
const ref = spec.ref || "HEAD";
|
|
194
|
+
const base = `https://raw.githubusercontent.com/${spec.owner}/${spec.repo}/${ref}`;
|
|
195
|
+
return spec.subpath ? [`${base}/${spec.subpath}/manifest.json`] : [`${base}/manifest.json`];
|
|
196
|
+
}
|
|
@@ -22,6 +22,7 @@ import { existsSync } from "node:fs";
|
|
|
22
22
|
import { join } from "node:path";
|
|
23
23
|
import { pick } from "./i18n.js";
|
|
24
24
|
import { isValidSource } from "./plugin-catalog.js";
|
|
25
|
+
import { inspectLocalInstallSpec, manifestCandidateUrls, readLocalManifest, suggestPluginId, } from "./plugin-install-spec.js";
|
|
25
26
|
import { killPidTree } from "./process-utils.js";
|
|
26
27
|
/** 插件 id 字符集(与 server/plugins.ts、plugin-catalog.ts 一致,防路径穿越)。 */
|
|
27
28
|
const ID_RE = /^[A-Za-z0-9_-]+$/;
|
|
@@ -60,6 +61,78 @@ export function buildPluginJobArgs(spec, dataDir, lang) {
|
|
|
60
61
|
args.push("--no-build");
|
|
61
62
|
return { args };
|
|
62
63
|
}
|
|
64
|
+
/** 安装前的 spec 检查(DSH P0-3 引导式安装)。
|
|
65
|
+
*
|
|
66
|
+
* 三层:① 形状分类(parseInstallSpec)+ 本地已装判定(inspectLocalInstallSpec);
|
|
67
|
+
* ② 本地路径源:直接读它的 manifest.json;
|
|
68
|
+
* ③ 远端 GitHub 源:一次 raw.githubusercontent 探测(超时 6s,失败不阻断)——
|
|
69
|
+
* 拿到 manifest 就归到 not-found / not-a-package / not-a-bundle 之一,
|
|
70
|
+
* 拿不到(网络/代理问题)回 network 但**不阻塞安装**(problem 只作提示)。
|
|
71
|
+
*
|
|
72
|
+
* 通一不联网:测试时传 fetchImpl 替身;生产走全局 fetch。
|
|
73
|
+
*/
|
|
74
|
+
export async function inspectInstallSpec(rawSpec, deps) {
|
|
75
|
+
const local = inspectLocalInstallSpec(rawSpec, deps);
|
|
76
|
+
// 形状就不对 / 本地路径不存在 / 已装(且没 force):本地已经能给出结论,不再联网。
|
|
77
|
+
if (local.problem || local.spec.kind === "npm" || local.spec.kind === "url") {
|
|
78
|
+
// npm/url 不在本检查的覆盖范围(CLI/注册表自己会报)——原样返回,让 CLI 说话。
|
|
79
|
+
return local;
|
|
80
|
+
}
|
|
81
|
+
if (local.spec.kind === "path") {
|
|
82
|
+
const manifest = readLocalManifest(local.spec.normalized);
|
|
83
|
+
if (!manifest)
|
|
84
|
+
return {
|
|
85
|
+
...local,
|
|
86
|
+
problem: "not-a-package",
|
|
87
|
+
detail: `No manifest.json found in ${local.spec.normalized} — this is not a pi-web-ui plugin.`,
|
|
88
|
+
};
|
|
89
|
+
return { ...local, manifest, suggestedId: suggestPluginId(local.spec, deps.explicitId ?? manifest.id) };
|
|
90
|
+
}
|
|
91
|
+
// github 简写:远端探测 best-effort(失败只补一条提示,不挡安装)。
|
|
92
|
+
for (const url of manifestCandidateUrls(local.spec)) {
|
|
93
|
+
try {
|
|
94
|
+
const res = await (deps.fetchImpl ?? fetch)(url, { signal: AbortSignal.timeout(deps.timeoutMs ?? 6000) });
|
|
95
|
+
if (res.status === 404)
|
|
96
|
+
continue;
|
|
97
|
+
if (!res.ok)
|
|
98
|
+
return { ...local, problem: "network", detail: `Remote probe failed: HTTP ${res.status}` };
|
|
99
|
+
const manifest = pickManifest(local, await res.json());
|
|
100
|
+
if (!manifest)
|
|
101
|
+
return {
|
|
102
|
+
...local,
|
|
103
|
+
problem: "not-a-bundle",
|
|
104
|
+
detail: "The remote manifest.json is not a valid plugin manifest (missing id/name).",
|
|
105
|
+
};
|
|
106
|
+
return { ...local, manifest, suggestedId: suggestPluginId(local.spec, deps.explicitId ?? manifest.id) };
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return { ...local, problem: "network", detail: "Could not reach the remote repository to verify the plugin." };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// 连 manifest 都没探到 —— 仓库/子目录/分支不存在,或根本不是插件包。
|
|
113
|
+
return {
|
|
114
|
+
...local,
|
|
115
|
+
problem: "not-found",
|
|
116
|
+
detail: `No manifest.json at the remote source — check the repo/subdirectory and the #ref.`,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function pickManifest(local, raw) {
|
|
120
|
+
if (!raw || typeof raw !== "object")
|
|
121
|
+
return null;
|
|
122
|
+
const o = raw;
|
|
123
|
+
if (typeof o.id !== "string" && typeof o.name !== "string")
|
|
124
|
+
return null;
|
|
125
|
+
void local;
|
|
126
|
+
return {
|
|
127
|
+
...(typeof o.id === "string" ? { id: o.id } : {}),
|
|
128
|
+
...(typeof o.name === "string" ? { name: o.name } : {}),
|
|
129
|
+
...(typeof o.version === "string" ? { version: o.version } : {}),
|
|
130
|
+
...(typeof o.description === "string" ? { description: o.description } : {}),
|
|
131
|
+
...(Array.isArray(o.permissions)
|
|
132
|
+
? { permissions: o.permissions.filter((x) => typeof x === "string").slice(0, 32) }
|
|
133
|
+
: {}),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
63
136
|
export class PluginInstaller {
|
|
64
137
|
deps;
|
|
65
138
|
child = null;
|
|
@@ -48,82 +48,88 @@ export async function completeWithIsolatedSession(env, req) {
|
|
|
48
48
|
const maxChars = Math.min(Math.max(Number(req?.maxChars ?? DEFAULT_MAX_CHARS) || DEFAULT_MAX_CHARS, 1), MAX_MAX_CHARS);
|
|
49
49
|
const timeoutMs = Math.min(Math.max(Number(req?.timeoutMs ?? DEFAULT_TIMEOUT_MS) || DEFAULT_TIMEOUT_MS, MIN_TIMEOUT_MS), MAX_TIMEOUT_MS);
|
|
50
50
|
inflight += 1;
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
modelName = `${env.fallbackModel.provider}/${env.fallbackModel.id}`;
|
|
81
|
-
}
|
|
82
|
-
const srv = await createAgentSessionFromServices({
|
|
83
|
-
services,
|
|
84
|
-
sessionManager: SessionManager.inMemory(env.cwd),
|
|
85
|
-
...(model ? { model } : {}),
|
|
86
|
-
// 全部工具关闭("all"):纯补全,无副作用(SDK 的 noTools 是 "builtin"|"all" 枚举)。
|
|
87
|
-
noTools: "all",
|
|
88
|
-
});
|
|
89
|
-
dispose = () => {
|
|
90
|
-
try {
|
|
91
|
-
srv.session.dispose();
|
|
92
|
-
}
|
|
93
|
-
catch {
|
|
94
|
-
/* dispose 尽力而为 */
|
|
95
|
-
}
|
|
96
|
-
};
|
|
97
|
-
const text = system.trim() ? `系统指令:${system.trim()}\n\n---\n\n任务:${prompt}` : prompt;
|
|
98
|
-
await srv.session.prompt(text, { expandPromptTemplates: false });
|
|
99
|
-
const raw = srv.session.getLastAssistantText() ?? "";
|
|
100
|
-
let usage;
|
|
101
|
-
try {
|
|
102
|
-
const stats = srv.session.getSessionStats();
|
|
103
|
-
usage = { input: stats.tokens.input, output: stats.tokens.output };
|
|
104
|
-
}
|
|
105
|
-
catch {
|
|
106
|
-
/* usage 尽力而为 */
|
|
107
|
-
}
|
|
108
|
-
return { ok: true, text: raw.slice(0, maxChars), model: modelName, ...(usage ? { usage } : {}) };
|
|
51
|
+
const run = (async () => {
|
|
52
|
+
let dispose;
|
|
53
|
+
try {
|
|
54
|
+
const services = await createAgentSessionServices({
|
|
55
|
+
cwd: env.cwd,
|
|
56
|
+
agentDir: env.agentDir,
|
|
57
|
+
// 技能全剥:纯补全不需要技能上下文(省 token,结果可预测)。
|
|
58
|
+
resourceLoaderOptions: {
|
|
59
|
+
skillsOverride: (res) => ({ ...res, skills: [] }),
|
|
60
|
+
},
|
|
61
|
+
modelRuntime: await ModelRuntime.create({
|
|
62
|
+
authPath: join(env.agentDir, "auth.json"),
|
|
63
|
+
modelsPath: join(env.agentDir, "models.json"),
|
|
64
|
+
}),
|
|
65
|
+
});
|
|
66
|
+
// 模型:显式指定 > 主会话回落 > 会话默认(与 reviewer 同优先级)。
|
|
67
|
+
let model;
|
|
68
|
+
let modelName = "default";
|
|
69
|
+
const spec = parseModelSpec(req?.model);
|
|
70
|
+
if (spec) {
|
|
71
|
+
model = services.modelRuntime.getModel(spec.provider, spec.id);
|
|
72
|
+
if (!model)
|
|
73
|
+
return fail(`llm.complete: 找不到模型 ${spec.spec}`);
|
|
74
|
+
modelName = spec.spec;
|
|
75
|
+
}
|
|
76
|
+
if (!model && env.fallbackModel) {
|
|
77
|
+
model = services.modelRuntime.getModel(env.fallbackModel.provider, env.fallbackModel.id);
|
|
78
|
+
if (model)
|
|
79
|
+
modelName = `${env.fallbackModel.provider}/${env.fallbackModel.id}`;
|
|
109
80
|
}
|
|
110
|
-
|
|
111
|
-
|
|
81
|
+
const srv = await createAgentSessionFromServices({
|
|
82
|
+
services,
|
|
83
|
+
sessionManager: SessionManager.inMemory(env.cwd),
|
|
84
|
+
...(model ? { model } : {}),
|
|
85
|
+
// 全部工具关闭("all"):纯补全,无副作用(SDK 的 noTools 是 "builtin"|"all" 枚举)。
|
|
86
|
+
noTools: "all",
|
|
87
|
+
});
|
|
88
|
+
dispose = () => {
|
|
112
89
|
try {
|
|
113
|
-
dispose
|
|
90
|
+
srv.session.dispose();
|
|
114
91
|
}
|
|
115
92
|
catch {
|
|
116
93
|
/* dispose 尽力而为 */
|
|
117
94
|
}
|
|
95
|
+
};
|
|
96
|
+
const text = system.trim() ? `系统指令:${system.trim()}\n\n---\n\n任务:${prompt}` : prompt;
|
|
97
|
+
await srv.session.prompt(text, { expandPromptTemplates: false });
|
|
98
|
+
const raw = srv.session.getLastAssistantText() ?? "";
|
|
99
|
+
let usage;
|
|
100
|
+
try {
|
|
101
|
+
const stats = srv.session.getSessionStats();
|
|
102
|
+
usage = { input: stats.tokens.input, output: stats.tokens.output };
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
/* usage 尽力而为 */
|
|
106
|
+
}
|
|
107
|
+
return { ok: true, text: raw.slice(0, maxChars), model: modelName, ...(usage ? { usage } : {}) };
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
// 会话回收跟 run 走:超时竞速输了 run 还在飞,run 落定才 dispose,不留孤儿。
|
|
111
|
+
try {
|
|
112
|
+
dispose?.();
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
/* dispose 尽力而为 */
|
|
118
116
|
}
|
|
119
|
-
}
|
|
117
|
+
}
|
|
118
|
+
})();
|
|
119
|
+
// 并发计数跟**真实 run** 走,而不是外层函数的 finally:超时竞速返回后 run 还在飞,
|
|
120
|
+
// 计数必须继续占着 MAX_INFLIGHT 的位 —— 否则每次超时都「腾出」一路并发,
|
|
121
|
+
// 连续超时的插件能把在飞会话越堆越多,护栏失效。catch 挂在分支上兜住 run 的
|
|
122
|
+
// 拒绝(竞速已由 timer 赢时,外层 catch 不会再收到它)。
|
|
123
|
+
void run
|
|
124
|
+
.catch(() => { })
|
|
125
|
+
.finally(() => {
|
|
126
|
+
inflight -= 1;
|
|
127
|
+
});
|
|
128
|
+
try {
|
|
120
129
|
const timer = new Promise((resolve) => setTimeout(() => resolve(fail(`llm.complete: 超时(约${Math.round(timeoutMs / 1000)}s)`)), timeoutMs));
|
|
121
130
|
return await Promise.race([run, timer]);
|
|
122
131
|
}
|
|
123
132
|
catch (err) {
|
|
124
133
|
return fail(`llm.complete: ${err.message}`);
|
|
125
134
|
}
|
|
126
|
-
finally {
|
|
127
|
-
inflight -= 1;
|
|
128
|
-
}
|
|
129
135
|
}
|