pi-web-ui 0.68.1 → 0.68.2
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/dist/server/agent-service.js +150 -53
- package/dist/server/client-state.js +4 -2
- package/dist/server/dsh/dsh-agent-service.js +15 -1
- package/dist/server/dsh/runtime/runtime-root.mjs +17 -11
- package/dist/server/index.js +61 -1
- package/dist/server/locales.js +156 -0
- package/dist/server/prompt-composer.js +34 -0
- package/dist/server/settings-service.js +9 -0
- package/dist/server/update-check.js +49 -50
- package/package.json +3 -1
- package/web/dist/assets/TerminalPanel-BQ5NTB9Y.js +6 -0
- package/web/dist/assets/TerminalPanel-DOrYoP_4.css +32 -0
- package/web/dist/assets/index-C_I-6Zul.css +10 -0
- package/web/dist/assets/index-Ck5pa3XK.js +333 -0
- package/web/dist/assets/markdown-DOsihKaR.js +51 -0
- package/web/dist/assets/{react-C9ovnpIm.js → react-DIP6JKYk.js} +2 -2
- package/web/dist/assets/xterm-B96xOxS9.js +38 -0
- package/web/dist/index.html +4 -4
- package/web/dist/assets/TerminalPanel-6GBZ9nXN.css +0 -32
- package/web/dist/assets/TerminalPanel-IJF_fssI.js +0 -6
- package/web/dist/assets/index-BmiyyjKp.css +0 -10
- package/web/dist/assets/index-qoTr5KXy.js +0 -332
- package/web/dist/assets/markdown-DRBrS2Nf.js +0 -51
- package/web/dist/assets/xterm-D1D2FVe3.js +0 -38
|
@@ -14,7 +14,7 @@ import { spawn } from "node:child_process";
|
|
|
14
14
|
import { randomUUID } from "node:crypto";
|
|
15
15
|
import { createRequire } from "node:module";
|
|
16
16
|
import { existsSync, readFileSync, rmSync, statSync, mkdirSync, watch } from "node:fs";
|
|
17
|
-
import { basename, dirname, join, resolve, sep } from "node:path";
|
|
17
|
+
import { basename, delimiter, dirname, join, resolve, sep } from "node:path";
|
|
18
18
|
import { fileURLToPath } from "node:url";
|
|
19
19
|
import { createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, createBashTool, createLocalBashOperations, getAgentDir, SessionManager, VERSION, } from "@earendil-works/pi-coding-agent";
|
|
20
20
|
import { Type } from "typebox";
|
|
@@ -35,7 +35,7 @@ import { WebUIContext, mockThemeProxy } from "./webui-context.js";
|
|
|
35
35
|
import { makeEditSoftTool, SOFT_EDIT_TOOL_NAME } from "./edit-soft-tool.js";
|
|
36
36
|
import { makeSubagentTools, subagentTitle, } from "./subagents.js";
|
|
37
37
|
import { buildAttachmentMessages } from "./attachments.js";
|
|
38
|
-
import { BUILTIN_SOUL, DEFAULT_PROMPT_TEMPLATE, renderPromptTemplate, resolveSectionTexts, } from "./prompt-composer.js";
|
|
38
|
+
import { BUILTIN_SOUL, DEFAULT_PROMPT_TEMPLATE, buildToolsSchemaText, renderPromptTemplate, resolveSectionTexts, } from "./prompt-composer.js";
|
|
39
39
|
import { serializeMessage, serializeStreamingMessage, stripTransientRetryErrors, } from "./serialize.js";
|
|
40
40
|
import { loadCommands, saveCommandsFile, TerminalManager } from "./terminals.js";
|
|
41
41
|
const SNAPSHOT_INTERVAL_MS = 60;
|
|
@@ -215,6 +215,67 @@ function makeMarkersListTool(getActiveId, markerSvc) {
|
|
|
215
215
|
},
|
|
216
216
|
};
|
|
217
217
|
}
|
|
218
|
+
/**
|
|
219
|
+
* 标准 pi 引擎的 ask_user_question 工具:模型调用时把问题桥到浏览器(复用 DSH
|
|
220
|
+
* 引擎的 question_pending/question_answer 协议,前端 DshQuestionDialog 富渲染),
|
|
221
|
+
* 阻塞 agent 循环直到用户在浏览器回答或取消。
|
|
222
|
+
*
|
|
223
|
+
* 标准 SDK 没有内建 ask_user_question,故由 pi-web-ui 以 customTool 注册(与
|
|
224
|
+
* bash/edit 同机制)。DSH 引擎走 goal-rpc 的 userQuestions provider,两者互不
|
|
225
|
+
* 冲突(各引擎各走各的)。
|
|
226
|
+
*
|
|
227
|
+
* askUser 签名带 {aborted} 快照而非完整 AbortSignal:customTool 的 execute 信号
|
|
228
|
+
* 服务于整个 agent 生命周期,这里按「已中止即拒绝」的最小语义处理,避免与其它
|
|
229
|
+
* 工具的取消逻辑纠缠。
|
|
230
|
+
*/
|
|
231
|
+
export function makeAskUserQuestionTool(clientSession) {
|
|
232
|
+
const QuestionOptionSchema = Type.Object({
|
|
233
|
+
label: Type.String({ description: "Display label for the option" }),
|
|
234
|
+
description: Type.Optional(Type.String({ description: "Optional description shown below label" })),
|
|
235
|
+
preview: Type.Optional(Type.String({
|
|
236
|
+
description: "Optional preview rendered below when this option is selected (markdown or HTML — use for mockups/code/config).",
|
|
237
|
+
})),
|
|
238
|
+
});
|
|
239
|
+
const QuestionSchema = Type.Object({
|
|
240
|
+
id: Type.String({ description: "Unique identifier for this question" }),
|
|
241
|
+
question: Type.String({ description: "The full question text to display (markdown/HTML ok)" }),
|
|
242
|
+
detail: Type.Optional(Type.String({ description: "Optional detail/context shown under the question" })),
|
|
243
|
+
header: Type.Optional(Type.String({ description: "Optional short header for this question" })),
|
|
244
|
+
options: Type.Optional(Type.Array(QuestionOptionSchema, { description: "Available options to choose from" })),
|
|
245
|
+
multiSelect: Type.Optional(Type.Boolean({ description: "Allow selecting multiple options (default: false)" })),
|
|
246
|
+
});
|
|
247
|
+
return {
|
|
248
|
+
name: "ask_user_question",
|
|
249
|
+
label: "Ask the user",
|
|
250
|
+
description: "Ask the user focused questions to pin down ambiguous requirements. Use for clarifying the task, confirming decisions, or getting preferences. Each question renders a browser dialog with markdown/HTML rich text; options may carry a `preview`. Submit or cancel to resume.",
|
|
251
|
+
parameters: Type.Object({
|
|
252
|
+
questions: Type.Array(QuestionSchema, { description: "Questions to ask the user" }),
|
|
253
|
+
}),
|
|
254
|
+
execute: async (_id, params, signal) => {
|
|
255
|
+
const qs = params.questions;
|
|
256
|
+
if (!Array.isArray(qs) || qs.length === 0) {
|
|
257
|
+
throw new Error("ask_user_question requires at least one question");
|
|
258
|
+
}
|
|
259
|
+
const answers = await clientSession.askUser(qs, {
|
|
260
|
+
aborted: signal?.aborted,
|
|
261
|
+
});
|
|
262
|
+
if (answers === null) {
|
|
263
|
+
throw new Error("用户取消了提问");
|
|
264
|
+
}
|
|
265
|
+
// 工具结果:把每道题的回答拼成简洁文本给模型,同时留 details 供 UI 展示。
|
|
266
|
+
const lines = answers.map((a) => {
|
|
267
|
+
const q = qs.find((q) => q.id === a.id);
|
|
268
|
+
const label = a.selected.join(", ");
|
|
269
|
+
const custom = a.custom?.trim() ? ` (wrote: ${a.custom.trim()})` : "";
|
|
270
|
+
return `${q?.header ?? q?.id ?? a.id}: ${label || "(no selection)"}${custom}`;
|
|
271
|
+
});
|
|
272
|
+
return {
|
|
273
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
274
|
+
details: { answers },
|
|
275
|
+
};
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
}
|
|
218
279
|
/**
|
|
219
280
|
* 插件结构化工具 → SDK ToolDefinition。
|
|
220
281
|
* execute 返回值宽容处理:{content,details} 原样收编;字符串/对象包成文本块。
|
|
@@ -868,6 +929,7 @@ export class ClientSession {
|
|
|
868
929
|
const active = sess.getActiveToolNames();
|
|
869
930
|
const snippets = {};
|
|
870
931
|
const guidelines = [];
|
|
932
|
+
const schemaEntries = [];
|
|
871
933
|
for (const name of active) {
|
|
872
934
|
const def = sess.getToolDefinition(name);
|
|
873
935
|
if (!def)
|
|
@@ -876,6 +938,11 @@ export class ClientSession {
|
|
|
876
938
|
snippets[name] = def.promptSnippet;
|
|
877
939
|
if (def.promptGuidelines)
|
|
878
940
|
guidelines.push(...def.promptGuidelines);
|
|
941
|
+
schemaEntries.push({
|
|
942
|
+
name,
|
|
943
|
+
description: def.description,
|
|
944
|
+
parameters: def.parameters,
|
|
945
|
+
});
|
|
879
946
|
}
|
|
880
947
|
const loader = sess.resourceLoader;
|
|
881
948
|
const texts = resolveSectionTexts(this.composeInputs({
|
|
@@ -895,7 +962,7 @@ export class ClientSession {
|
|
|
895
962
|
const ovs = this.settingsSvc.current.promptOverrides ?? {};
|
|
896
963
|
const hasOverride = Object.values(ovs).some((v) => typeof v === "string" && v.trim());
|
|
897
964
|
const rendered = !tpl && !hasOverride ? undefined : renderPromptTemplate(tpl || DEFAULT_PROMPT_TEMPLATE, texts, ovs);
|
|
898
|
-
return { texts, full: rendered ?? sess.systemPrompt };
|
|
965
|
+
return { texts, full: rendered ?? sess.systemPrompt, toolsSchema: buildToolsSchemaText(schemaEntries) };
|
|
899
966
|
}
|
|
900
967
|
catch {
|
|
901
968
|
// Session not ready yet.
|
|
@@ -905,7 +972,7 @@ export class ClientSession {
|
|
|
905
972
|
/** 设置面板预览用的 host 回调(见 SettingsHost.promptSnapshot):完整生效提示词
|
|
906
973
|
* + 各来源默认(自动)内容。会话未就绪时给空值,面板保持可编辑但不预览。 */
|
|
907
974
|
promptSnapshot() {
|
|
908
|
-
return this.sessionPromptSnapshot() ?? { full: "", texts: {} };
|
|
975
|
+
return this.sessionPromptSnapshot() ?? { full: "", texts: {}, toolsSchema: "" };
|
|
909
976
|
}
|
|
910
977
|
/** Web-facing extension UI context (widgets, notifications). */
|
|
911
978
|
webUi = new WebUIContext((msg) => this.emit(msg));
|
|
@@ -1000,6 +1067,14 @@ export class ClientSession {
|
|
|
1000
1067
|
subagentTemplates;
|
|
1001
1068
|
/** 内置标记服务(todo/notify/svc/rename 等,可全局/分组开关)。 */
|
|
1002
1069
|
markerSvc;
|
|
1070
|
+
// -----------------------------------------------------------------------
|
|
1071
|
+
// 用户提问桥(标准 pi 引擎的 ask_user_question customTool):与 DSH 引擎的
|
|
1072
|
+
// question_pending/question_answer 同协议。模型调 ask_user_question 工具 →
|
|
1073
|
+
// 本桥发 question_pending 给浏览器 → 等 question_answer → resolve/reject
|
|
1074
|
+
// 工具结果(agent 循环阻塞)。一次只展示一个提问(agent 阻塞在工具执行)。
|
|
1075
|
+
// -----------------------------------------------------------------------
|
|
1076
|
+
questionSeq = 0;
|
|
1077
|
+
pendingQuestions = new Map();
|
|
1003
1078
|
constructor(clientId, cwd, agentDir, stateStore) {
|
|
1004
1079
|
this.clientId = clientId;
|
|
1005
1080
|
this.cwd = cwd;
|
|
@@ -1276,6 +1351,10 @@ export class ClientSession {
|
|
|
1276
1351
|
...makeSubagentTools(this.subagentHost),
|
|
1277
1352
|
// 内置标记只读查询工具(todo/svc 状态查询,写操作走内联标记)。
|
|
1278
1353
|
makeMarkersListTool(() => this.activeId, this.markerSvc),
|
|
1354
|
+
// 标准引擎的 ask_user_question:模型调用 → 浏览器富渲染问卷(复用 DSH
|
|
1355
|
+
// 的 question_pending/question_answer 协议,前端 DshQuestionDialog)。
|
|
1356
|
+
// DSH 引擎不经此(它走 goal-rpc 的 userQuestions provider)。
|
|
1357
|
+
makeAskUserQuestionTool(this),
|
|
1279
1358
|
],
|
|
1280
1359
|
});
|
|
1281
1360
|
// 终端工具开关从创建起就生效(工具始终注册进注册表,只调活跃集)。
|
|
@@ -2168,6 +2247,55 @@ export class ClientSession {
|
|
|
2168
2247
|
resolveDialog(id, value) {
|
|
2169
2248
|
this.webUi.resolveDialog(id, value);
|
|
2170
2249
|
}
|
|
2250
|
+
// -----------------------------------------------------------------------
|
|
2251
|
+
// 用户提问桥(标准 pi 引擎 ask_user_question customTool)
|
|
2252
|
+
// -----------------------------------------------------------------------
|
|
2253
|
+
/** 标准引擎模型调 ask_user_question:发 question_pending 给浏览器并阻塞等待
|
|
2254
|
+
* question_answer。sig 为工具执行信号的当前状态(aborted → 立即 reject)。
|
|
2255
|
+
* 返回 answers(用户选中/自定义),或 null(用户取消)。 */
|
|
2256
|
+
askUser(questions, sig) {
|
|
2257
|
+
return new Promise((resolve, reject) => {
|
|
2258
|
+
if (sig?.aborted || this.disposed) {
|
|
2259
|
+
reject(new Error("ask_user_question 已中止"));
|
|
2260
|
+
return;
|
|
2261
|
+
}
|
|
2262
|
+
// 问卷开关(默认开):关 → 不弹对话框,立即报错让模型得知已禁用。
|
|
2263
|
+
if (this.settingsSvc.current.questionnaireEnabled === false) {
|
|
2264
|
+
reject(new Error("问卷功能已关闭,可在设置中重新开启"));
|
|
2265
|
+
return;
|
|
2266
|
+
}
|
|
2267
|
+
const id = `q-${++this.questionSeq}`;
|
|
2268
|
+
this.pendingQuestions.set(id, resolve);
|
|
2269
|
+
this.emit({
|
|
2270
|
+
type: "question_pending",
|
|
2271
|
+
id,
|
|
2272
|
+
questions,
|
|
2273
|
+
});
|
|
2274
|
+
});
|
|
2275
|
+
}
|
|
2276
|
+
/** 前端回答模型提问(question_answer → 恢复 askUser 的 Promise)。id 需匹配
|
|
2277
|
+
* pendingQuestions 中键;cancelled 或未匹配(例如用户早已切走)时按「取消」处理
|
|
2278
|
+
* —— 把挂起的提问全部 reject,让模型知道用户离开了。 */
|
|
2279
|
+
resolveQuestion(id, answers, cancelled) {
|
|
2280
|
+
const resolve = this.pendingQuestions.get(id);
|
|
2281
|
+
if (resolve) {
|
|
2282
|
+
this.pendingQuestions.delete(id);
|
|
2283
|
+
resolve(cancelled ? null : answers);
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
/** 标准引擎的 question_answer 路由入口(index.ts 经 cs.answerQuestion?. 转发)。
|
|
2287
|
+
* DSH 引擎的 AgentService 也实现了同名方法,此处为 ClientSession 的转发。 */
|
|
2288
|
+
answerQuestion(id, answers, cancelled) {
|
|
2289
|
+
this.resolveQuestion(id, answers, cancelled);
|
|
2290
|
+
return Promise.resolve();
|
|
2291
|
+
}
|
|
2292
|
+
/** 关闭所有挂起提问(切对话 / dispose 时清理):以「取消」解析,避免模型挂死。 */
|
|
2293
|
+
cancelPendingQuestions() {
|
|
2294
|
+
for (const [, resolve] of this.pendingQuestions) {
|
|
2295
|
+
resolve(null);
|
|
2296
|
+
}
|
|
2297
|
+
this.pendingQuestions.clear();
|
|
2298
|
+
}
|
|
2171
2299
|
/**
|
|
2172
2300
|
* Whether the pi agent has at least one usable model. ModelRuntime's
|
|
2173
2301
|
* available snapshot already accounts for models.json, auth.json, env-var
|
|
@@ -2188,65 +2316,32 @@ export class ClientSession {
|
|
|
2188
2316
|
* probe). Cached machine-wide (same binary for every client) for 10s —
|
|
2189
2317
|
* the check is only rerun after install or when the cache expires.
|
|
2190
2318
|
*
|
|
2191
|
-
* The probe
|
|
2192
|
-
*
|
|
2193
|
-
*
|
|
2194
|
-
*
|
|
2195
|
-
*
|
|
2196
|
-
* between fork and exec
|
|
2197
|
-
*
|
|
2198
|
-
* connections. Observed reproducibly on Android/Termux (Node 26).
|
|
2319
|
+
* The probe is FORK-FREE: it scans PATH for the pi executable instead of
|
|
2320
|
+
* spawning `pi --version`. Do not reintroduce a spawn here — ANY fork on
|
|
2321
|
+
* the main thread of this multi-threaded server can deadlock the whole
|
|
2322
|
+
* process on Android/Termux (issue #78): libuv's uv_spawn blocks its
|
|
2323
|
+
* caller reading the child's error pipe, and that pipe never closes when
|
|
2324
|
+
* the forked child deadlocks between fork and exec. This applies to
|
|
2325
|
+
* asynchronous spawns too — the previous async probe reproduced the hang.
|
|
2199
2326
|
*/
|
|
2200
2327
|
static piCliProbe = null;
|
|
2201
|
-
static piCliProbePending = false;
|
|
2202
2328
|
static PI_CLI_PROBE_TTL_MS = 10_000;
|
|
2203
2329
|
isPiCliInstalled() {
|
|
2204
2330
|
const now = Date.now();
|
|
2205
2331
|
const cached = ClientSession.piCliProbe;
|
|
2206
2332
|
if (cached && now - cached.at < ClientSession.PI_CLI_PROBE_TTL_MS)
|
|
2207
2333
|
return cached.installed;
|
|
2208
|
-
ClientSession.
|
|
2209
|
-
|
|
2334
|
+
const installed = ClientSession.piCliOnPath();
|
|
2335
|
+
ClientSession.piCliProbe = { at: now, installed };
|
|
2336
|
+
return installed;
|
|
2210
2337
|
}
|
|
2211
|
-
static
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
try {
|
|
2217
|
-
proc = spawn("pi", ["--version"], {
|
|
2218
|
-
stdio: "ignore",
|
|
2219
|
-
// Windows: `pi` resolves to a pi.cmd shim — spawn can only
|
|
2220
|
-
// exec those through a shell (else ENOENT).
|
|
2221
|
-
shell: process.platform === "win32",
|
|
2222
|
-
});
|
|
2223
|
-
}
|
|
2224
|
-
catch {
|
|
2225
|
-
ClientSession.piCliProbe = { at: Date.now(), installed: false };
|
|
2226
|
-
ClientSession.piCliProbePending = false;
|
|
2227
|
-
return;
|
|
2338
|
+
static piCliOnPath() {
|
|
2339
|
+
const dirs = (process.env.PATH ?? "").split(delimiter);
|
|
2340
|
+
for (const dir of dirs) {
|
|
2341
|
+
if (dir && existsSync(join(dir, "pi")))
|
|
2342
|
+
return true;
|
|
2228
2343
|
}
|
|
2229
|
-
|
|
2230
|
-
ClientSession.piCliProbe = { at: Date.now(), installed };
|
|
2231
|
-
ClientSession.piCliProbePending = false;
|
|
2232
|
-
};
|
|
2233
|
-
const timer = setTimeout(() => {
|
|
2234
|
-
try {
|
|
2235
|
-
proc.kill();
|
|
2236
|
-
}
|
|
2237
|
-
catch {
|
|
2238
|
-
/* already exited */
|
|
2239
|
-
}
|
|
2240
|
-
finish(false);
|
|
2241
|
-
}, 5000);
|
|
2242
|
-
proc.on("error", () => {
|
|
2243
|
-
clearTimeout(timer);
|
|
2244
|
-
finish(false);
|
|
2245
|
-
});
|
|
2246
|
-
proc.on("close", (code) => {
|
|
2247
|
-
clearTimeout(timer);
|
|
2248
|
-
finish(code === 0);
|
|
2249
|
-
});
|
|
2344
|
+
return false;
|
|
2250
2345
|
}
|
|
2251
2346
|
static invalidatePiCliProbe() {
|
|
2252
2347
|
ClientSession.piCliProbe = null;
|
|
@@ -4327,6 +4422,8 @@ export class ClientSession {
|
|
|
4327
4422
|
this.files.unwatchDir();
|
|
4328
4423
|
this.files.unwatchGit();
|
|
4329
4424
|
this.webUi.dispose();
|
|
4425
|
+
// 关闭所有挂起的用户提问(dispose 时以「取消」解析,避免模型挂死)。
|
|
4426
|
+
this.cancelPendingQuestions();
|
|
4330
4427
|
this.bg.stop();
|
|
4331
4428
|
for (const conv of this.convs.values()) {
|
|
4332
4429
|
this.clearAllToolWatchdogs(conv);
|
|
@@ -200,10 +200,11 @@ export class ClientStateStore {
|
|
|
200
200
|
promptOverrides,
|
|
201
201
|
disabledSkills: stored?.disabledSkills ?? [],
|
|
202
202
|
disabledExtensions: stored?.disabledExtensions ?? [],
|
|
203
|
-
terminalToolsEnabled: stored?.terminalToolsEnabled ??
|
|
203
|
+
terminalToolsEnabled: stored?.terminalToolsEnabled ?? false,
|
|
204
204
|
terminalBash: stored?.terminalBash ?? false,
|
|
205
205
|
terminalBashIdleMs: stored?.terminalBashIdleMs ?? 15_000,
|
|
206
206
|
editSoftEnabled: stored?.editSoftEnabled ?? false,
|
|
207
|
+
questionnaireEnabled: stored?.questionnaireEnabled ?? true,
|
|
207
208
|
thinkingWrap: stored?.thinkingWrap ?? false,
|
|
208
209
|
toolsWrap: stored?.toolsWrap ?? true,
|
|
209
210
|
visionBridgeEnabled: stored?.visionBridgeEnabled ?? true,
|
|
@@ -230,10 +231,11 @@ export class ClientStateStore {
|
|
|
230
231
|
promptOverrides: { ...(settings.promptOverrides ?? cur.promptOverrides) },
|
|
231
232
|
disabledSkills: settings.disabledSkills ?? cur.disabledSkills ?? [],
|
|
232
233
|
disabledExtensions: settings.disabledExtensions ?? cur.disabledExtensions ?? [],
|
|
233
|
-
terminalToolsEnabled: settings.terminalToolsEnabled ?? cur.terminalToolsEnabled ??
|
|
234
|
+
terminalToolsEnabled: settings.terminalToolsEnabled ?? cur.terminalToolsEnabled ?? false,
|
|
234
235
|
terminalBash: settings.terminalBash ?? cur.terminalBash ?? false,
|
|
235
236
|
terminalBashIdleMs: settings.terminalBashIdleMs ?? cur.terminalBashIdleMs ?? 15_000,
|
|
236
237
|
editSoftEnabled: settings.editSoftEnabled ?? cur.editSoftEnabled ?? false,
|
|
238
|
+
questionnaireEnabled: settings.questionnaireEnabled ?? cur.questionnaireEnabled ?? true,
|
|
237
239
|
thinkingWrap: settings.thinkingWrap ?? cur.thinkingWrap ?? false,
|
|
238
240
|
toolsWrap: settings.toolsWrap ?? cur.toolsWrap ?? true,
|
|
239
241
|
visionBridgeEnabled: settings.visionBridgeEnabled ?? cur.visionBridgeEnabled ?? true,
|
|
@@ -104,10 +104,11 @@ const DEFAULT_SETTINGS = {
|
|
|
104
104
|
customSystemPrompt: "",
|
|
105
105
|
disabledSkills: [],
|
|
106
106
|
disabledExtensions: [],
|
|
107
|
-
terminalToolsEnabled:
|
|
107
|
+
terminalToolsEnabled: false,
|
|
108
108
|
terminalBash: false,
|
|
109
109
|
terminalBashIdleMs: 15_000,
|
|
110
110
|
editSoftEnabled: false,
|
|
111
|
+
questionnaireEnabled: true,
|
|
111
112
|
thinkingWrap: false,
|
|
112
113
|
toolsWrap: true,
|
|
113
114
|
disabledPlugins: [],
|
|
@@ -221,6 +222,7 @@ export class DshClientSession {
|
|
|
221
222
|
terminalBash: savedSettings.terminalBash,
|
|
222
223
|
terminalBashIdleMs: savedSettings.terminalBashIdleMs,
|
|
223
224
|
editSoftEnabled: savedSettings.editSoftEnabled,
|
|
225
|
+
questionnaireEnabled: savedSettings.questionnaireEnabled ?? true,
|
|
224
226
|
thinkingWrap: savedSettings.thinkingWrap,
|
|
225
227
|
toolsWrap: savedSettings.toolsWrap,
|
|
226
228
|
disabledPlugins: savedSettings.disabledPlugins ?? [],
|
|
@@ -443,6 +445,11 @@ export class DshClientSession {
|
|
|
443
445
|
else if (method === "question.pending") {
|
|
444
446
|
// 模型 ask_user_question → 转发给浏览器对话框(deadline = 服务端超时时间戳)。
|
|
445
447
|
const params0 = params;
|
|
448
|
+
// 问卷开关(默认开):关 → 不弹框,立即取消让模型得知已禁用。
|
|
449
|
+
if (this.settings.questionnaireEnabled === false) {
|
|
450
|
+
void this.answerQuestion(params0.id, [], true);
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
446
453
|
this.emit({
|
|
447
454
|
type: "question_pending",
|
|
448
455
|
id: params0.id,
|
|
@@ -461,6 +468,7 @@ export class DshClientSession {
|
|
|
461
468
|
options: q.options.map((o) => ({
|
|
462
469
|
label: String(o.label ?? ""),
|
|
463
470
|
...(typeof o.description === "string" ? { description: o.description } : {}),
|
|
471
|
+
...(typeof o.preview === "string" ? { preview: o.preview } : {}),
|
|
464
472
|
})),
|
|
465
473
|
}
|
|
466
474
|
: {}),
|
|
@@ -2060,6 +2068,7 @@ export class DshClientSession {
|
|
|
2060
2068
|
terminalBash: this.settings.terminalBash,
|
|
2061
2069
|
terminalBashIdleMs: this.settings.terminalBashIdleMs,
|
|
2062
2070
|
editSoftEnabled: this.settings.editSoftEnabled,
|
|
2071
|
+
questionnaireEnabled: this.settings.questionnaireEnabled,
|
|
2063
2072
|
thinkingWrap: this.settings.thinkingWrap,
|
|
2064
2073
|
toolsWrap: this.settings.toolsWrap,
|
|
2065
2074
|
visionBridgeEnabled: false,
|
|
@@ -2073,6 +2082,8 @@ export class DshClientSession {
|
|
|
2073
2082
|
promptOverrides: {},
|
|
2074
2083
|
effectiveSystemPrompt: this.settings.customSystemPrompt,
|
|
2075
2084
|
promptSourceDefaults: {},
|
|
2085
|
+
// DSH 引擎不接标准 pi 的 customTool 工具 schema(走 goal-rpc),此处给空。
|
|
2086
|
+
toolsSchema: "",
|
|
2076
2087
|
visionBridgeDefaultPrompt: "",
|
|
2077
2088
|
visionModels: [],
|
|
2078
2089
|
skills: this.skillsCache,
|
|
@@ -2108,6 +2119,8 @@ export class DshClientSession {
|
|
|
2108
2119
|
this.settings.terminalBashIdleMs = partial.terminalBashIdleMs;
|
|
2109
2120
|
if (partial.editSoftEnabled !== undefined)
|
|
2110
2121
|
this.settings.editSoftEnabled = partial.editSoftEnabled;
|
|
2122
|
+
if (partial.questionnaireEnabled !== undefined)
|
|
2123
|
+
this.settings.questionnaireEnabled = partial.questionnaireEnabled;
|
|
2111
2124
|
if (partial.thinkingWrap !== undefined)
|
|
2112
2125
|
this.settings.thinkingWrap = partial.thinkingWrap;
|
|
2113
2126
|
if (partial.toolsWrap !== undefined)
|
|
@@ -2135,6 +2148,7 @@ export class DshClientSession {
|
|
|
2135
2148
|
terminalBash: this.settings.terminalBash,
|
|
2136
2149
|
terminalBashIdleMs: this.settings.terminalBashIdleMs,
|
|
2137
2150
|
editSoftEnabled: this.settings.editSoftEnabled,
|
|
2151
|
+
questionnaireEnabled: this.settings.questionnaireEnabled,
|
|
2138
2152
|
thinkingWrap: this.settings.thinkingWrap,
|
|
2139
2153
|
toolsWrap: this.settings.toolsWrap,
|
|
2140
2154
|
disabledPlugins: this.settings.disabledPlugins,
|
|
@@ -66,18 +66,24 @@ export async function resolveRuntimeBase() {
|
|
|
66
66
|
const base = runtimeBaseFor(adjacent);
|
|
67
67
|
if (base) return base;
|
|
68
68
|
}
|
|
69
|
+
// PATCHED (pi-web-ui #78 / Android-Termux deadlock): this used to run
|
|
70
|
+
// spawnSync("npm", ["root", "-g"]) as a last resort — a synchronous fork
|
|
71
|
+
// inside a multi-threaded process, which can deadlock on Android/Termux
|
|
72
|
+
// (child stuck between fork and exec while the caller blocks on libuv's
|
|
73
|
+
// spawn error pipe). Compute the npm global root without spawning instead.
|
|
69
74
|
try {
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
75
|
+
const prefixCandidates = [
|
|
76
|
+
process.env.NPM_CONFIG_PREFIX,
|
|
77
|
+
process.env.npm_config_prefix,
|
|
78
|
+
join(dirname(process.execPath), ".."),
|
|
79
|
+
];
|
|
80
|
+
for (const prefix of prefixCandidates) {
|
|
81
|
+
if (!prefix) continue;
|
|
82
|
+
for (const root of [join(prefix, "lib", "node_modules"), join(prefix, "node_modules")]) {
|
|
83
|
+
if (!existsSync(root)) continue;
|
|
84
|
+
const base = runtimeBaseFor(root);
|
|
85
|
+
if (base) return base;
|
|
86
|
+
}
|
|
81
87
|
}
|
|
82
88
|
} catch {
|
|
83
89
|
/* fall through */
|
package/dist/server/index.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* all share one conversation list per project.
|
|
17
17
|
* PI_CODING_AGENT_DIR pi config dir (auth/models/skills) — passed to the SDK
|
|
18
18
|
*/
|
|
19
|
-
import { existsSync } from "node:fs";
|
|
19
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
20
20
|
import { stat } from "node:fs/promises";
|
|
21
21
|
import { createServer } from "node:http";
|
|
22
22
|
import { createConnection } from "node:net";
|
|
@@ -36,6 +36,7 @@ import { startControlServer } from "./control-socket.js";
|
|
|
36
36
|
import { scheduleUploadCleanup } from "./uploads.js";
|
|
37
37
|
import { ensureWindowsBash, windowsBashDir } from "./ensure-bash.js";
|
|
38
38
|
import { listThemes, resolveThemeFile } from "./themes.js";
|
|
39
|
+
import { installPack, isKnownPack, listPacks, readPackFile, removePack } from "./locales.js";
|
|
39
40
|
import { PluginManager, resolvePluginClientFile, } from "./plugins.js";
|
|
40
41
|
import { McpBridge } from "./mcp-bridge.js";
|
|
41
42
|
/** 从 CLI 参数中取 flag 值:支持 --flag value 与 --flag=value 两种写法。
|
|
@@ -76,6 +77,19 @@ const ALLOW_ORIGINS = (process.env.PI_WEB_ALLOW_ORIGINS ?? "")
|
|
|
76
77
|
* Authorization: Bearer / X-PI-Token 头、?token= 查询参数或 pi_web_token cookie
|
|
77
78
|
* 任一匹配即可;供 0.0.0.0 / 反代等暴露场景兜底,未设置则行为不变。 */
|
|
78
79
|
const AUTH_TOKEN = process.env.PI_WEB_TOKEN?.trim() ?? "";
|
|
80
|
+
/** 语言包下载根(语言包仓库的 raw 文件地址;版本 tag 优先、main 兜底,见 locales.ts)。 */
|
|
81
|
+
const LOCALE_BASE_URL = process.env.PI_WEB_LOCALE_BASE_URL?.trim() || "https://raw.githubusercontent.com/xing-shuyin/pi-web-ui";
|
|
82
|
+
/** 本包版本 —— 下载语言包时优先取同版本 tag,保证 key 对齐。 */
|
|
83
|
+
const APP_VERSION = (() => {
|
|
84
|
+
try {
|
|
85
|
+
// 注意:此处不能用下面的 pkgRoot 常量(TDZ)——直接调函数声明(已提升)。
|
|
86
|
+
const pkg = JSON.parse(readFileSync(join(resolvePkgRoot(), "package.json"), "utf8"));
|
|
87
|
+
return pkg.version ?? "";
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return "";
|
|
91
|
+
}
|
|
92
|
+
})();
|
|
79
93
|
// Root of the SDK default per-project session dirs — chat transcripts live in
|
|
80
94
|
// <SESSION_DIR_ROOT>/--<cwd>--/, shared with the pi CLI/TUI (getAgentDir
|
|
81
95
|
// honors PI_CODING_AGENT_DIR).
|
|
@@ -252,6 +266,51 @@ const USER_THEMES_DIR = join(DATA_DIR, "themes");
|
|
|
252
266
|
app.get("/api/themes", (_req, res) => {
|
|
253
267
|
res.json({ themes: listThemes(BUILTIN_THEMES_DIR, USER_THEMES_DIR) });
|
|
254
268
|
});
|
|
269
|
+
// 语言包:核心只随包发布中英,其余按需下载到 <dataDir>/locales/<code>.json。
|
|
270
|
+
// 手工放进去的同名 JSON 也会被识别(离线安装)。PI_WEB_TOKEN 鉴权自动覆盖。
|
|
271
|
+
app.get("/api/locales", (_req, res) => {
|
|
272
|
+
res.json({ packs: listPacks(DATA_DIR) });
|
|
273
|
+
});
|
|
274
|
+
app.get("/api/locales/:code", (req, res) => {
|
|
275
|
+
const code = String(req.params.code ?? "");
|
|
276
|
+
if (!isKnownPack(code)) {
|
|
277
|
+
res.status(404).end("unknown locale");
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const pack = readPackFile(DATA_DIR, code);
|
|
281
|
+
if (!pack) {
|
|
282
|
+
res.status(404).end("locale not installed");
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
286
|
+
res.json(pack);
|
|
287
|
+
});
|
|
288
|
+
app.post("/api/locales/:code/install", async (req, res) => {
|
|
289
|
+
const code = String(req.params.code ?? "");
|
|
290
|
+
if (!isKnownPack(code)) {
|
|
291
|
+
res.status(400).json({ error: `unknown locale: ${code}` });
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
try {
|
|
295
|
+
const meta = await installPack(DATA_DIR, code, { baseUrl: LOCALE_BASE_URL, version: APP_VERSION });
|
|
296
|
+
res.json({ ok: true, ...meta });
|
|
297
|
+
}
|
|
298
|
+
catch (e) {
|
|
299
|
+
res.status(502).json({ error: e instanceof Error ? e.message : String(e) });
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
app.delete("/api/locales/:code", (req, res) => {
|
|
303
|
+
const code = String(req.params.code ?? "");
|
|
304
|
+
if (!isKnownPack(code)) {
|
|
305
|
+
res.status(404).end("unknown locale");
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
if (!removePack(DATA_DIR, code)) {
|
|
309
|
+
res.status(404).end("locale not installed");
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
res.json({ ok: true });
|
|
313
|
+
});
|
|
255
314
|
// Serve a theme's full CSS file so the frontend can swap the whole stylesheet.
|
|
256
315
|
// Registered before the SPA catch-all below (otherwise it'd return index.html).
|
|
257
316
|
app.get("/themes/:id.css", (req, res) => {
|
|
@@ -826,6 +885,7 @@ wss.on("connection", (ws) => {
|
|
|
826
885
|
terminalBash: msg.terminalBash,
|
|
827
886
|
terminalBashIdleMs: msg.terminalBashIdleMs,
|
|
828
887
|
editSoftEnabled: msg.editSoftEnabled,
|
|
888
|
+
questionnaireEnabled: msg.questionnaireEnabled,
|
|
829
889
|
thinkingWrap: msg.thinkingWrap,
|
|
830
890
|
toolsWrap: msg.toolsWrap,
|
|
831
891
|
visionBridgeEnabled: msg.visionBridgeEnabled,
|