u1s1-cli 1.14.6 → 1.15.1
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/agent-setup.d.ts +7 -0
- package/dist/agent-setup.js +14 -0
- package/dist/agents/agent-config.d.ts +83 -0
- package/dist/agents/agent-config.js +154 -0
- package/dist/agents/claude.d.ts +113 -0
- package/dist/agents/claude.js +411 -0
- package/dist/agents/codex.d.ts +111 -0
- package/dist/agents/codex.js +392 -0
- package/dist/agents/launch.d.ts +109 -0
- package/dist/agents/launch.js +444 -0
- package/dist/agents/pi.d.ts +9 -0
- package/dist/agents/pi.js +562 -0
- package/dist/agents/prefs.d.ts +35 -0
- package/dist/agents/prefs.js +96 -0
- package/dist/agents/runtime.d.ts +354 -0
- package/dist/agents/runtime.js +884 -0
- package/dist/agents/status.d.ts +20 -0
- package/dist/agents/status.js +241 -0
- package/dist/config-command.d.ts +18 -0
- package/dist/config-command.js +335 -0
- package/dist/config.d.ts +17 -0
- package/dist/config.js +35 -1
- package/dist/device-auth.d.ts +37 -1
- package/dist/device-auth.js +43 -8
- package/dist/embed.d.ts +13 -0
- package/dist/embed.js +15 -0
- package/dist/index.js +22 -556
- package/dist/launcher.d.ts +48 -0
- package/dist/launcher.js +235 -0
- package/dist/mcp-server.d.ts +105 -0
- package/dist/mcp-server.js +449 -0
- package/dist/pi-ui-text.d.ts +57 -0
- package/dist/pi-ui-text.js +148 -0
- package/dist/usage.js +2 -2
- package/package.json +1 -1
- package/scripts/p0-claude-code/README.md +39 -0
- package/scripts/p0-claude-code/launch-claude.ts +94 -0
- package/scripts/p0-claude-code/replay.ts +74 -0
- package/scripts/p0-claude-code/seed-local.mjs +145 -0
- package/scripts/p0-claude-code/tap.mjs +105 -0
- package/scripts/patch-pi.js +76 -55
- package/scripts/pi-patch-report.mjs +224 -0
- package/scripts/pi-upgrade.mjs +829 -0
- package/scripts/render-regression.mjs +67 -5
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `u1s1 mcp-server`:标准输入输出(stdio)的 MCP 工具服务,给启动器带起的官方 Claude Code / Codex
|
|
3
|
+
* 补上 Pi 里已有的三样工具:联网搜索、读取网页、生成图片。
|
|
4
|
+
*
|
|
5
|
+
* 由客户端按 MCP 配置拉起(启动器每次启动时注入,见 agents/claude.ts、agents/codex.ts),是启动器的
|
|
6
|
+
* 孙进程。它拿不到设备凭证:环境里只有
|
|
7
|
+
* U1S1_MCP_PROXY 本机签名代理地址 http://127.0.0.1:<端口>/v1(只认回环地址)
|
|
8
|
+
* U1S1_MCP_TOKEN 这次启动有效的本地 token(与客户端自己用的那个相同,启动器退出即作废)
|
|
9
|
+
* U1S1_MCP_TOOLS 网关开了哪些能力(web_search、web_fetch_render、generate_image,逗号分隔)
|
|
10
|
+
* 到网关的请求全部经签名代理(代理负责 DPoP 签名、客户端证明和声明头),而且只走回环直连,
|
|
11
|
+
* 不经用户的 HTTP(S)_PROXY;读网页的直连抓取和下载生成的图片照常按用户的代理设置走(与 Pi 相同)。
|
|
12
|
+
*
|
|
13
|
+
* 工具的名字、参数、执行逻辑和报错文案直接复用 Pi 的实现(tools.ts),只有描述按站点语言重写、
|
|
14
|
+
* 生图结果的说明改成终端客户端适用的写法。
|
|
15
|
+
*
|
|
16
|
+
* 协议:手写的最小实现(不引 SDK),按 MCP 规范 2025-06-18 的 stdio 传输:每行一条 JSON-RPC 消息,
|
|
17
|
+
* stdout 只写协议消息,日志走 stderr。只提供 tools 能力。
|
|
18
|
+
*/
|
|
19
|
+
import { appendFileSync } from "node:fs";
|
|
20
|
+
import { createInterface } from "node:readline";
|
|
21
|
+
import { fileURLToPath } from "node:url";
|
|
22
|
+
import { Agent, getGlobalDispatcher, setGlobalDispatcher } from "undici";
|
|
23
|
+
import { VERSION } from "./config.js";
|
|
24
|
+
import { tr } from "./i18n.js";
|
|
25
|
+
export const MCP_SERVER_NAME = "u1s1";
|
|
26
|
+
export const MCP_PROXY_ENV = "U1S1_MCP_PROXY";
|
|
27
|
+
export const MCP_TOKEN_ENV = "U1S1_MCP_TOKEN";
|
|
28
|
+
export const MCP_TOOLS_ENV = "U1S1_MCP_TOOLS";
|
|
29
|
+
/** 关闭开关:U1S1_MCP=0(或 off / false / no)时启动器不注入 MCP 服务。 */
|
|
30
|
+
export const MCP_SWITCH_ENV = "U1S1_MCP";
|
|
31
|
+
/** 排查用:设成一个文件路径时,服务把收发的每条协议消息和启动信息(不含 token)追加写进去。 */
|
|
32
|
+
export const MCP_TRACE_ENV = "U1S1_MCP_TRACE";
|
|
33
|
+
/** 启动器写进客户端环境、再由客户端转给 MCP 服务的变量(客户端配置里只引用名字,不写值)。 */
|
|
34
|
+
export const MCP_ENV_NAMES = [MCP_PROXY_ENV, MCP_TOKEN_ENV, MCP_TOOLS_ENV, "U1S1_LANG"];
|
|
35
|
+
/** 我们支持的协议版本,新的在前。客户端要的版本在列表里就原样回,否则回第一个(规范的版本协商)。 */
|
|
36
|
+
export const MCP_PROTOCOL_VERSIONS = ["2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05"];
|
|
37
|
+
const FALLBACK_PROTOCOL_VERSION = "2025-06-18";
|
|
38
|
+
// ---------------------------------------------------------------- launcher side
|
|
39
|
+
/** 用户用环境变量关掉了注入。 */
|
|
40
|
+
export function mcpInjectionDisabled(env = process.env) {
|
|
41
|
+
const v = env[MCP_SWITCH_ENV]?.trim().toLowerCase();
|
|
42
|
+
return v === "0" || v === "off" || v === "false" || v === "no";
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* 网关能力 → U1S1_MCP_TOOLS。缺省值与 Pi 相同(web.ts / agents/pi.ts):搜索缺字段按开、
|
|
46
|
+
* 云端渲染和生图缺字段按关;拿不到 /v1/models 时(features 为 undefined)同样处理。
|
|
47
|
+
*/
|
|
48
|
+
export function mcpToolFlags(features) {
|
|
49
|
+
const flags = [];
|
|
50
|
+
if (features?.web_search !== false)
|
|
51
|
+
flags.push("web_search");
|
|
52
|
+
if (features?.web_fetch_render === true)
|
|
53
|
+
flags.push("web_fetch_render");
|
|
54
|
+
if (features?.image_gen === true)
|
|
55
|
+
flags.push("generate_image");
|
|
56
|
+
return flags.join(",");
|
|
57
|
+
}
|
|
58
|
+
/** 客户端拉起服务用的命令:同一个 node、同一份 CLI(dist/index.js),子命令 mcp-server。 */
|
|
59
|
+
export function mcpServerCommand() {
|
|
60
|
+
return { command: process.execPath, args: [fileURLToPath(new URL("./index.js", import.meta.url)), "mcp-server"] };
|
|
61
|
+
}
|
|
62
|
+
export function mcpLaunchEnv(input) {
|
|
63
|
+
if (!isLoopbackProxyUrl(input.proxyBaseUrl)) {
|
|
64
|
+
throw new Error(`MCP proxy must be the local signing proxy: ${input.proxyBaseUrl}`);
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
[MCP_PROXY_ENV]: input.proxyBaseUrl,
|
|
68
|
+
[MCP_TOKEN_ENV]: input.localToken,
|
|
69
|
+
[MCP_TOOLS_ENV]: mcpToolFlags(input.features),
|
|
70
|
+
U1S1_LANG: input.lang,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
export function isLoopbackProxyUrl(url) {
|
|
74
|
+
return /^http:\/\/127\.0\.0\.1:\d{1,5}\/v1$/.test(url);
|
|
75
|
+
}
|
|
76
|
+
/** TypeBox 对象本身就是 JSON Schema;序列化一遍去掉它的 symbol 键,得到纯 JSON。 */
|
|
77
|
+
function plainSchema(schema) {
|
|
78
|
+
return JSON.parse(JSON.stringify(schema));
|
|
79
|
+
}
|
|
80
|
+
function textOnly(result) {
|
|
81
|
+
return result.content
|
|
82
|
+
.filter((c) => c.type === "text" && typeof c.text === "string")
|
|
83
|
+
.map((c) => ({ type: "text", text: c.text }));
|
|
84
|
+
}
|
|
85
|
+
function toolDescriptions() {
|
|
86
|
+
return {
|
|
87
|
+
web_search: {
|
|
88
|
+
title: tr("联网搜索", "Web search"),
|
|
89
|
+
description: tr("联网搜索最新信息:文档、报错、版本号、近期事件等仓库里没有或可能过时的内容。返回简短答案和带链接、摘要的结果列表;" +
|
|
90
|
+
"要读全文再用 web_fetch。每次搜索按次计费(从用户的 u1s1 余额扣),优先用一个具体的查询,不要连发相近的查询。" +
|
|
91
|
+
"搜索只读查询的前 100 个字符,关键词尽量短。", "Search the web for current information: docs, error messages, latest versions, recent events, anything newer than " +
|
|
92
|
+
"your knowledge or outside the repository. Returns a short answer plus ranked results with URLs and snippets; use " +
|
|
93
|
+
"web_fetch to read a result in full. Each search is billed per query to the user's u1s1 balance, so prefer one " +
|
|
94
|
+
"focused query over several near-duplicates. Only the first 100 characters of the query are used; keep it short."),
|
|
95
|
+
},
|
|
96
|
+
web_fetch: {
|
|
97
|
+
title: tr("读取网页", "Fetch page"),
|
|
98
|
+
description: tr("读取一个网址并返回可读的正文(HTML 转成纯文本,JSON 和纯文本原样返回)。用来读 web_search 找到的页面或用户给的链接。" +
|
|
99
|
+
"先从本机直接访问(不计费);打不开或页面要靠 JavaScript 渲染时,如果服务端开了云端浏览器渲染,会自动改用它重试" +
|
|
100
|
+
"(这一次按次计费)。长页面按 30000 字符分段返回,结果末尾说被截断时,用同一个 url 和它给出的 offset 再调用一次读下一段。", "Fetch a URL and return its readable text (HTML is stripped to text; JSON and plain text are returned as-is). Use it " +
|
|
101
|
+
"to read a page found via web_search or a URL the user gave you. The page is fetched directly from this machine " +
|
|
102
|
+
"(free); if that fails or the page needs JavaScript and cloud rendering is enabled, it retries through a cloud " +
|
|
103
|
+
"browser (billed per render). Long pages come back in 30,000-character windows: when a result says it was " +
|
|
104
|
+
"truncated, call again with the same url and the offset it gives."),
|
|
105
|
+
},
|
|
106
|
+
generate_image: {
|
|
107
|
+
title: tr("生成图片", "Generate image"),
|
|
108
|
+
description: tr("根据文字描述生成图片,或在已有图片上修改、合成(images 里传本地路径或 http(s) 链接,最多 10 张)。结果保存为本地图片文件," +
|
|
109
|
+
"返回文件路径。每次调用生成一张图并按次计费(从用户的 u1s1 余额扣):先把描述写清楚,不要反复重新生成;" +
|
|
110
|
+
"报错里说图片可能已经生成、或让你不要再调用时,立即停止,不要重试。中英文描述都可以,写清主体、风格、构图、颜色和要出现的文字。", "Generate an image from a text prompt, or edit/compose existing images (pass local paths or http(s) URLs in " +
|
|
111
|
+
"`images`, up to 10). The result is saved as a local image file and its path is returned. Each call generates one " +
|
|
112
|
+
"image and is billed to the user's u1s1 balance: refine the prompt instead of regenerating repeatedly, and if an " +
|
|
113
|
+
"error says the image may already have been generated or not to call generate_image again, stop immediately and " +
|
|
114
|
+
"do not retry. Prompts work in Chinese or English; describe subject, style, composition, colors and any text to render."),
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
export function parseMcpToolsConfig(env) {
|
|
119
|
+
const proxy = env[MCP_PROXY_ENV]?.trim();
|
|
120
|
+
const token = env[MCP_TOKEN_ENV]?.trim();
|
|
121
|
+
const usable = !!proxy && !!token && isLoopbackProxyUrl(proxy);
|
|
122
|
+
return {
|
|
123
|
+
proxyBaseUrl: usable ? proxy : undefined,
|
|
124
|
+
localToken: usable ? token : undefined,
|
|
125
|
+
flags: new Set((env[MCP_TOOLS_ENV] ?? "").split(",").map((s) => s.trim()).filter(Boolean)),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* 按配置建工具。执行逻辑来自 Pi 的 tools.ts(动态引入:它会带进 Pi 的模块,只在这里用)。
|
|
130
|
+
* cfg 里只有代理地址和本地 token;没有设备凭证,api.ts 会以 Bearer <本地 token> 访问代理。
|
|
131
|
+
*/
|
|
132
|
+
export async function buildMcpTools(config) {
|
|
133
|
+
const piTools = await import("./tools.js");
|
|
134
|
+
const text = toolDescriptions();
|
|
135
|
+
const gateway = config.proxyBaseUrl && config.localToken
|
|
136
|
+
? { baseUrl: config.proxyBaseUrl, apiKey: config.localToken }
|
|
137
|
+
: undefined;
|
|
138
|
+
const tools = [];
|
|
139
|
+
let seq = 0;
|
|
140
|
+
const wrap = (name, tool, annotations, format = textOnly) => {
|
|
141
|
+
tools.push({
|
|
142
|
+
name,
|
|
143
|
+
...text[name],
|
|
144
|
+
inputSchema: plainSchema(tool.parameters),
|
|
145
|
+
annotations: { title: text[name].title, ...annotations },
|
|
146
|
+
async run(args, signal) {
|
|
147
|
+
const result = await tool.execute(`mcp-${++seq}`, args, signal);
|
|
148
|
+
return { content: format(result) };
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
};
|
|
152
|
+
if (gateway && config.flags.has("web_search")) {
|
|
153
|
+
wrap("web_search", piTools.createSearchTool(gateway), {
|
|
154
|
+
readOnlyHint: true,
|
|
155
|
+
openWorldHint: true,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
// 直连抓取不需要网关;云端渲染回退要网关开了且有代理
|
|
159
|
+
wrap("web_fetch", piTools.createFetchTool({
|
|
160
|
+
baseUrl: gateway?.baseUrl ?? "http://127.0.0.1:0/v1",
|
|
161
|
+
apiKey: gateway?.apiKey,
|
|
162
|
+
renderFallback: !!gateway && config.flags.has("web_fetch_render"),
|
|
163
|
+
}), { readOnlyHint: true, openWorldHint: true });
|
|
164
|
+
if (gateway && config.flags.has("generate_image")) {
|
|
165
|
+
wrap("generate_image", piTools.createImageTool(gateway), { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, (result) => {
|
|
166
|
+
const d = (result.details ?? {});
|
|
167
|
+
if (typeof d.path !== "string")
|
|
168
|
+
return textOnly(result);
|
|
169
|
+
const size = typeof d.size === "string" && d.size ? ` (${d.size})` : "";
|
|
170
|
+
// Pi 的文案是「已在对话中展示」;终端客户端不显示图片,改成给路径
|
|
171
|
+
return [{
|
|
172
|
+
type: "text",
|
|
173
|
+
text: tr(`图片已生成并保存到: ${d.path}${size}\n终端里不会直接显示图片;需要查看时告诉用户这个文件路径。不要再次调用 generate_image 来「显示」它。`, `Image generated and saved to: ${d.path}${size}\nThe terminal can't display images; give the user this file path to open it. Don't call generate_image again just to show it.`),
|
|
174
|
+
}];
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
return tools;
|
|
178
|
+
}
|
|
179
|
+
// ---------------------------------------------------------------- argument check
|
|
180
|
+
/** 只做 inputSchema 里用到的那几种检查(必填、类型、数值范围、数组元素为字符串),出错给模型能看懂的一句话。 */
|
|
181
|
+
export function checkArguments(schema, args) {
|
|
182
|
+
if (!args || typeof args !== "object" || Array.isArray(args))
|
|
183
|
+
return "arguments must be an object";
|
|
184
|
+
const value = args;
|
|
185
|
+
const props = (schema["properties"] ?? {});
|
|
186
|
+
for (const key of (schema["required"] ?? [])) {
|
|
187
|
+
if (value[key] === undefined || value[key] === null)
|
|
188
|
+
return `missing required argument: ${key}`;
|
|
189
|
+
}
|
|
190
|
+
for (const [key, v] of Object.entries(value)) {
|
|
191
|
+
const p = props[key];
|
|
192
|
+
if (!p || v === undefined || v === null)
|
|
193
|
+
continue;
|
|
194
|
+
const type = p["type"];
|
|
195
|
+
if (type === "string" && typeof v !== "string")
|
|
196
|
+
return `${key} must be a string`;
|
|
197
|
+
if (type === "number" || type === "integer") {
|
|
198
|
+
if (typeof v !== "number" || !Number.isFinite(v))
|
|
199
|
+
return `${key} must be a number`;
|
|
200
|
+
if (typeof p["minimum"] === "number" && v < p["minimum"])
|
|
201
|
+
return `${key} must be >= ${p["minimum"]}`;
|
|
202
|
+
if (typeof p["maximum"] === "number" && v > p["maximum"])
|
|
203
|
+
return `${key} must be <= ${p["maximum"]}`;
|
|
204
|
+
}
|
|
205
|
+
if (type === "array") {
|
|
206
|
+
if (!Array.isArray(v))
|
|
207
|
+
return `${key} must be an array`;
|
|
208
|
+
const items = p["items"];
|
|
209
|
+
if (items?.["type"] === "string" && v.some((x) => typeof x !== "string"))
|
|
210
|
+
return `${key} must be an array of strings`;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
const PARSE_ERROR = -32700;
|
|
216
|
+
const INVALID_REQUEST = -32600;
|
|
217
|
+
const METHOD_NOT_FOUND = -32601;
|
|
218
|
+
const INVALID_PARAMS = -32602;
|
|
219
|
+
/**
|
|
220
|
+
* 协议核心:喂进一行,按需回一行。与传输无关,测试直接驱动它。
|
|
221
|
+
* 请求并发处理;notifications/cancelled 中止对应的 tools/call,之后不再回结果(规范要求)。
|
|
222
|
+
*/
|
|
223
|
+
export class McpServer {
|
|
224
|
+
options;
|
|
225
|
+
inflight = new Map();
|
|
226
|
+
toolsPromise;
|
|
227
|
+
constructor(options) {
|
|
228
|
+
this.options = options;
|
|
229
|
+
}
|
|
230
|
+
tools() {
|
|
231
|
+
const t = this.options.tools;
|
|
232
|
+
this.toolsPromise ??= Array.isArray(t) ? Promise.resolve(t) : t();
|
|
233
|
+
return this.toolsPromise;
|
|
234
|
+
}
|
|
235
|
+
reply(id, payload) {
|
|
236
|
+
this.options.send(JSON.stringify({ jsonrpc: "2.0", id, ...payload }));
|
|
237
|
+
}
|
|
238
|
+
/** 处理一行输入。返回的 Promise 在这条消息处理完(含工具执行)后落定。 */
|
|
239
|
+
async handleLine(line) {
|
|
240
|
+
const trimmed = line.trim();
|
|
241
|
+
if (!trimmed)
|
|
242
|
+
return;
|
|
243
|
+
let parsed;
|
|
244
|
+
try {
|
|
245
|
+
parsed = JSON.parse(trimmed);
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
this.reply(null, { error: { code: PARSE_ERROR, message: "Parse error" } });
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (Array.isArray(parsed)) {
|
|
252
|
+
// 2025-06-18 起不再支持批量;老版本的批量逐条处理
|
|
253
|
+
await Promise.all(parsed.map((m) => this.handleMessage(m)));
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
await this.handleMessage(parsed);
|
|
257
|
+
}
|
|
258
|
+
/** 关闭:中止所有在执行的工具调用。 */
|
|
259
|
+
abortAll() {
|
|
260
|
+
for (const c of this.inflight.values())
|
|
261
|
+
c.abort(new Error("MCP server shutting down"));
|
|
262
|
+
this.inflight.clear();
|
|
263
|
+
}
|
|
264
|
+
async handleMessage(raw) {
|
|
265
|
+
if (!raw || typeof raw !== "object") {
|
|
266
|
+
this.reply(null, { error: { code: INVALID_REQUEST, message: "Invalid Request" } });
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
const msg = raw;
|
|
270
|
+
const isRequest = msg.id !== undefined && msg.id !== null;
|
|
271
|
+
if (typeof msg.method !== "string") {
|
|
272
|
+
// 客户端发来的响应(我们不发请求,所以不会有)或无效消息
|
|
273
|
+
if (isRequest && !("result" in msg) && !("error" in msg)) {
|
|
274
|
+
this.reply(msg.id, { error: { code: INVALID_REQUEST, message: "Invalid Request" } });
|
|
275
|
+
}
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
if (!isRequest) {
|
|
279
|
+
this.handleNotification(msg.method, msg.params);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
const id = msg.id;
|
|
283
|
+
try {
|
|
284
|
+
switch (msg.method) {
|
|
285
|
+
case "initialize":
|
|
286
|
+
this.reply(id, { result: this.initializeResult(msg.params) });
|
|
287
|
+
return;
|
|
288
|
+
case "ping":
|
|
289
|
+
this.reply(id, { result: {} });
|
|
290
|
+
return;
|
|
291
|
+
case "tools/list": {
|
|
292
|
+
const tools = await this.tools();
|
|
293
|
+
this.reply(id, {
|
|
294
|
+
result: {
|
|
295
|
+
tools: tools.map((t) => ({
|
|
296
|
+
name: t.name,
|
|
297
|
+
title: t.title,
|
|
298
|
+
description: t.description,
|
|
299
|
+
inputSchema: t.inputSchema,
|
|
300
|
+
annotations: t.annotations,
|
|
301
|
+
})),
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
case "tools/call":
|
|
307
|
+
await this.callTool(id, msg.params);
|
|
308
|
+
return;
|
|
309
|
+
default:
|
|
310
|
+
this.reply(id, { error: { code: METHOD_NOT_FOUND, message: `Method not found: ${msg.method}` } });
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
catch (e) {
|
|
314
|
+
this.options.log?.(`mcp-server: ${msg.method} failed: ${e.stack ?? String(e)}`);
|
|
315
|
+
this.reply(id, { error: { code: -32603, message: e.message || "Internal error" } });
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
handleNotification(method, params) {
|
|
319
|
+
if (method === "notifications/cancelled") {
|
|
320
|
+
const requestId = params?.requestId;
|
|
321
|
+
if (requestId === undefined)
|
|
322
|
+
return;
|
|
323
|
+
const controller = this.inflight.get(requestId);
|
|
324
|
+
if (controller) {
|
|
325
|
+
this.inflight.delete(requestId);
|
|
326
|
+
controller.abort(new Error("cancelled by client"));
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
// notifications/initialized 等其余通知:无需处理
|
|
330
|
+
}
|
|
331
|
+
initializeResult(params) {
|
|
332
|
+
const requested = params?.protocolVersion;
|
|
333
|
+
const protocolVersion = typeof requested === "string" && MCP_PROTOCOL_VERSIONS.includes(requested)
|
|
334
|
+
? requested
|
|
335
|
+
: FALLBACK_PROTOCOL_VERSION;
|
|
336
|
+
return {
|
|
337
|
+
protocolVersion,
|
|
338
|
+
capabilities: { tools: { listChanged: false } },
|
|
339
|
+
serverInfo: { name: MCP_SERVER_NAME, title: "u1s1", version: VERSION },
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
async callTool(id, params) {
|
|
343
|
+
const p = (params ?? {});
|
|
344
|
+
const tools = await this.tools();
|
|
345
|
+
const tool = tools.find((t) => t.name === p.name);
|
|
346
|
+
if (!tool) {
|
|
347
|
+
this.reply(id, { error: { code: INVALID_PARAMS, message: `Unknown tool: ${String(p.name)}` } });
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
const args = p.arguments ?? {};
|
|
351
|
+
const invalid = checkArguments(tool.inputSchema, args);
|
|
352
|
+
if (invalid) {
|
|
353
|
+
// 参数问题按工具执行错误回(isError),模型能看到原因并改正后重试
|
|
354
|
+
this.reply(id, { result: { content: [{ type: "text", text: `Invalid arguments for ${tool.name}: ${invalid}` }], isError: true } });
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
const controller = new AbortController();
|
|
358
|
+
this.inflight.set(id, controller);
|
|
359
|
+
let result;
|
|
360
|
+
try {
|
|
361
|
+
result = await tool.run(args, controller.signal);
|
|
362
|
+
}
|
|
363
|
+
catch (e) {
|
|
364
|
+
result = { content: [{ type: "text", text: e?.message || String(e) }], isError: true };
|
|
365
|
+
}
|
|
366
|
+
if (controller.signal.aborted)
|
|
367
|
+
return; // 已取消:不回结果
|
|
368
|
+
this.inflight.delete(id);
|
|
369
|
+
this.reply(id, { result });
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
// ---------------------------------------------------------------- transport & entry
|
|
373
|
+
function isLoopbackOrigin(origin) {
|
|
374
|
+
try {
|
|
375
|
+
const host = new URL(String(origin)).hostname;
|
|
376
|
+
return host === "127.0.0.1" || host === "localhost" || host === "[::1]";
|
|
377
|
+
}
|
|
378
|
+
catch {
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* 本进程的全局 dispatcher:发往回环地址(签名代理)的请求直连,其余照旧交给原来的 dispatcher
|
|
384
|
+
* (按用户的 HTTP(S)_PROXY 走)。tools.ts / api.ts 用全局 fetch,这样不用改它们。
|
|
385
|
+
*/
|
|
386
|
+
export function installLoopbackDirectDispatcher() {
|
|
387
|
+
const fallback = getGlobalDispatcher();
|
|
388
|
+
const direct = new Agent();
|
|
389
|
+
const routed = fallback.compose((dispatch) => (opts, handler) => isLoopbackOrigin(opts.origin) ? direct.dispatch(opts, handler) : dispatch(opts, handler));
|
|
390
|
+
setGlobalDispatcher(routed);
|
|
391
|
+
}
|
|
392
|
+
/** `u1s1 mcp-server` 入口:stdin 一行一条消息,stdout 只写协议消息。stdin 关闭即退出。 */
|
|
393
|
+
export async function runMcpServer() {
|
|
394
|
+
// 任何模块往 stdout 打印都会弄坏协议:把 console 的 stdout 输出改到 stderr
|
|
395
|
+
const toStderr = (...args) => console.error(...args);
|
|
396
|
+
console.log = toStderr;
|
|
397
|
+
console.info = toStderr;
|
|
398
|
+
console.debug = toStderr;
|
|
399
|
+
installLoopbackDirectDispatcher();
|
|
400
|
+
if (process.stdin.isTTY) {
|
|
401
|
+
console.error(tr("u1s1 mcp-server 是给 Claude Code / Codex 用的 MCP 工具服务,由 u1s1 claude / u1s1 codex 自动配置,不用手动运行。按 Ctrl+D 退出。", "u1s1 mcp-server is the MCP tool server for Claude Code / Codex; u1s1 claude / u1s1 codex configure it automatically, so there's no need to run it by hand. Press Ctrl+D to exit."));
|
|
402
|
+
}
|
|
403
|
+
const config = parseMcpToolsConfig(process.env);
|
|
404
|
+
const tracePath = process.env[MCP_TRACE_ENV]?.trim();
|
|
405
|
+
const trace = tracePath
|
|
406
|
+
? (dir, line) => {
|
|
407
|
+
try {
|
|
408
|
+
appendFileSync(tracePath, `${JSON.stringify({ at: new Date().toISOString(), pid: process.pid, dir, line })}\n`, { mode: 0o600 });
|
|
409
|
+
}
|
|
410
|
+
catch {
|
|
411
|
+
// 排查文件写不进去不影响服务
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
: undefined;
|
|
415
|
+
trace?.("info", JSON.stringify({
|
|
416
|
+
cwd: process.cwd(),
|
|
417
|
+
proxy: config.proxyBaseUrl ?? null,
|
|
418
|
+
token: config.localToken ? "present" : "missing",
|
|
419
|
+
flags: [...config.flags],
|
|
420
|
+
lang: process.env["U1S1_LANG"] ?? null,
|
|
421
|
+
envKeys: Object.keys(process.env).sort(),
|
|
422
|
+
}));
|
|
423
|
+
// 环境里只该有本地 token;即使有人手工设了设备凭证变量,也别留给工具代码
|
|
424
|
+
for (const key of ["U1S1_DEVICE_TOKEN", "U1S1_DEVICE_PRIVATE_JWK", "U1S1_API_KEY"])
|
|
425
|
+
delete process.env[key];
|
|
426
|
+
delete process.env[MCP_TOKEN_ENV];
|
|
427
|
+
const server = new McpServer({
|
|
428
|
+
tools: () => buildMcpTools(config),
|
|
429
|
+
send: (line) => {
|
|
430
|
+
trace?.("out", line);
|
|
431
|
+
process.stdout.write(`${line}\n`);
|
|
432
|
+
},
|
|
433
|
+
log: (line) => process.stderr.write(`${line}\n`),
|
|
434
|
+
});
|
|
435
|
+
const pending = new Set();
|
|
436
|
+
const rl = createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
437
|
+
rl.on("line", (line) => {
|
|
438
|
+
trace?.("in", line);
|
|
439
|
+
const p = server.handleLine(line).catch((e) => {
|
|
440
|
+
process.stderr.write(`mcp-server: ${String(e)}\n`);
|
|
441
|
+
});
|
|
442
|
+
pending.add(p);
|
|
443
|
+
void p.finally(() => pending.delete(p));
|
|
444
|
+
});
|
|
445
|
+
await new Promise((resolve) => rl.once("close", resolve));
|
|
446
|
+
// 客户端关了输入:不再有人接结果,停掉在跑的调用
|
|
447
|
+
server.abortAll();
|
|
448
|
+
await Promise.race([Promise.allSettled(pending), new Promise((r) => setTimeout(r, 1_000).unref())]);
|
|
449
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pi 内置斜杠命令的补全描述、「思考中」提示的 u1s1 文案(中英),以及把它们装进 Pi 的扩展逻辑。
|
|
3
|
+
*
|
|
4
|
+
* 以前这 26 处是改 Pi 源码的补丁(patch-pi.js 的 SLASH_COMMANDS 24 条、WORKING 2 条)。现在改用
|
|
5
|
+
* Pi 公开的扩展 API,由 agents/pi.ts 的进程内扩展(main 的 extensionFactories)调用 registerUiText:
|
|
6
|
+
* - 进程内扩展不受 --no-extensions、设置里禁用 agentDir 扩展影响,跟以前的补丁一样总是生效;
|
|
7
|
+
* Desktop App 也不会加载它(它只读 agentDir 里的扩展文件)。
|
|
8
|
+
* - session_start 时 ctx.ui.setWorkingMessage / setHiddenThinkingLabel;/reload、换会话前 Pi 会
|
|
9
|
+
* resetExtensionUI 清掉这些设置,随后的 session_start 再设一遍。
|
|
10
|
+
* - ctx.ui.addAutocompleteProvider 包一层补全:内置命令的描述按 U1S1_LANG 换成下表文案,并去掉 /bug。
|
|
11
|
+
* 只改补全列表;手动输入 /bug 仍由补丁 BUG_REPORT[0] 拦截。
|
|
12
|
+
*
|
|
13
|
+
* 文案必须与原补丁逐字相同(test/pi-ui-text.test.ts 对照删补丁前的快照)。上游新增或改名内置
|
|
14
|
+
* 命令时,同一测试会对照已安装 Pi 的 BUILTIN_SLASH_COMMANDS 失败,提醒补表。
|
|
15
|
+
*/
|
|
16
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
17
|
+
import type { AutocompleteItem, AutocompleteProvider } from "@earendil-works/pi-tui";
|
|
18
|
+
/**
|
|
19
|
+
* 上一版(P5-B)投影到 <agentDir>/extensions 下的扩展文件名。现在改为进程内扩展,
|
|
20
|
+
* agent-setup.ts 的 removeLegacyUiTextExtension 启动时删掉这个残留文件。
|
|
21
|
+
*/
|
|
22
|
+
export declare const LEGACY_UI_TEXT_EXTENSION_FILE = "u1s1-ui-text.js";
|
|
23
|
+
/** 描述里的应用名占位:/quit 的上游原文是 `Quit ${APP_NAME}`,运行时从现有描述里取回实际名字。 */
|
|
24
|
+
export declare const APP_NAME_PLACEHOLDER = "{app}";
|
|
25
|
+
export interface BuiltinCommandText {
|
|
26
|
+
name: string;
|
|
27
|
+
argumentHint?: string;
|
|
28
|
+
/** Pi 上游英文原文 */
|
|
29
|
+
upstream: string;
|
|
30
|
+
/** 中国站文案 */
|
|
31
|
+
zh?: string;
|
|
32
|
+
/** 国际站文案;不写就是上游原文 */
|
|
33
|
+
en?: string;
|
|
34
|
+
/** 从补全里去掉(/bug:诊断会上传给 Pi 上游,u1s1 用户的问题走 /feedback) */
|
|
35
|
+
hide?: true;
|
|
36
|
+
}
|
|
37
|
+
/** 顺序与 Pi 1.2.0 的 BUILTIN_SLASH_COMMANDS 相同。 */
|
|
38
|
+
export declare const BUILTIN_COMMAND_TEXT: readonly BuiltinCommandText[];
|
|
39
|
+
/** 流式回复时的工作提示(Pi 默认 "Working")。 */
|
|
40
|
+
export declare const WORKING_MESSAGE: {
|
|
41
|
+
readonly zh: "思考中…";
|
|
42
|
+
readonly en: "Working";
|
|
43
|
+
};
|
|
44
|
+
/** 隐藏思考块时的占位标签(Pi 默认 "Thinking...")。 */
|
|
45
|
+
export declare const HIDDEN_THINKING_LABEL: {
|
|
46
|
+
readonly zh: "思考中…";
|
|
47
|
+
readonly en: "Thinking...";
|
|
48
|
+
};
|
|
49
|
+
/** 把补全项里的内置命令描述换成当前语言的文案、去掉隐藏的命令;没有改动时返回原数组。 */
|
|
50
|
+
export declare function localizeSlashItems(items: AutocompleteItem[], en: boolean): AutocompleteItem[];
|
|
51
|
+
/** 包一层补全 provider:只处理「/命令名」的补全,参数补全、文件补全原样返回。 */
|
|
52
|
+
export declare function wrapAutocompleteProvider(current: AutocompleteProvider): AutocompleteProvider;
|
|
53
|
+
/**
|
|
54
|
+
* 注册到 Pi 扩展上:session_start 时设工作提示、隐藏思考标签,包一层补全。
|
|
55
|
+
* 只在 TUI 生效:print/json 没有界面,rpc 的 ctx.ui 这三个方法都是空实现。
|
|
56
|
+
*/
|
|
57
|
+
export declare function registerUiText(pi: Pick<ExtensionAPI, "on">): void;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pi 内置斜杠命令的补全描述、「思考中」提示的 u1s1 文案(中英),以及把它们装进 Pi 的扩展逻辑。
|
|
3
|
+
*
|
|
4
|
+
* 以前这 26 处是改 Pi 源码的补丁(patch-pi.js 的 SLASH_COMMANDS 24 条、WORKING 2 条)。现在改用
|
|
5
|
+
* Pi 公开的扩展 API,由 agents/pi.ts 的进程内扩展(main 的 extensionFactories)调用 registerUiText:
|
|
6
|
+
* - 进程内扩展不受 --no-extensions、设置里禁用 agentDir 扩展影响,跟以前的补丁一样总是生效;
|
|
7
|
+
* Desktop App 也不会加载它(它只读 agentDir 里的扩展文件)。
|
|
8
|
+
* - session_start 时 ctx.ui.setWorkingMessage / setHiddenThinkingLabel;/reload、换会话前 Pi 会
|
|
9
|
+
* resetExtensionUI 清掉这些设置,随后的 session_start 再设一遍。
|
|
10
|
+
* - ctx.ui.addAutocompleteProvider 包一层补全:内置命令的描述按 U1S1_LANG 换成下表文案,并去掉 /bug。
|
|
11
|
+
* 只改补全列表;手动输入 /bug 仍由补丁 BUG_REPORT[0] 拦截。
|
|
12
|
+
*
|
|
13
|
+
* 文案必须与原补丁逐字相同(test/pi-ui-text.test.ts 对照删补丁前的快照)。上游新增或改名内置
|
|
14
|
+
* 命令时,同一测试会对照已安装 Pi 的 BUILTIN_SLASH_COMMANDS 失败,提醒补表。
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* 上一版(P5-B)投影到 <agentDir>/extensions 下的扩展文件名。现在改为进程内扩展,
|
|
18
|
+
* agent-setup.ts 的 removeLegacyUiTextExtension 启动时删掉这个残留文件。
|
|
19
|
+
*/
|
|
20
|
+
export const LEGACY_UI_TEXT_EXTENSION_FILE = "u1s1-ui-text.js";
|
|
21
|
+
/** 描述里的应用名占位:/quit 的上游原文是 `Quit ${APP_NAME}`,运行时从现有描述里取回实际名字。 */
|
|
22
|
+
export const APP_NAME_PLACEHOLDER = "{app}";
|
|
23
|
+
/** 顺序与 Pi 1.2.0 的 BUILTIN_SLASH_COMMANDS 相同。 */
|
|
24
|
+
export const BUILTIN_COMMAND_TEXT = [
|
|
25
|
+
{ name: "settings", upstream: "Open settings menu", zh: "打开设置菜单" },
|
|
26
|
+
{ name: "model", argumentHint: "<provider/model>", upstream: "Select model (opens selector UI)", zh: "切换模型(列表里有免费/价格提示)" },
|
|
27
|
+
{ name: "tree", upstream: "Navigate session tree (switch branches)", zh: "查看会话分支树,切换分支" },
|
|
28
|
+
{ name: "thinking", argumentHint: "<level>", upstream: "Set thinking level", zh: "调整思考强度(想得越深越费额度)" },
|
|
29
|
+
{ name: "scoped-models", upstream: "Enable/disable models for Ctrl+P cycling", zh: "设置 Ctrl+P 轮换的模型范围" },
|
|
30
|
+
{ name: "export", upstream: "Export session (HTML default, or specify path: .html/.jsonl)", zh: "导出会话(默认 HTML,也可指定 .html/.jsonl 路径)" },
|
|
31
|
+
{ name: "import", upstream: "Import and resume a session from a JSONL file", zh: "从 JSONL 文件导入并继续会话" },
|
|
32
|
+
{ name: "share", upstream: "Share session as a secret GitHub gist", zh: "把会话分享为 GitHub 私密 gist" },
|
|
33
|
+
{ name: "bug", argumentHint: "<description>", upstream: "Report a bug to the Pi developers", hide: true },
|
|
34
|
+
{ name: "copy", upstream: "Copy last agent message to clipboard", zh: "复制上一条回复到剪贴板" },
|
|
35
|
+
{ name: "name", upstream: "Set session display name", zh: "给当前会话起个名字" },
|
|
36
|
+
{ name: "session", upstream: "Show session info and stats", zh: "查看会话信息与统计" },
|
|
37
|
+
{ name: "changelog", upstream: "Show changelog entries", zh: "查看引擎更新日志(英文)" },
|
|
38
|
+
{ name: "hotkeys", upstream: "Show all keyboard shortcuts", zh: "查看全部快捷键" },
|
|
39
|
+
{ name: "fork", upstream: "Create a new fork from a previous user message", zh: "从之前某条消息分叉出一个新会话" },
|
|
40
|
+
{ name: "clone", upstream: "Duplicate the current session at the current position", zh: "复制当前会话再继续" },
|
|
41
|
+
{ name: "trust", upstream: "Save project trust decision for future sessions", zh: "记住对这个项目目录的信任选择" },
|
|
42
|
+
// /login /logout 管的是 Pi 自己的渠道登录,u1s1 账号要退出 TUI 用 u1s1 login/logout,英文也照这个意思写
|
|
43
|
+
{
|
|
44
|
+
name: "login",
|
|
45
|
+
argumentHint: "<provider>",
|
|
46
|
+
upstream: "Configure provider authentication",
|
|
47
|
+
zh: "换 u1s1 账号请先 /exit,再运行 u1s1 login",
|
|
48
|
+
en: "To switch u1s1 accounts, /exit and run u1s1 login",
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: "logout",
|
|
52
|
+
upstream: "Remove provider authentication",
|
|
53
|
+
zh: "退出登录请先 /exit,再运行 u1s1 logout",
|
|
54
|
+
en: "To sign out, /exit and run u1s1 logout",
|
|
55
|
+
},
|
|
56
|
+
{ name: "new", upstream: "Start a new session", zh: "开始新会话" },
|
|
57
|
+
{ name: "compact", upstream: "Manually compact the session context", zh: "压缩会话上下文,腾出空间继续聊" },
|
|
58
|
+
{ name: "resume", upstream: "Resume a different session", zh: "恢复或切换到其他会话" },
|
|
59
|
+
{ name: "reload", upstream: "Reload keybindings, extensions, skills, prompts, themes, and context files", zh: "重新加载扩展、技能、主题等资源" },
|
|
60
|
+
{ name: "quit", upstream: `Quit ${APP_NAME_PLACEHOLDER}`, zh: `退出 ${APP_NAME_PLACEHOLDER}` },
|
|
61
|
+
];
|
|
62
|
+
/** 流式回复时的工作提示(Pi 默认 "Working")。 */
|
|
63
|
+
export const WORKING_MESSAGE = { zh: "思考中…", en: "Working" };
|
|
64
|
+
/** 隐藏思考块时的占位标签(Pi 默认 "Thinking...")。 */
|
|
65
|
+
export const HIDDEN_THINKING_LABEL = { zh: "思考中…", en: "Thinking..." };
|
|
66
|
+
const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
67
|
+
/** 与 pi-tui CombinedAutocompleteProvider 拼补全描述的方式相同 */
|
|
68
|
+
const withHint = (hint, text) => (hint ? (text ? `${hint} — ${text}` : hint) : text);
|
|
69
|
+
// 每条命令认三种现有描述:上游原文,以及旧版 u1s1 补丁打过的中文、英文(从旧版原地升级时
|
|
70
|
+
// node_modules 里的 Pi 可能还带着那份补丁;上一版投影的 u1s1-ui-text.js 还在时它已先换过一遍)。
|
|
71
|
+
// 都换成当前语言的文案,不叠加、不混语言。
|
|
72
|
+
const RULES = new Map(BUILTIN_COMMAND_TEXT.map((c) => {
|
|
73
|
+
const en = c.en ?? c.upstream;
|
|
74
|
+
const patterns = [c.upstream, c.zh, en]
|
|
75
|
+
.filter((t) => typeof t === "string")
|
|
76
|
+
.map((t) => new RegExp(`^${escapeRe(withHint(c.argumentHint, t)).split(escapeRe(APP_NAME_PLACEHOLDER)).join("(.+)")}$`));
|
|
77
|
+
return [c.name, { argumentHint: c.argumentHint, zh: c.zh ?? en, en, hide: c.hide === true, patterns }];
|
|
78
|
+
}));
|
|
79
|
+
/** 把补全项里的内置命令描述换成当前语言的文案、去掉隐藏的命令;没有改动时返回原数组。 */
|
|
80
|
+
export function localizeSlashItems(items, en) {
|
|
81
|
+
let changed = false;
|
|
82
|
+
const out = [];
|
|
83
|
+
for (const item of items) {
|
|
84
|
+
// 内置命令的补全项 value 与 label 都是命令名;同名的提示词模板描述不同,匹配不上,原样保留
|
|
85
|
+
const rule = item && item.label === item.value ? RULES.get(item.value) : undefined;
|
|
86
|
+
let match = null;
|
|
87
|
+
for (const re of rule?.patterns ?? []) {
|
|
88
|
+
match = re.exec(item.description ?? "");
|
|
89
|
+
if (match)
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
if (!rule || !match) {
|
|
93
|
+
out.push(item);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
changed = true;
|
|
97
|
+
if (rule.hide)
|
|
98
|
+
continue;
|
|
99
|
+
const text = (en ? rule.en : rule.zh).split(APP_NAME_PLACEHOLDER).join(match[1] ?? "");
|
|
100
|
+
out.push({ ...item, description: withHint(rule.argumentHint, text) });
|
|
101
|
+
}
|
|
102
|
+
return changed ? out : items;
|
|
103
|
+
}
|
|
104
|
+
/** 包一层补全 provider:只处理「/命令名」的补全,参数补全、文件补全原样返回。 */
|
|
105
|
+
export function wrapAutocompleteProvider(current) {
|
|
106
|
+
return {
|
|
107
|
+
// Pi 会给最外层 provider 赋值 triggerCharacters,这里必须是可写的数据属性
|
|
108
|
+
triggerCharacters: current.triggerCharacters,
|
|
109
|
+
async getSuggestions(lines, cursorLine, cursorCol, options) {
|
|
110
|
+
const result = await current.getSuggestions(lines, cursorLine, cursorCol, options);
|
|
111
|
+
if (!result || !Array.isArray(result.items) || typeof result.prefix !== "string" || !result.prefix.startsWith("/")) {
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
// 语言在运行时读 U1S1_LANG(pi.ts 在加载 Pi 前写好),与原补丁的求值方式相同
|
|
115
|
+
const items = localizeSlashItems(result.items, process.env["U1S1_LANG"] === "en");
|
|
116
|
+
if (items === result.items)
|
|
117
|
+
return result;
|
|
118
|
+
return items.length > 0 ? { ...result, items } : null;
|
|
119
|
+
},
|
|
120
|
+
applyCompletion: (...args) => current.applyCompletion(...args),
|
|
121
|
+
shouldTriggerFileCompletion: current.shouldTriggerFileCompletion
|
|
122
|
+
? (...args) => current.shouldTriggerFileCompletion(...args)
|
|
123
|
+
: undefined,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* 注册到 Pi 扩展上:session_start 时设工作提示、隐藏思考标签,包一层补全。
|
|
128
|
+
* 只在 TUI 生效:print/json 没有界面,rpc 的 ctx.ui 这三个方法都是空实现。
|
|
129
|
+
*/
|
|
130
|
+
export function registerUiText(pi) {
|
|
131
|
+
pi.on("session_start", (_event, ctx) => {
|
|
132
|
+
if (ctx.mode !== "tui" || !ctx.hasUI)
|
|
133
|
+
return;
|
|
134
|
+
const en = process.env["U1S1_LANG"] === "en";
|
|
135
|
+
try {
|
|
136
|
+
ctx.ui.setWorkingMessage(en ? WORKING_MESSAGE.en : WORKING_MESSAGE.zh);
|
|
137
|
+
}
|
|
138
|
+
catch { }
|
|
139
|
+
try {
|
|
140
|
+
ctx.ui.setHiddenThinkingLabel(en ? HIDDEN_THINKING_LABEL.en : HIDDEN_THINKING_LABEL.zh);
|
|
141
|
+
}
|
|
142
|
+
catch { }
|
|
143
|
+
try {
|
|
144
|
+
ctx.ui.addAutocompleteProvider(wrapAutocompleteProvider);
|
|
145
|
+
}
|
|
146
|
+
catch { }
|
|
147
|
+
});
|
|
148
|
+
}
|