u1s1-cli 0.15.0 → 0.16.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/bench/suite.json +123 -14
- package/dist/agent-setup.d.ts +7 -0
- package/dist/agent-setup.js +93 -0
- package/dist/bench.js +245 -97
- package/dist/index.js +6 -1
- package/dist/loop.d.ts +2 -0
- package/dist/loop.js +134 -0
- package/package.json +4 -2
- package/scripts/patch-pi.js +85 -0
package/bench/suite.json
CHANGED
|
@@ -6,12 +6,26 @@
|
|
|
6
6
|
{
|
|
7
7
|
"id": "hello",
|
|
8
8
|
"category": "基础",
|
|
9
|
-
"prompt": "只说一句「你好,我是 u1s1」就行,不要多余的话。"
|
|
9
|
+
"prompt": "只说一句「你好,我是 u1s1」就行,不要多余的话。",
|
|
10
|
+
"maxScore": 5,
|
|
11
|
+
"checks": [
|
|
12
|
+
{ "type": "contains", "value": "你好", "ignoreCase": false, "points": 2, "description": "包含「你好」" },
|
|
13
|
+
{ "type": "contains", "value": "u1s1", "ignoreCase": true, "points": 2, "description": "包含「u1s1」" },
|
|
14
|
+
{ "type": "max_length", "value": 50, "points": 1, "description": "不超过 50 字(啰嗦扣分)" }
|
|
15
|
+
]
|
|
10
16
|
},
|
|
11
17
|
{
|
|
12
18
|
"id": "reverse",
|
|
13
19
|
"category": "编码",
|
|
14
|
-
"prompt": "写一个 TypeScript 函数,把字符串里的单词顺序反转(不是字符反转)。示例: 'hello world' → 'world hello'。只给代码,不要解释。"
|
|
20
|
+
"prompt": "写一个 TypeScript 函数,把字符串里的单词顺序反转(不是字符反转)。示例: 'hello world' → 'world hello'。只给代码,不要解释。",
|
|
21
|
+
"maxScore": 10,
|
|
22
|
+
"checks": [
|
|
23
|
+
{ "type": "contains_code_block", "value": true, "points": 2, "description": "包含代码块" },
|
|
24
|
+
{ "type": "contains", "value": "reverse", "ignoreCase": false, "points": 2, "description": "函数名包含 reverse" },
|
|
25
|
+
{ "type": "contains", "value": "split", "ignoreCase": false, "points": 2, "description": "用了 split" },
|
|
26
|
+
{ "type": "contains", "value": "reverse(", "ignoreCase": false, "points": 2, "description": "调用了数组 reverse" },
|
|
27
|
+
{ "type": "contains", "value": "join", "ignoreCase": false, "points": 2, "description": "用了 join" }
|
|
28
|
+
]
|
|
15
29
|
}
|
|
16
30
|
]
|
|
17
31
|
},
|
|
@@ -22,62 +36,157 @@
|
|
|
22
36
|
{
|
|
23
37
|
"id": "fizzbuzz",
|
|
24
38
|
"category": "编码",
|
|
25
|
-
"prompt": "用 TypeScript 写 fizzbuzz,从 1 到 100,3 的倍数打印 fizz,5 的倍数打印 buzz,同时是 3 和 5 的倍数打印 fizzbuzz。只给代码,不要解释。"
|
|
39
|
+
"prompt": "用 TypeScript 写 fizzbuzz,从 1 到 100,3 的倍数打印 fizz,5 的倍数打印 buzz,同时是 3 和 5 的倍数打印 fizzbuzz。只给代码,不要解释。",
|
|
40
|
+
"maxScore": 10,
|
|
41
|
+
"checks": [
|
|
42
|
+
{ "type": "contains_code_block", "value": true, "points": 1, "description": "包含代码" },
|
|
43
|
+
{ "type": "contains", "value": "% 3", "ignoreCase": false, "points": 2, "description": "判断 3 的倍数" },
|
|
44
|
+
{ "type": "contains", "value": "% 5", "ignoreCase": false, "points": 2, "description": "判断 5 的倍数" },
|
|
45
|
+
{ "type": "contains", "value": "fizzbuzz", "ignoreCase": false, "points": 2, "description": "输出了 fizzbuzz" },
|
|
46
|
+
{ "type": "contains", "value": "100", "ignoreCase": false, "points": 1.5, "description": "循环到 100" },
|
|
47
|
+
{ "type": "not_contains", "value": "解释", "ignoreCase": true, "points": 1.5, "description": "没有多余解释" }
|
|
48
|
+
]
|
|
26
49
|
},
|
|
27
50
|
{
|
|
28
51
|
"id": "sort-algo",
|
|
29
52
|
"category": "编码",
|
|
30
|
-
"prompt": "用 TypeScript 实现一个快速排序(原地排序,不创建新数组)。只给代码,不要解释。"
|
|
53
|
+
"prompt": "用 TypeScript 实现一个快速排序(原地排序,不创建新数组)。只给代码,不要解释。",
|
|
54
|
+
"maxScore": 10,
|
|
55
|
+
"checks": [
|
|
56
|
+
{ "type": "contains_code_block", "value": true, "points": 1, "description": "包含代码" },
|
|
57
|
+
{ "type": "contains", "value": "partition", "ignoreCase": false, "points": 2, "description": "实现了 partition 函数" },
|
|
58
|
+
{ "type": "contains", "value": "pivot", "ignoreCase": false, "points": 2, "description": "选定了 pivot" },
|
|
59
|
+
{ "type": "contains", "value": "quickSort", "ignoreCase": false, "points": 2, "description": "递归调用 quickSort" },
|
|
60
|
+
{ "type": "not_contains", "value": "concat", "ignoreCase": false, "points": 2, "description": "不是非原地排序(没用 concat)" },
|
|
61
|
+
{ "type": "not_contains", "value": "解释", "ignoreCase": true, "points": 1, "description": "没有多余解释" }
|
|
62
|
+
]
|
|
31
63
|
},
|
|
32
64
|
{
|
|
33
65
|
"id": "react-component",
|
|
34
66
|
"category": "编码",
|
|
35
|
-
"prompt": "写一个 React 计数器组件(TypeScript),包含:+ 按钮、- 按钮、重置按钮,显示当前数值。用 useState。只给代码,不要解释。"
|
|
67
|
+
"prompt": "写一个 React 计数器组件(TypeScript),包含:+ 按钮、- 按钮、重置按钮,显示当前数值。用 useState。只给代码,不要解释。",
|
|
68
|
+
"maxScore": 10,
|
|
69
|
+
"checks": [
|
|
70
|
+
{ "type": "contains_code_block", "value": true, "points": 1, "description": "包含代码" },
|
|
71
|
+
{ "type": "contains", "value": "useState", "ignoreCase": false, "points": 2, "description": "用了 useState" },
|
|
72
|
+
{ "type": "contains", "value": "button", "ignoreCase": true, "points": 1.5, "description": "包含按钮元素" },
|
|
73
|
+
{ "type": "contains", "value": "+", "ignoreCase": false, "points": 1, "description": "有加号按钮" },
|
|
74
|
+
{ "type": "contains", "value": "-", "ignoreCase": false, "points": 1, "description": "有减号按钮" },
|
|
75
|
+
{ "type": "contains", "value": "重置", "ignoreCase": false, "points": 1.5, "description": "有重置按钮" },
|
|
76
|
+
{ "type": "contains", "value": "React", "ignoreCase": true, "points": 1, "description": "导入 React" },
|
|
77
|
+
{ "type": "contains", "value": "FC", "ignoreCase": false, "points": 1, "description": "用了 React.FC 类型" }
|
|
78
|
+
]
|
|
36
79
|
},
|
|
37
80
|
{
|
|
38
81
|
"id": "regex",
|
|
39
82
|
"category": "编码",
|
|
40
|
-
"prompt": "写一个 JavaScript 正则表达式,匹配中国大陆手机号(11 位,1 开头,第二位 3-9)。只给正则,不要解释。"
|
|
83
|
+
"prompt": "写一个 JavaScript 正则表达式,匹配中国大陆手机号(11 位,1 开头,第二位 3-9)。只给正则,不要解释。",
|
|
84
|
+
"maxScore": 10,
|
|
85
|
+
"checks": [
|
|
86
|
+
{ "type": "valid_regex", "value": true, "points": 3, "description": "正则语法有效" },
|
|
87
|
+
{ "type": "contains", "value": "1[3-9]", "ignoreCase": false, "points": 3, "description": "匹配 1 开头+第二位 3-9" },
|
|
88
|
+
{ "type": "contains", "value": "\\d{9}", "ignoreCase": false, "points": 2, "description": "匹配后 9 位数字" },
|
|
89
|
+
{ "type": "contains", "value": "^", "ignoreCase": false, "points": 1, "description": "有开头锚点" },
|
|
90
|
+
{ "type": "contains", "value": "$", "ignoreCase": false, "points": 1, "description": "有结尾锚点" }
|
|
91
|
+
]
|
|
41
92
|
},
|
|
42
93
|
{
|
|
43
94
|
"id": "reasoning-1",
|
|
44
95
|
"category": "推理",
|
|
45
|
-
"prompt": "有三个箱子:一个只装苹果,一个只装橘子,一个混装。所有标签都贴错了。你只能从一个箱子里拿一个水果看,就能推断出所有箱子的正确内容。请问该从哪个箱子拿?为什么?"
|
|
96
|
+
"prompt": "有三个箱子:一个只装苹果,一个只装橘子,一个混装。所有标签都贴错了。你只能从一个箱子里拿一个水果看,就能推断出所有箱子的正确内容。请问该从哪个箱子拿?为什么?",
|
|
97
|
+
"maxScore": 10,
|
|
98
|
+
"checks": [
|
|
99
|
+
{ "type": "contains", "value": "混装", "ignoreCase": false, "points": 3, "description": "回答从混装箱拿" },
|
|
100
|
+
{ "type": "contains", "value": "标签", "ignoreCase": false, "points": 2, "description": "提到标签贴错" },
|
|
101
|
+
{ "type": "contains", "value": "推理", "ignoreCase": true, "points": 2, "description": "有推理过程" },
|
|
102
|
+
{ "type": "min_length", "value": 100, "points": 3, "description": "解释够详细(>=100 字)" }
|
|
103
|
+
]
|
|
46
104
|
},
|
|
47
105
|
{
|
|
48
106
|
"id": "reasoning-2",
|
|
49
107
|
"category": "推理",
|
|
50
|
-
"prompt": "一个人花 8 元买了一只鸡,9 元卖出,10 元买回,11 元卖出。他赚了多少钱?一步步算。"
|
|
108
|
+
"prompt": "一个人花 8 元买了一只鸡,9 元卖出,10 元买回,11 元卖出。他赚了多少钱?一步步算。",
|
|
109
|
+
"maxScore": 10,
|
|
110
|
+
"checks": [
|
|
111
|
+
{ "type": "contains", "value": "2", "ignoreCase": false, "points": 4, "description": "答案正确(赚 2 元)" },
|
|
112
|
+
{ "type": "contains", "value": "8", "ignoreCase": false, "points": 1.5, "description": "提到了 8 元买入" },
|
|
113
|
+
{ "type": "contains", "value": "11", "ignoreCase": false, "points": 1.5, "description": "提到了 11 元卖出" },
|
|
114
|
+
{ "type": "contains", "value": "利润", "ignoreCase": true, "points": 1.5, "description": "有利润计算过程" },
|
|
115
|
+
{ "type": "min_length", "value": 80, "points": 1.5, "description": "有分步计算过程" }
|
|
116
|
+
]
|
|
51
117
|
},
|
|
52
118
|
{
|
|
53
119
|
"id": "zh-explain",
|
|
54
120
|
"category": "中文",
|
|
55
|
-
"prompt": "用一句话给完全不懂编程的人解释什么是「递归」。说得通俗一点。"
|
|
121
|
+
"prompt": "用一句话给完全不懂编程的人解释什么是「递归」。说得通俗一点。",
|
|
122
|
+
"maxScore": 10,
|
|
123
|
+
"checks": [
|
|
124
|
+
{ "type": "contains", "value": "套娃", "ignoreCase": false, "points": 3, "description": "用了套娃类比(通俗)" },
|
|
125
|
+
{ "type": "min_length", "value": 20, "points": 2, "description": "不是敷衍(>=20 字)" },
|
|
126
|
+
{ "type": "max_length", "value": 200, "points": 2, "description": "真的是一句话(<=200 字)" },
|
|
127
|
+
{ "type": "not_contains", "value": "函数调用", "ignoreCase": true, "points": 1.5, "description": "没用编程术语" },
|
|
128
|
+
{ "type": "not_contains", "value": "递归调用", "ignoreCase": true, "points": 1.5, "description": "没递归解释递归(循环定义)" }
|
|
129
|
+
]
|
|
56
130
|
},
|
|
57
131
|
{
|
|
58
132
|
"id": "zh-poem",
|
|
59
133
|
"category": "中文",
|
|
60
|
-
"prompt": "以「AI」为主题写一首五言绝句(每句五个字,共四句)
|
|
134
|
+
"prompt": "以「AI」为主题写一首五言绝句(每句五个字,共四句)。只给诗句,不要解释。",
|
|
135
|
+
"maxScore": 10,
|
|
136
|
+
"checks": [
|
|
137
|
+
{ "type": "has_chinese", "value": true, "points": 2, "description": "包含中文" },
|
|
138
|
+
{ "type": "line_count_5chars", "value": 4, "points": 4, "description": "四句每句五字" },
|
|
139
|
+
{ "type": "contains", "value": "AI", "ignoreCase": true, "points": 2, "description": "主题相关" },
|
|
140
|
+
{ "type": "max_length", "value": 100, "points": 2, "description": "没有多余解释" }
|
|
141
|
+
]
|
|
61
142
|
},
|
|
62
143
|
{
|
|
63
144
|
"id": "instruction-follow",
|
|
64
145
|
"category": "指令遵循",
|
|
65
|
-
"prompt": "你的回答里只能包含以下三个词(可以重复):好、的、行。其他任何词都不许出现。开始:今天天气怎么样?"
|
|
146
|
+
"prompt": "你的回答里只能包含以下三个词(可以重复):好、的、行。其他任何词都不许出现。开始:今天天气怎么样?",
|
|
147
|
+
"maxScore": 10,
|
|
148
|
+
"checks": [
|
|
149
|
+
{ "type": "allowed_words_only", "value": ["好", "的", "行"], "points": 8, "description": "只用了允许的词" },
|
|
150
|
+
{ "type": "min_length", "value": 1, "points": 2, "description": "有实际回答(没沉默)" }
|
|
151
|
+
]
|
|
66
152
|
},
|
|
67
153
|
{
|
|
68
154
|
"id": "format-json",
|
|
69
155
|
"category": "指令遵循",
|
|
70
|
-
"prompt": "输出一个 JSON 对象,包含 name(你的名字)、version(当前日期)、models(数组,随便列三个模型名)。只输出 JSON,不要 markdown 代码块标记。"
|
|
156
|
+
"prompt": "输出一个 JSON 对象,包含 name(你的名字)、version(当前日期)、models(数组,随便列三个模型名)。只输出 JSON,不要 markdown 代码块标记。",
|
|
157
|
+
"maxScore": 10,
|
|
158
|
+
"checks": [
|
|
159
|
+
{ "type": "parse_json", "value": true, "points": 4, "description": "能解析为合法 JSON" },
|
|
160
|
+
{ "type": "has_json_key", "value": "name", "points": 1.5, "description": "包含 name 字段" },
|
|
161
|
+
{ "type": "has_json_key", "value": "version", "points": 1.5, "description": "包含 version 字段" },
|
|
162
|
+
{ "type": "has_json_key", "value": "models", "points": 1.5, "description": "包含 models 字段" },
|
|
163
|
+
{ "type": "not_contains", "value": "```", "ignoreCase": false, "points": 1.5, "description": "没包在 markdown 代码块里" }
|
|
164
|
+
]
|
|
71
165
|
},
|
|
72
166
|
{
|
|
73
167
|
"id": "context-length",
|
|
74
168
|
"category": "长上下文",
|
|
75
|
-
"prompt": "请重复以下句子 50 遍:「u1s1 有一说一。」然后告诉我你一共输出了多少遍。"
|
|
169
|
+
"prompt": "请重复以下句子 50 遍:「u1s1 有一说一。」然后告诉我你一共输出了多少遍。",
|
|
170
|
+
"maxScore": 10,
|
|
171
|
+
"checks": [
|
|
172
|
+
{ "type": "contains", "value": "50", "ignoreCase": false, "points": 4, "description": "输出了 50 遍(或声明了 50)" },
|
|
173
|
+
{ "type": "contains", "value": "u1s1 有一说一", "ignoreCase": true, "points": 3, "description": "内容包含 u1s1 有一说一" },
|
|
174
|
+
{ "type": "min_length", "value": 200, "points": 3, "description": "长度足够(>=200 字符)" }
|
|
175
|
+
]
|
|
76
176
|
},
|
|
77
177
|
{
|
|
78
178
|
"id": "translation",
|
|
79
179
|
"category": "翻译",
|
|
80
|
-
"prompt": "把这句话翻译成地道的中文: 'One should always code as if the person who ends up maintaining your code is a violent psychopath who knows where you live.'"
|
|
180
|
+
"prompt": "把这句话翻译成地道的中文: 'One should always code as if the person who ends up maintaining your code is a violent psychopath who knows where you live.'",
|
|
181
|
+
"maxScore": 10,
|
|
182
|
+
"checks": [
|
|
183
|
+
{ "type": "has_chinese", "value": true, "points": 2, "description": "输出是中文" },
|
|
184
|
+
{ "type": "contains", "value": "暴力", "ignoreCase": false, "points": 2, "description": "翻出了 violent" },
|
|
185
|
+
{ "type": "contains", "value": "代码", "ignoreCase": false, "points": 2, "description": "翻出了 code" },
|
|
186
|
+
{ "type": "contains", "value": "维护", "ignoreCase": false, "points": 2, "description": "翻出了 maintaining" },
|
|
187
|
+
{ "type": "contains", "value": "你", "ignoreCase": false, "points": 1, "description": "翻出了 you" },
|
|
188
|
+
{ "type": "contains", "value": "住", "ignoreCase": false, "points": 1, "description": "翻出了 where you live" }
|
|
189
|
+
]
|
|
81
190
|
}
|
|
82
191
|
]
|
|
83
192
|
}
|
package/dist/agent-setup.d.ts
CHANGED
|
@@ -26,6 +26,13 @@ export declare function writeWebToolsExtension(cfg: CliConfig, features: {
|
|
|
26
26
|
webFetchRender: boolean;
|
|
27
27
|
imageGen: boolean;
|
|
28
28
|
}): void;
|
|
29
|
+
/**
|
|
30
|
+
* 生成「精简 UI」扩展到 <agentDir>/extensions/u1s1-compact-ui.js:
|
|
31
|
+
* - 隐藏内置工具调用(read/bash/edit/write/grep/find/ls),出错才显示一行;ctrl+o 可展开
|
|
32
|
+
* - 每轮回复结束追加一条汇总行,如 ⚙ 9 tools · bash×4 · read×3 · ✗ 1
|
|
33
|
+
* - 思考块折叠标签设为空,配合 pi 补丁(见 patches/)消除多余空行
|
|
34
|
+
*/
|
|
35
|
+
export declare function writeCompactUiExtension(): void;
|
|
29
36
|
/**
|
|
30
37
|
* pi-web-ui 的就绪检测只认 <agentDir>/auth.json 里有没有凭据条目(models.json
|
|
31
38
|
* 的 apiKey 它不看),空 {} 会在网页里弹「未检测到 pi agent 配置」引导装 pi。
|
package/dist/agent-setup.js
CHANGED
|
@@ -142,6 +142,99 @@ export function writeWebToolsExtension(cfg, features) {
|
|
|
142
142
|
imageLine +
|
|
143
143
|
`}\n`);
|
|
144
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* 生成「精简 UI」扩展到 <agentDir>/extensions/u1s1-compact-ui.js:
|
|
147
|
+
* - 隐藏内置工具调用(read/bash/edit/write/grep/find/ls),出错才显示一行;ctrl+o 可展开
|
|
148
|
+
* - 每轮回复结束追加一条汇总行,如 ⚙ 9 tools · bash×4 · read×3 · ✗ 1
|
|
149
|
+
* - 思考块折叠标签设为空,配合 pi 补丁(见 patches/)消除多余空行
|
|
150
|
+
*/
|
|
151
|
+
export function writeCompactUiExtension() {
|
|
152
|
+
const dir = join(agentDir, "extensions");
|
|
153
|
+
mkdirSync(dir, { recursive: true });
|
|
154
|
+
writeFileSync(join(dir, "u1s1-compact-ui.js"), `// 由 u1s1 每次启动自动生成,请勿手改
|
|
155
|
+
function oneLine(text, max = 160) {
|
|
156
|
+
const flat = String(text).replace(/\\s*\\n\\s*/g, " ; ").trim();
|
|
157
|
+
return flat.length > max ? flat.slice(0, max - 1) + "…" : flat;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export default async function (pi) {
|
|
161
|
+
// ---- 隐藏思考块折叠标签 ----
|
|
162
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
163
|
+
if (ctx.hasUI) ctx.ui.setHiddenThinkingLabel("");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// ---- 汇总统计 ----
|
|
167
|
+
let counts = {};
|
|
168
|
+
let errors = 0;
|
|
169
|
+
pi.on("agent_start", async () => {
|
|
170
|
+
counts = {};
|
|
171
|
+
errors = 0;
|
|
172
|
+
});
|
|
173
|
+
pi.on("tool_call", async (event) => {
|
|
174
|
+
counts[event.toolName] = (counts[event.toolName] ?? 0) + 1;
|
|
175
|
+
});
|
|
176
|
+
pi.on("tool_result", async (event) => {
|
|
177
|
+
if (event.isError) errors++;
|
|
178
|
+
});
|
|
179
|
+
pi.on("agent_end", async () => {
|
|
180
|
+
const total = Object.values(counts).reduce((a, b) => a + b, 0);
|
|
181
|
+
if (total === 0) return;
|
|
182
|
+
const parts = Object.entries(counts)
|
|
183
|
+
.sort((a, b) => b[1] - a[1])
|
|
184
|
+
.map(([name, n]) => name + "×" + n);
|
|
185
|
+
if (errors > 0) parts.push("✗ " + errors);
|
|
186
|
+
pi.appendEntry("tool-summary", { total, summary: parts.join(" · ") });
|
|
187
|
+
});
|
|
188
|
+
pi.registerEntryRenderer("tool-summary", (entry, _opts, theme) => {
|
|
189
|
+
const d = entry.data;
|
|
190
|
+
let text = theme.fg("muted", "⚙ ");
|
|
191
|
+
text += theme.fg("toolTitle", theme.bold(d.total + " tool" + (d.total > 1 ? "s" : "")));
|
|
192
|
+
text += theme.fg("dim", " · " + d.summary);
|
|
193
|
+
return new Text(text, 0, 0);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// ---- 隐藏各内置工具 ----
|
|
197
|
+
const { createReadTool, createBashTool, createEditTool, createWriteTool, createGrepTool, createFindTool, createLsTool } = await import("@earendil-works/pi-coding-agent");
|
|
198
|
+
const { Text } = await import("@earendil-works/pi-tui");
|
|
199
|
+
|
|
200
|
+
function hideTool(name, orig) {
|
|
201
|
+
pi.registerTool({
|
|
202
|
+
name,
|
|
203
|
+
label: name,
|
|
204
|
+
description: orig.description,
|
|
205
|
+
parameters: orig.parameters,
|
|
206
|
+
// 自己管外壳:不用默认的 Box 包装,空内容才不会产生空行
|
|
207
|
+
renderShell: "self",
|
|
208
|
+
async execute(toolCallId, params, signal, onUpdate) {
|
|
209
|
+
return orig.execute(toolCallId, params, signal, onUpdate);
|
|
210
|
+
},
|
|
211
|
+
renderCall() {
|
|
212
|
+
return new Text("", 0, 0);
|
|
213
|
+
},
|
|
214
|
+
renderResult(result, { expanded }, theme, context) {
|
|
215
|
+
if (context.isError) {
|
|
216
|
+
const c = result.content.find((x) => x.type === "text");
|
|
217
|
+
return new Text(theme.fg("error", "✗ " + oneLine(c && c.type === "text" ? c.text : "error")), 0, 0);
|
|
218
|
+
}
|
|
219
|
+
if (!expanded) return new Text("", 0, 0);
|
|
220
|
+
const c = result.content.find((x) => x.type === "text");
|
|
221
|
+
if (!c || c.type !== "text") return new Text("", 0, 0);
|
|
222
|
+
return new Text(c.text, 0, 0);
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const cwd = process.cwd();
|
|
228
|
+
hideTool("read", createReadTool(cwd));
|
|
229
|
+
hideTool("bash", createBashTool(cwd));
|
|
230
|
+
hideTool("edit", createEditTool(cwd));
|
|
231
|
+
hideTool("write", createWriteTool(cwd));
|
|
232
|
+
hideTool("grep", createGrepTool(cwd));
|
|
233
|
+
hideTool("find", createFindTool(cwd));
|
|
234
|
+
hideTool("ls", createLsTool(cwd));
|
|
235
|
+
}
|
|
236
|
+
`);
|
|
237
|
+
}
|
|
145
238
|
/**
|
|
146
239
|
* pi-web-ui 的就绪检测只认 <agentDir>/auth.json 里有没有凭据条目(models.json
|
|
147
240
|
* 的 apiKey 它不看),空 {} 会在网页里弹「未检测到 pi agent 配置」引导装 pi。
|
package/dist/bench.js
CHANGED
|
@@ -21,14 +21,12 @@ function readSuitesFile(filePath) {
|
|
|
21
21
|
}
|
|
22
22
|
/** 读内置或用户自定义的 suite */
|
|
23
23
|
function loadSuite(name) {
|
|
24
|
-
// 先找内置的(suite.json 含多个 suite,按 name 匹配)
|
|
25
24
|
const builtinPath = join(benchDataDir(), "suite.json");
|
|
26
25
|
if (existsSync(builtinPath)) {
|
|
27
26
|
const found = findSuite(readSuitesFile(builtinPath), name);
|
|
28
27
|
if (found)
|
|
29
28
|
return found;
|
|
30
29
|
}
|
|
31
|
-
// 再找用户的 ~/.u1s1/bench/<name>.json(单文件单 suite,沿用 suite.json 数组格式)
|
|
32
30
|
const userPath = join(BENCH_DIR, `${name}.json`);
|
|
33
31
|
if (existsSync(userPath)) {
|
|
34
32
|
const found = findSuite(readSuitesFile(userPath), name);
|
|
@@ -40,7 +38,6 @@ function loadSuite(name) {
|
|
|
40
38
|
/** 列出可用的 suite */
|
|
41
39
|
function listSuites() {
|
|
42
40
|
const suites = [];
|
|
43
|
-
// 内置
|
|
44
41
|
const builtinPath = join(benchDataDir(), "suite.json");
|
|
45
42
|
if (existsSync(builtinPath)) {
|
|
46
43
|
const raw = JSON.parse(readFileSync(builtinPath, "utf8"));
|
|
@@ -48,7 +45,6 @@ function listSuites() {
|
|
|
48
45
|
suites.push({ name: s.name, description: s.description, builtin: true });
|
|
49
46
|
}
|
|
50
47
|
}
|
|
51
|
-
// 用户自定义
|
|
52
48
|
if (existsSync(BENCH_DIR)) {
|
|
53
49
|
try {
|
|
54
50
|
const files = readdirSync(BENCH_DIR).filter((f) => f.endsWith(".json"));
|
|
@@ -80,91 +76,204 @@ function costStr(usd) {
|
|
|
80
76
|
return "<$0.001";
|
|
81
77
|
return `$${usd.toFixed(4)}`;
|
|
82
78
|
}
|
|
83
|
-
/**
|
|
79
|
+
/** 排列表格 */
|
|
84
80
|
function renderTable(header, rows) {
|
|
85
81
|
const colW = header.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i]?.length ?? 0)));
|
|
86
82
|
const sep = "─".repeat(colW.reduce((a, b) => a + b + 3, 1));
|
|
87
83
|
const line = (cells) => " " + cells.map((c, i) => c.padEnd(colW[i])).join(" │ ") + " ";
|
|
88
|
-
return [
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
84
|
+
return [line(header), sep, ...rows.map((r) => line(r))].join("\n");
|
|
85
|
+
}
|
|
86
|
+
/** 热度条:分数按比例转成视觉条 */
|
|
87
|
+
function scoreBar(score, maxScore, width = 8) {
|
|
88
|
+
const ratio = maxScore > 0 ? Math.min(score / maxScore, 1) : 0;
|
|
89
|
+
const filled = Math.round(ratio * width);
|
|
90
|
+
const empty = width - filled;
|
|
91
|
+
const bar = "█".repeat(filled) + "░".repeat(empty);
|
|
92
|
+
// 颜色标记
|
|
93
|
+
if (ratio >= 0.8)
|
|
94
|
+
return `\x1b[32m${bar}\x1b[0m`; // 绿
|
|
95
|
+
if (ratio >= 0.5)
|
|
96
|
+
return `\x1b[33m${bar}\x1b[0m`; // 黄
|
|
97
|
+
return `\x1b[31m${bar}\x1b[0m`; // 红
|
|
98
|
+
}
|
|
99
|
+
// ─── 评分引擎 ───
|
|
100
|
+
/** 对一条回答按 checks 逐项打分 */
|
|
101
|
+
function scoreResponse(response, checks, maxScore) {
|
|
102
|
+
const details = [];
|
|
103
|
+
let total = 0;
|
|
104
|
+
for (const check of checks) {
|
|
105
|
+
const result = runCheck(response, check);
|
|
106
|
+
details.push({
|
|
107
|
+
check: check.description,
|
|
108
|
+
passed: result,
|
|
109
|
+
points: check.points,
|
|
110
|
+
earned: result ? check.points : 0,
|
|
111
|
+
});
|
|
112
|
+
if (result)
|
|
113
|
+
total += check.points;
|
|
114
|
+
}
|
|
115
|
+
return { score: Math.min(total, maxScore), details };
|
|
116
|
+
}
|
|
117
|
+
/** 执行单项检查 */
|
|
118
|
+
function runCheck(response, check) {
|
|
119
|
+
const val = check.value;
|
|
120
|
+
switch (check.type) {
|
|
121
|
+
case "contains": {
|
|
122
|
+
const needle = String(val);
|
|
123
|
+
const text = check.ignoreCase ? response.toLowerCase() : response;
|
|
124
|
+
const search = check.ignoreCase ? needle.toLowerCase() : needle;
|
|
125
|
+
return text.includes(search);
|
|
126
|
+
}
|
|
127
|
+
case "not_contains": {
|
|
128
|
+
const needle = String(val);
|
|
129
|
+
const text = check.ignoreCase ? response.toLowerCase() : response;
|
|
130
|
+
const search = check.ignoreCase ? needle.toLowerCase() : needle;
|
|
131
|
+
return !text.includes(search);
|
|
132
|
+
}
|
|
133
|
+
case "contains_code_block":
|
|
134
|
+
return /```[\s\S]*?```/.test(response) || /`[^`]+`/.test(response);
|
|
135
|
+
case "parse_json": {
|
|
136
|
+
try {
|
|
137
|
+
const stripped = response.replace(/```json\s*\n?/gi, "").replace(/\n?```/g, "").trim();
|
|
138
|
+
JSON.parse(stripped);
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
case "valid_regex": {
|
|
146
|
+
try {
|
|
147
|
+
new RegExp(String(val));
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
// 从回答里提取 regex 来验证
|
|
152
|
+
const match = response.match(/\/(.+)\/[gimsuy]*/);
|
|
153
|
+
if (match) {
|
|
154
|
+
new RegExp(match[1]);
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
// 也可能是文本形式
|
|
158
|
+
const cleaned = response.replace(/```[\s\S]*?```/g, "").trim();
|
|
159
|
+
if (cleaned.startsWith("/")) {
|
|
160
|
+
const parts = cleaned.match(/^\/(.+)\/([gimsuy]*)$/);
|
|
161
|
+
if (parts) {
|
|
162
|
+
new RegExp(parts[1]);
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
case "has_json_key": {
|
|
170
|
+
try {
|
|
171
|
+
const stripped = response.replace(/```json\s*\n?/gi, "").replace(/\n?```/g, "").trim();
|
|
172
|
+
const obj = JSON.parse(stripped);
|
|
173
|
+
return obj[String(val)] !== undefined;
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
case "max_length": {
|
|
180
|
+
const limit = Number(val);
|
|
181
|
+
return response.length <= limit;
|
|
182
|
+
}
|
|
183
|
+
case "min_length": {
|
|
184
|
+
const limit = Number(val);
|
|
185
|
+
return response.length >= limit;
|
|
186
|
+
}
|
|
187
|
+
case "allowed_words_only": {
|
|
188
|
+
const allowed = val;
|
|
189
|
+
// 提取回答中的所有中文/英文字词
|
|
190
|
+
const tokens = response.split(/[\s,。!?、;:""''()\[【】\]「」]+/).filter(Boolean);
|
|
191
|
+
for (const token of tokens) {
|
|
192
|
+
if (token.length === 0)
|
|
193
|
+
continue;
|
|
194
|
+
// 标点/空格不检查
|
|
195
|
+
if (/^[,。!?、;:""''()《》\s.。,,、!?;:…\-—]+$/.test(token))
|
|
196
|
+
continue;
|
|
197
|
+
if (!allowed.includes(token))
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
case "has_chinese": {
|
|
203
|
+
return /[\u4e00-\u9fff\u3400-\u4dbf]/.test(response);
|
|
204
|
+
}
|
|
205
|
+
case "line_count_5chars": {
|
|
206
|
+
// 检查有几行是恰好 5 个汉字(去除标点空格)
|
|
207
|
+
const expected = Number(val);
|
|
208
|
+
const lines = response.split("\n");
|
|
209
|
+
let count5 = 0;
|
|
210
|
+
for (const line of lines) {
|
|
211
|
+
const chars = line.replace(/[,。!?、;:""''()《》\s,\.\-,;:!?\dA-Za-z]/g, "").trim();
|
|
212
|
+
if (/^[\u4e00-\u9fff]{5}$/.test(chars))
|
|
213
|
+
count5++;
|
|
214
|
+
}
|
|
215
|
+
return count5 >= expected;
|
|
216
|
+
}
|
|
217
|
+
default:
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
93
220
|
}
|
|
94
221
|
// ─── 模型调用 ───
|
|
95
222
|
async function callModel(baseUrl, apiKey, modelId, prompt) {
|
|
96
223
|
const start = performance.now();
|
|
97
|
-
// 简单判断是不是推理模型 — deepseek-reasoner、grok 带 reasoning
|
|
98
224
|
const isReasoning = /reasoner|grok/i.test(modelId);
|
|
99
225
|
const body = {
|
|
100
226
|
model: modelId,
|
|
101
227
|
messages: [{ role: "user", content: prompt }],
|
|
102
228
|
max_tokens: 4096,
|
|
103
|
-
// 推理模型不需要 temperature,传了可能报错
|
|
104
229
|
...(isReasoning ? {} : { temperature: 0.3 }),
|
|
105
230
|
stream: false,
|
|
106
231
|
};
|
|
107
|
-
const headers = {
|
|
108
|
-
|
|
109
|
-
};
|
|
110
|
-
if (apiKey) {
|
|
232
|
+
const headers = { "content-type": "application/json" };
|
|
233
|
+
if (apiKey)
|
|
111
234
|
headers["authorization"] = `Bearer ${apiKey}`;
|
|
112
|
-
}
|
|
113
235
|
try {
|
|
114
236
|
const res = await fetch(`${baseUrl}/chat/completions`, {
|
|
115
237
|
method: "POST",
|
|
116
238
|
headers,
|
|
117
239
|
body: JSON.stringify(body),
|
|
118
|
-
signal: AbortSignal.timeout(120_000),
|
|
240
|
+
signal: AbortSignal.timeout(120_000),
|
|
119
241
|
});
|
|
120
242
|
const latencyMs = Math.round(performance.now() - start);
|
|
121
243
|
if (!res.ok) {
|
|
122
244
|
const errBody = await res.text().catch(() => "未知错误");
|
|
123
|
-
return {
|
|
124
|
-
response: "",
|
|
125
|
-
latencyMs,
|
|
126
|
-
tokensIn: 0,
|
|
127
|
-
tokensOut: 0,
|
|
128
|
-
error: `HTTP ${res.status}: ${errBody.slice(0, 200)}`,
|
|
129
|
-
};
|
|
245
|
+
return { response: "", latencyMs, tokensIn: 0, tokensOut: 0, error: `HTTP ${res.status}: ${errBody.slice(0, 200)}` };
|
|
130
246
|
}
|
|
131
247
|
const data = (await res.json());
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
248
|
+
return {
|
|
249
|
+
response: data.choices?.[0]?.message?.content ?? "",
|
|
250
|
+
latencyMs,
|
|
251
|
+
tokensIn: data.usage?.prompt_tokens ?? 0,
|
|
252
|
+
tokensOut: data.usage?.completion_tokens ?? 0,
|
|
253
|
+
};
|
|
136
254
|
}
|
|
137
255
|
catch (e) {
|
|
138
256
|
const latencyMs = Math.round(performance.now() - start);
|
|
139
257
|
return {
|
|
140
|
-
response: "",
|
|
141
|
-
latencyMs,
|
|
142
|
-
tokensIn: 0,
|
|
143
|
-
tokensOut: 0,
|
|
258
|
+
response: "", latencyMs, tokensIn: 0, tokensOut: 0,
|
|
144
259
|
error: e instanceof Error ? e.message : String(e),
|
|
145
260
|
};
|
|
146
261
|
}
|
|
147
262
|
}
|
|
148
|
-
/** 找一个模型的 cost 信息(用于计费估算) */
|
|
149
263
|
function findModelCost(modelId) {
|
|
150
|
-
for (const m of MODELS)
|
|
264
|
+
for (const m of MODELS)
|
|
151
265
|
if (m.id === modelId)
|
|
152
266
|
return m.cost;
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
for (const m of ep.models) {
|
|
267
|
+
for (const ep of CUSTOM_ENDPOINTS)
|
|
268
|
+
for (const m of ep.models)
|
|
156
269
|
if (m.id === modelId)
|
|
157
270
|
return m.cost;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
271
|
return null;
|
|
161
272
|
}
|
|
162
|
-
/** 估算花费 */
|
|
163
273
|
function estimateCost(modelId, tokensIn, tokensOut) {
|
|
164
274
|
const cost = findModelCost(modelId);
|
|
165
275
|
if (!cost)
|
|
166
276
|
return 0;
|
|
167
|
-
// cost 是每百万 token 的美元价格
|
|
168
277
|
return (tokensIn * cost.input + tokensOut * cost.output) / 1_000_000;
|
|
169
278
|
}
|
|
170
279
|
// ─── 保存 & 报告 ───
|
|
@@ -180,7 +289,6 @@ function loadRun(id) {
|
|
|
180
289
|
throw new Error(`找不到运行记录「${id}」`);
|
|
181
290
|
return JSON.parse(readFileSync(file, "utf8"));
|
|
182
291
|
}
|
|
183
|
-
/** 列出最近的运行记录 */
|
|
184
292
|
function listRuns() {
|
|
185
293
|
if (!existsSync(BENCH_DIR))
|
|
186
294
|
return [];
|
|
@@ -191,7 +299,7 @@ function listRuns() {
|
|
|
191
299
|
const run = JSON.parse(readFileSync(join(BENCH_DIR, f), "utf8"));
|
|
192
300
|
runs.push({ id: run.id, suiteName: run.suiteName, timestamp: run.timestamp, models: run.models });
|
|
193
301
|
}
|
|
194
|
-
catch { /*
|
|
302
|
+
catch { /* 跳过损坏 */ }
|
|
195
303
|
}
|
|
196
304
|
return runs;
|
|
197
305
|
}
|
|
@@ -202,7 +310,6 @@ function generateReport(run) {
|
|
|
202
310
|
lines.push(`运行时间: ${run.timestamp}`);
|
|
203
311
|
lines.push(`模型: ${run.models.join(", ")}`);
|
|
204
312
|
lines.push("");
|
|
205
|
-
// 按 category 分组
|
|
206
313
|
const byCategory = new Map();
|
|
207
314
|
for (const r of run.results) {
|
|
208
315
|
const cat = r.category || "未分类";
|
|
@@ -213,7 +320,6 @@ function generateReport(run) {
|
|
|
213
320
|
for (const [cat, results] of byCategory) {
|
|
214
321
|
lines.push(`## ${cat}`);
|
|
215
322
|
lines.push("");
|
|
216
|
-
// 按 question 分组
|
|
217
323
|
const byQuestion = new Map();
|
|
218
324
|
for (const r of results) {
|
|
219
325
|
if (!byQuestion.has(r.questionId))
|
|
@@ -222,34 +328,52 @@ function generateReport(run) {
|
|
|
222
328
|
}
|
|
223
329
|
for (const [qId, qResults] of byQuestion) {
|
|
224
330
|
const prompt = qResults[0].prompt;
|
|
331
|
+
const maxScore = qResults[0].maxScore ?? 10;
|
|
225
332
|
lines.push(`### ${qId}`);
|
|
226
333
|
lines.push(`> ${prompt}`);
|
|
227
334
|
lines.push("");
|
|
228
|
-
//
|
|
229
|
-
const header = ["模型", "延迟", "
|
|
335
|
+
// 表格: 模型 | 分数 | 延迟 | 输出token | 状态
|
|
336
|
+
const header = ["模型", `得分/${maxScore}`, "延迟", "输出tok", "状态"];
|
|
230
337
|
const rows = [];
|
|
231
338
|
for (const r of qResults) {
|
|
232
339
|
const modelLabel = r.providerName === PROVIDER_ID ? r.modelId : `${r.providerName}:${r.modelId}`;
|
|
340
|
+
const scoreStr = r.score !== undefined ? `${r.score}/${maxScore}` : "—";
|
|
233
341
|
rows.push([
|
|
234
342
|
modelLabel,
|
|
343
|
+
scoreStr,
|
|
235
344
|
formatDuration(r.latencyMs),
|
|
236
|
-
String(r.tokensIn),
|
|
237
345
|
String(r.tokensOut),
|
|
238
|
-
|
|
239
|
-
r.error ? `❌ ${r.error}` : "✅",
|
|
346
|
+
r.error ? `❌ ${r.error.slice(0, 40)}` : "✅",
|
|
240
347
|
]);
|
|
241
348
|
}
|
|
242
349
|
lines.push("```");
|
|
243
350
|
lines.push(renderTable(header, rows));
|
|
244
351
|
lines.push("```");
|
|
245
|
-
|
|
246
|
-
|
|
352
|
+
// 评分明细
|
|
353
|
+
const scored = qResults.filter((r) => r.scoreDetails && r.scoreDetails.length > 0);
|
|
354
|
+
if (scored.length > 0) {
|
|
355
|
+
lines.push("");
|
|
356
|
+
lines.push("<details><summary>评分明细</summary>");
|
|
357
|
+
lines.push("");
|
|
358
|
+
for (const r of scored) {
|
|
359
|
+
const modelLabel = r.providerName === PROVIDER_ID ? r.modelId : `${r.providerName}:${r.modelId}`;
|
|
360
|
+
lines.push(`**${modelLabel}:** ${r.score}/${maxScore}`);
|
|
361
|
+
for (const d of r.scoreDetails) {
|
|
362
|
+
const icon = d.passed ? "✅" : "❌";
|
|
363
|
+
lines.push(`- ${icon} ${d.check} (+${d.earned}/${d.points})`);
|
|
364
|
+
}
|
|
365
|
+
lines.push("");
|
|
366
|
+
}
|
|
367
|
+
lines.push("</details>");
|
|
368
|
+
lines.push("");
|
|
369
|
+
}
|
|
370
|
+
// 各模型回答
|
|
247
371
|
for (const r of qResults) {
|
|
248
372
|
const modelLabel = r.providerName === PROVIDER_ID ? r.modelId : `${r.providerName}:${r.modelId}`;
|
|
249
373
|
lines.push(`<details><summary>${modelLabel} 的回答</summary>`);
|
|
250
374
|
lines.push("");
|
|
251
375
|
lines.push("```");
|
|
252
|
-
lines.push(r.response.slice(0, 2000));
|
|
376
|
+
lines.push(r.response.slice(0, 2000));
|
|
253
377
|
if (r.response.length > 2000)
|
|
254
378
|
lines.push("...(截断)");
|
|
255
379
|
lines.push("```");
|
|
@@ -258,27 +382,33 @@ function generateReport(run) {
|
|
|
258
382
|
}
|
|
259
383
|
}
|
|
260
384
|
}
|
|
261
|
-
// 汇总
|
|
385
|
+
// 汇总 — 带分数
|
|
262
386
|
lines.push("## 汇总");
|
|
263
387
|
lines.push("");
|
|
264
388
|
const modelSummary = new Map();
|
|
265
389
|
for (const r of run.results) {
|
|
266
390
|
const key = r.providerName === PROVIDER_ID ? r.modelId : `${r.providerName}:${r.modelId}`;
|
|
267
391
|
if (!modelSummary.has(key))
|
|
268
|
-
modelSummary.set(key, { totalLatency: 0, totalCost: 0, errors: 0, total: 0 });
|
|
392
|
+
modelSummary.set(key, { totalScore: 0, totalMax: 0, totalLatency: 0, totalCost: 0, errors: 0, total: 0 });
|
|
269
393
|
const s = modelSummary.get(key);
|
|
394
|
+
s.totalScore += r.score ?? 0;
|
|
395
|
+
s.totalMax += r.maxScore ?? 10;
|
|
270
396
|
s.totalLatency += r.latencyMs;
|
|
271
397
|
s.totalCost += r.costUsd;
|
|
272
398
|
if (r.error)
|
|
273
399
|
s.errors++;
|
|
274
400
|
s.total++;
|
|
275
401
|
}
|
|
276
|
-
const header = ["模型", "
|
|
402
|
+
const header = ["模型", "总分", "得分率", "平均延迟", "总花费", "错误率"];
|
|
277
403
|
const rows = [];
|
|
278
|
-
|
|
404
|
+
// 按总分排序
|
|
405
|
+
const sorted = [...modelSummary.entries()].sort((a, b) => b[1].totalScore - a[1].totalScore);
|
|
406
|
+
for (const [model, s] of sorted) {
|
|
407
|
+
const pct = s.totalMax > 0 ? ((s.totalScore / s.totalMax) * 100).toFixed(1) : "0.0";
|
|
279
408
|
rows.push([
|
|
280
409
|
model,
|
|
281
|
-
|
|
410
|
+
`${s.totalScore}/${s.totalMax}`,
|
|
411
|
+
`${pct}%`,
|
|
282
412
|
formatDuration(s.totalLatency / s.total),
|
|
283
413
|
costStr(s.totalCost),
|
|
284
414
|
s.errors > 0 ? `${((s.errors / s.total) * 100).toFixed(0)}%` : "0%",
|
|
@@ -294,7 +424,7 @@ export async function benchCommand(args) {
|
|
|
294
424
|
const sub = args[0];
|
|
295
425
|
if (!sub || sub === "help" || sub === "--help") {
|
|
296
426
|
console.log("");
|
|
297
|
-
console.log(" u1s1 bench —
|
|
427
|
+
console.log(" u1s1 bench — 模型质量基准测试(带自动评分)");
|
|
298
428
|
console.log("");
|
|
299
429
|
console.log(" 用法:");
|
|
300
430
|
console.log(" u1s1 bench list 列出可用的测试套件");
|
|
@@ -304,7 +434,7 @@ export async function benchCommand(args) {
|
|
|
304
434
|
console.log("");
|
|
305
435
|
console.log(" 示例:");
|
|
306
436
|
console.log(" u1s1 bench run quick 用当前默认模型跑快速测试");
|
|
307
|
-
console.log(" u1s1 bench run full deepseek
|
|
437
|
+
console.log(" u1s1 bench run full deepseek pro 用多个模型跑全面测试");
|
|
308
438
|
console.log(" u1s1 bench report latest 看最近一次报告");
|
|
309
439
|
console.log("");
|
|
310
440
|
return;
|
|
@@ -330,9 +460,7 @@ export async function benchCommand(args) {
|
|
|
330
460
|
console.error(" 还没有登录,先 u1s1 login");
|
|
331
461
|
process.exit(1);
|
|
332
462
|
}
|
|
333
|
-
// 加载端点配置
|
|
334
463
|
await loadCustomEndpoints(cfg);
|
|
335
|
-
// 加载测试套件
|
|
336
464
|
let suite;
|
|
337
465
|
try {
|
|
338
466
|
suite = loadSuite(suiteName);
|
|
@@ -344,28 +472,22 @@ export async function benchCommand(args) {
|
|
|
344
472
|
let targets;
|
|
345
473
|
function resolveModelTarget(query) {
|
|
346
474
|
const lower = query.toLowerCase();
|
|
347
|
-
// u1s1 官方模型
|
|
348
475
|
const m = MODELS.find((x) => x.id.toLowerCase() === lower || x.aliases.includes(lower));
|
|
349
|
-
if (m)
|
|
476
|
+
if (m)
|
|
350
477
|
return { id: m.id, label: m.id, baseUrl: cfg.baseUrl, apiKey: cfg.apiKey, source: "u1s1" };
|
|
351
|
-
}
|
|
352
|
-
// 自定义端点
|
|
353
478
|
for (const ep of CUSTOM_ENDPOINTS) {
|
|
354
479
|
const em = ep.models.find((x) => x.id.toLowerCase() === lower || x.aliases.includes(lower));
|
|
355
480
|
if (em) {
|
|
356
|
-
// 自定义端点传模型短 ID(本地 Ollama 等不认带前缀的 huihui_ai/xxx)
|
|
357
481
|
const shortId = em.id.includes("/") ? em.id.split("/")[1] : em.id;
|
|
358
482
|
return { id: shortId, label: `${ep.name}:${em.id}`, baseUrl: ep.baseUrl, apiKey: ep.apiKey ?? "", source: "custom" };
|
|
359
483
|
}
|
|
360
484
|
}
|
|
361
|
-
// 直接当 ID 用
|
|
362
485
|
return { id: query, label: query, baseUrl: cfg.baseUrl, apiKey: cfg.apiKey, source: "u1s1" };
|
|
363
486
|
}
|
|
364
487
|
if (modelQueries.length > 0) {
|
|
365
488
|
targets = modelQueries.map((q) => resolveModelTarget(q)).filter((t) => t !== null);
|
|
366
489
|
}
|
|
367
490
|
else {
|
|
368
|
-
// 默认:用当前首选模型
|
|
369
491
|
const { resolvePreferredModel } = await import("./config.js");
|
|
370
492
|
const pref = resolvePreferredModel(cfg);
|
|
371
493
|
if (pref.provider === PROVIDER_ID) {
|
|
@@ -382,14 +504,12 @@ export async function benchCommand(args) {
|
|
|
382
504
|
}
|
|
383
505
|
}
|
|
384
506
|
}
|
|
385
|
-
// 确认
|
|
386
507
|
console.log("");
|
|
387
508
|
console.log(` 📊 Bench: ${suite.name}`);
|
|
388
509
|
console.log(` ${suite.description}`);
|
|
389
510
|
console.log(` 题目数: ${suite.questions.length}`);
|
|
390
511
|
console.log(` 模型: ${targets.map((t) => t.label).join(", ")}`);
|
|
391
512
|
console.log("");
|
|
392
|
-
// 跑
|
|
393
513
|
const results = [];
|
|
394
514
|
const total = suite.questions.length * targets.length;
|
|
395
515
|
let done = 0;
|
|
@@ -397,31 +517,47 @@ export async function benchCommand(args) {
|
|
|
397
517
|
for (const q of suite.questions) {
|
|
398
518
|
const label = `${target.label} / ${q.id}`;
|
|
399
519
|
process.stdout.write(` [${++done}/${total}] ${label.padEnd(40)} `);
|
|
400
|
-
const
|
|
401
|
-
const costUsd = estimateCost(target.id,
|
|
520
|
+
const resp = await callModel(target.baseUrl, target.apiKey, target.id, q.prompt);
|
|
521
|
+
const costUsd = estimateCost(target.id, resp.tokensIn, resp.tokensOut);
|
|
522
|
+
// 评分
|
|
523
|
+
let score;
|
|
524
|
+
let maxScore;
|
|
525
|
+
let scoreDetails;
|
|
526
|
+
if (!resp.error && q.checks && q.checks.length > 0) {
|
|
527
|
+
const ms = q.maxScore ?? 10;
|
|
528
|
+
const result = scoreResponse(resp.response, q.checks, ms);
|
|
529
|
+
score = result.score;
|
|
530
|
+
maxScore = ms;
|
|
531
|
+
scoreDetails = result.details;
|
|
532
|
+
}
|
|
402
533
|
results.push({
|
|
403
534
|
questionId: q.id,
|
|
404
535
|
category: q.category,
|
|
405
536
|
prompt: q.prompt,
|
|
406
537
|
modelId: target.label,
|
|
407
538
|
providerName: target.source === "u1s1" ? PROVIDER_ID : target.label,
|
|
408
|
-
response:
|
|
409
|
-
latencyMs:
|
|
410
|
-
tokensIn:
|
|
411
|
-
tokensOut:
|
|
539
|
+
response: resp.response,
|
|
540
|
+
latencyMs: resp.latencyMs,
|
|
541
|
+
tokensIn: resp.tokensIn,
|
|
542
|
+
tokensOut: resp.tokensOut,
|
|
412
543
|
costUsd,
|
|
413
|
-
error:
|
|
414
|
-
|
|
544
|
+
error: resp.error,
|
|
545
|
+
score,
|
|
546
|
+
maxScore,
|
|
547
|
+
scoreDetails,
|
|
415
548
|
});
|
|
416
|
-
if (
|
|
417
|
-
console.log(`❌ ${
|
|
549
|
+
if (resp.error) {
|
|
550
|
+
console.log(`❌ ${resp.error.slice(0, 60)}`);
|
|
551
|
+
}
|
|
552
|
+
else if (score !== undefined) {
|
|
553
|
+
const bar = scoreBar(score, maxScore);
|
|
554
|
+
console.log(`✅ ${bar} ${score}/${maxScore} · ${formatDuration(resp.latencyMs)}`);
|
|
418
555
|
}
|
|
419
556
|
else {
|
|
420
|
-
console.log(`✅ ${formatDuration(
|
|
557
|
+
console.log(`✅ ${formatDuration(resp.latencyMs)} · ${resp.tokensIn}→${resp.tokensOut} tok`);
|
|
421
558
|
}
|
|
422
559
|
}
|
|
423
560
|
}
|
|
424
|
-
// 保存
|
|
425
561
|
const runId = `bench-${suite.name}-${Date.now()}`;
|
|
426
562
|
const run = {
|
|
427
563
|
id: runId,
|
|
@@ -431,26 +567,39 @@ export async function benchCommand(args) {
|
|
|
431
567
|
results,
|
|
432
568
|
};
|
|
433
569
|
saveRun(run);
|
|
434
|
-
//
|
|
570
|
+
// 简易汇总(带分数)
|
|
435
571
|
console.log("");
|
|
436
|
-
console.log(" 📋
|
|
572
|
+
console.log(" 📋 评分汇总:");
|
|
437
573
|
const byModel = new Map();
|
|
438
574
|
for (const r of results) {
|
|
439
575
|
const key = r.modelId;
|
|
440
576
|
if (!byModel.has(key))
|
|
441
|
-
byModel.set(key, {
|
|
577
|
+
byModel.set(key, { totalScore: 0, totalMax: 0, totalLatency: 0, totalCost: 0, ok: 0, fail: 0 });
|
|
442
578
|
const s = byModel.get(key);
|
|
579
|
+
s.totalScore += r.score ?? 0;
|
|
580
|
+
s.totalMax += r.maxScore ?? 10;
|
|
581
|
+
s.totalLatency += r.latencyMs;
|
|
582
|
+
s.totalCost += r.costUsd;
|
|
443
583
|
if (r.error)
|
|
444
584
|
s.fail++;
|
|
445
585
|
else
|
|
446
586
|
s.ok++;
|
|
447
|
-
s.totalLatency += r.latencyMs;
|
|
448
|
-
s.totalCost += r.costUsd;
|
|
449
587
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
588
|
+
const sumHeader = ["模型", "总分", "得分率", "平均延迟", "花费", "状态"];
|
|
589
|
+
const sumRows = [];
|
|
590
|
+
const sorted = [...byModel.entries()].sort((a, b) => b[1].totalScore - a[1].totalScore);
|
|
591
|
+
for (const [model, s] of sorted) {
|
|
592
|
+
const pct = s.totalMax > 0 ? ((s.totalScore / s.totalMax) * 100).toFixed(1) : "0.0";
|
|
593
|
+
sumRows.push([
|
|
594
|
+
model,
|
|
595
|
+
`${s.totalScore}/${s.totalMax}`,
|
|
596
|
+
`${pct}%`,
|
|
597
|
+
formatDuration(s.totalLatency / (s.ok + s.fail)),
|
|
598
|
+
costStr(s.totalCost),
|
|
599
|
+
s.fail > 0 ? `${((s.fail / (s.ok + s.fail)) * 100).toFixed(0)}%失败` : "✅全部通过",
|
|
600
|
+
]);
|
|
453
601
|
}
|
|
602
|
+
console.log(" " + renderTable(sumHeader, sumRows));
|
|
454
603
|
console.log("");
|
|
455
604
|
console.log(` 查看完整报告: u1s1 bench report ${runId}`);
|
|
456
605
|
console.log("");
|
|
@@ -470,7 +619,6 @@ export async function benchCommand(args) {
|
|
|
470
619
|
process.exit(1);
|
|
471
620
|
}
|
|
472
621
|
const report = generateReport(run);
|
|
473
|
-
// 写入可读文件
|
|
474
622
|
const reportFile = join(BENCH_DIR, `${run.id}.md`);
|
|
475
623
|
writeFileSync(reportFile, report);
|
|
476
624
|
console.log(report);
|
|
@@ -484,7 +632,6 @@ export async function benchCommand(args) {
|
|
|
484
632
|
console.log("");
|
|
485
633
|
console.log(` 对比 ${run1.suiteName}(${run1.timestamp}) vs ${run2.suiteName}(${run2.timestamp})`);
|
|
486
634
|
console.log("");
|
|
487
|
-
// 找共同题目,并排展示
|
|
488
635
|
const qIds1 = new Set(run1.results.map((r) => r.questionId));
|
|
489
636
|
const qIds2 = new Set(run2.results.map((r) => r.questionId));
|
|
490
637
|
const common = [...qIds1].filter((q) => qIds2.has(q));
|
|
@@ -493,8 +640,10 @@ export async function benchCommand(args) {
|
|
|
493
640
|
const r1 = run1.results.find((r) => r.questionId === qId);
|
|
494
641
|
const r2 = run2.results.find((r) => r.questionId === qId);
|
|
495
642
|
if (r1 && r2) {
|
|
496
|
-
|
|
497
|
-
|
|
643
|
+
const s1 = r1.score !== undefined ? `[${r1.score}/${r1.maxScore}]` : "";
|
|
644
|
+
const s2 = r2.score !== undefined ? `[${r2.score}/${r2.maxScore}]` : "";
|
|
645
|
+
console.log(` ${s1} ${r1.modelId}: ${r1.response.slice(0, 200)}`);
|
|
646
|
+
console.log(` ${s2} ${r2.modelId}: ${r2.response.slice(0, 200)}`);
|
|
498
647
|
console.log("");
|
|
499
648
|
}
|
|
500
649
|
}
|
|
@@ -513,7 +662,6 @@ export async function benchCommand(args) {
|
|
|
513
662
|
console.log(" u1s1 bench help 查看用法");
|
|
514
663
|
process.exit(1);
|
|
515
664
|
}
|
|
516
|
-
/** 找最近一次运行的 id */
|
|
517
665
|
function findLatestRun() {
|
|
518
666
|
const runs = listRuns();
|
|
519
667
|
return runs[0]?.id ?? null;
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { execSync, spawnSync } from "node:child_process";
|
|
3
3
|
import { writeFileSync } from "node:fs";
|
|
4
|
-
import { cleanupBrandThemes, endpointKeyEnv, endpointProviderEntry, ensureBrandPrompt, ensureDefaultSettings, ensureProviderModels, scrubForeignProviderEnv, toProviderModels, writeWebToolsExtension, } from "./agent-setup.js";
|
|
4
|
+
import { cleanupBrandThemes, endpointKeyEnv, endpointProviderEntry, ensureBrandPrompt, ensureDefaultSettings, ensureProviderModels, scrubForeignProviderEnv, toProviderModels, writeCompactUiExtension, writeWebToolsExtension, } from "./agent-setup.js";
|
|
5
5
|
import { printConsoleBanner } from "./brand.js";
|
|
6
6
|
import { agentDir, apiModelToDef, CUSTOM_ENDPOINTS, isPortableInstall, loadConfig, MODELS, persistPreferredModel, PROVIDER_ID, readSettings, refValid, resolvePreferredModel, setModelsFromApi, VERSION, } from "./config.js";
|
|
7
|
+
import { registerLoopCommand } from "./loop.js";
|
|
7
8
|
import { ensureSearchTools } from "./search-tools.js";
|
|
8
9
|
import { ensureUsableShell } from "./shell-doctor.js";
|
|
9
10
|
import { applyBrandUi, setUpdateNotice } from "./style.js";
|
|
@@ -169,6 +170,8 @@ async function runAgent(cfg, args) {
|
|
|
169
170
|
webFetchRender: webFetchRenderEnabled,
|
|
170
171
|
imageGen: imageGenEnabled,
|
|
171
172
|
});
|
|
173
|
+
// 精简 UI:隐藏工具调用 + 汇总行 + 隐藏思考标签(同样投影到 agentDir/extensions)
|
|
174
|
+
writeCompactUiExtension();
|
|
172
175
|
ensureTmuxKeyboardProtocol();
|
|
173
176
|
// must be set before pi reads them (getAgentDir() reads at call time, env at import is fine too)
|
|
174
177
|
process.env["PI_CODING_AGENT_DIR"] = agentDir;
|
|
@@ -213,6 +216,8 @@ async function runAgent(cfg, args) {
|
|
|
213
216
|
factory: (pi) => {
|
|
214
217
|
applyBrandUi(pi, VERSION);
|
|
215
218
|
offerStarterTemplates(pi);
|
|
219
|
+
// /loop: 会话级定时循环执行提示词(对标 Claude Code),实现见 loop.ts
|
|
220
|
+
registerLoopCommand(pi);
|
|
216
221
|
// 会话标题:拿用户第一条正经输入的前半截当名字(像 Claude Code 那样),
|
|
217
222
|
// /resume 的会话列表里就能一眼认出每个会话。/clear 后 session_start
|
|
218
223
|
// 会重置标记,新会话重新起标题。
|
package/dist/loop.d.ts
ADDED
package/dist/loop.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /loop —— 会话级定时循环执行提示词(对标 Claude Code 的 /loop)。
|
|
3
|
+
*
|
|
4
|
+
* 用法:
|
|
5
|
+
* /loop 5m 检查部署状态 每 5 分钟跑一次
|
|
6
|
+
* /loop 检查构建 every 2h 间隔放后面也行
|
|
7
|
+
* /loop 盯一下 PR 不写间隔,默认 10 分钟
|
|
8
|
+
* /loop 20m /review-pr 1234 循环里也能跑其他斜杠命令
|
|
9
|
+
* /loop stop 停止当前循环
|
|
10
|
+
*
|
|
11
|
+
* 语义与 Claude Code 对齐:任务只活在当前会话,/clear 或退出即消失。
|
|
12
|
+
* 实现核心是 pi.sendUserMessage():定时器到点就"替用户打字"发一轮,
|
|
13
|
+
* deliverAs: "followUp" 保证上一轮没跑完时排队等它结束,不硬插打断;
|
|
14
|
+
* expandPromptTemplates: true 让 prompt 里的斜杠命令也能被展开执行。
|
|
15
|
+
*/
|
|
16
|
+
const DEFAULT_INTERVAL_MS = 10 * 60_000;
|
|
17
|
+
const STATUS_KEY = "loop";
|
|
18
|
+
/** 状态栏/通知里提示词最多显示这么长,防止长 prompt 把 UI 撑爆 */
|
|
19
|
+
const PROMPT_PREVIEW_MAX = 40;
|
|
20
|
+
let timer;
|
|
21
|
+
let activePrompt = "";
|
|
22
|
+
let activeIntervalMs = DEFAULT_INTERVAL_MS;
|
|
23
|
+
/** "30s"/"5m"/"2h"/"1d" → 毫秒;不合法返回 undefined */
|
|
24
|
+
function parseIntervalMs(token) {
|
|
25
|
+
const m = /^(\d+)([smhd])$/i.exec(token);
|
|
26
|
+
if (!m)
|
|
27
|
+
return undefined;
|
|
28
|
+
const n = Number(m[1]);
|
|
29
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
30
|
+
return undefined;
|
|
31
|
+
const unitMs = { s: 1_000, m: 60_000, h: 3_600_000, d: 86_400_000 }[m[2].toLowerCase()];
|
|
32
|
+
return n * unitMs;
|
|
33
|
+
}
|
|
34
|
+
function formatInterval(ms) {
|
|
35
|
+
if (ms >= 3_600_000 && ms % 3_600_000 === 0) {
|
|
36
|
+
return `${ms / 3_600_000}小时`;
|
|
37
|
+
}
|
|
38
|
+
if (ms % 60_000 === 0) {
|
|
39
|
+
return `${ms / 60_000}分钟`;
|
|
40
|
+
}
|
|
41
|
+
return `${Math.round(ms / 1_000)}秒`;
|
|
42
|
+
}
|
|
43
|
+
function preview(text) {
|
|
44
|
+
return text.length > PROMPT_PREVIEW_MAX ? `${text.slice(0, PROMPT_PREVIEW_MAX)}…` : text;
|
|
45
|
+
}
|
|
46
|
+
function stopLoop(ui) {
|
|
47
|
+
if (timer)
|
|
48
|
+
clearInterval(timer);
|
|
49
|
+
timer = undefined;
|
|
50
|
+
activePrompt = "";
|
|
51
|
+
ui?.setStatus(STATUS_KEY, undefined);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* 解析出 [间隔?, 提示词]。间隔可放在最前或最后(Claude Code 同款规则),
|
|
55
|
+
* 没写就用默认 10 分钟。解析失败返回 null 并已通过 notify 报错。
|
|
56
|
+
*/
|
|
57
|
+
function parseArgs(ui, raw) {
|
|
58
|
+
const tokens = raw.trim().split(/\s+/).filter(Boolean);
|
|
59
|
+
let intervalMs;
|
|
60
|
+
let intervalIdx = -1;
|
|
61
|
+
// 末尾的 "every 30m" 形式占两个 token,先于前置匹配检查
|
|
62
|
+
if (tokens.length >= 2 && tokens[tokens.length - 2].toLowerCase() === "every") {
|
|
63
|
+
const parsed = parseIntervalMs(tokens[tokens.length - 1]);
|
|
64
|
+
if (parsed !== undefined) {
|
|
65
|
+
intervalMs = parsed;
|
|
66
|
+
intervalIdx = tokens.length - 2;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (intervalMs === undefined) {
|
|
70
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
71
|
+
const parsed = parseIntervalMs(tokens[i]);
|
|
72
|
+
if (parsed !== undefined) {
|
|
73
|
+
intervalMs = parsed;
|
|
74
|
+
intervalIdx = i;
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const rest = tokens.slice();
|
|
80
|
+
if (intervalMs !== undefined) {
|
|
81
|
+
rest.splice(intervalIdx, intervalIdx === tokens.length - 2 ? 2 : 1);
|
|
82
|
+
}
|
|
83
|
+
const prompt = rest.join(" ").trim();
|
|
84
|
+
if (!prompt) {
|
|
85
|
+
ui.notify("用法: /loop [间隔] <提示词>,如 /loop 5m 检查部署状态;停止用 /loop stop", "error");
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
return { intervalMs: intervalMs ?? DEFAULT_INTERVAL_MS, prompt };
|
|
89
|
+
}
|
|
90
|
+
export function registerLoopCommand(pi) {
|
|
91
|
+
// 会话重置(/clear、新会话)时循环随之消亡——和 Claude Code 的会话级语义一致
|
|
92
|
+
pi.on("session_start", (_event, ctx) => stopLoop(ctx.ui));
|
|
93
|
+
pi.registerCommand("loop", {
|
|
94
|
+
description: "定时重复执行提示词,如 /loop 5m 检查部署状态;/loop stop 停止",
|
|
95
|
+
handler: async (args, ctx) => {
|
|
96
|
+
const input = args.trim();
|
|
97
|
+
if (!input) {
|
|
98
|
+
ctx.ui.notify(timer
|
|
99
|
+
? `🔁 当前循环:每${formatInterval(activeIntervalMs)} · ${preview(activePrompt)}(/loop stop 停止)`
|
|
100
|
+
: "用法: /loop [间隔] <提示词>,如 /loop 5m 检查部署状态", "info");
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (/^(stop|off|取消|停止)$/i.test(input)) {
|
|
104
|
+
if (!timer) {
|
|
105
|
+
ctx.ui.notify("没有正在运行的循环", "info");
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
stopLoop(ctx.ui);
|
|
109
|
+
ctx.ui.notify("⏹ 循环已停止", "info");
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const parsed = parseArgs(ctx.ui, input);
|
|
113
|
+
if (!parsed)
|
|
114
|
+
return;
|
|
115
|
+
// 已有循环在跑就直接替换成新的(先清旧定时器)
|
|
116
|
+
stopLoop(ctx.ui);
|
|
117
|
+
activePrompt = parsed.prompt;
|
|
118
|
+
activeIntervalMs = parsed.intervalMs;
|
|
119
|
+
timer = setInterval(() => {
|
|
120
|
+
try {
|
|
121
|
+
pi.sendUserMessage(activePrompt, {
|
|
122
|
+
deliverAs: "followUp",
|
|
123
|
+
expandPromptTemplates: true,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
catch (e) {
|
|
127
|
+
ctx.ui.notify(`loop 执行失败: ${e instanceof Error ? e.message : String(e)}`, "error");
|
|
128
|
+
}
|
|
129
|
+
}, activeIntervalMs);
|
|
130
|
+
ctx.ui.setStatus(STATUS_KEY, `🔁 每${formatInterval(activeIntervalMs)} · ${preview(activePrompt)}`);
|
|
131
|
+
ctx.ui.notify(`🔁 已开始:每${formatInterval(activeIntervalMs)}执行一次「${preview(activePrompt)}」`, "info");
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "u1s1-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.1",
|
|
4
4
|
"description": "u1s1 — 有一说一,最省心的 AI 编程搭子。终端里用中文说需求,AI 帮你读文件、改代码、跑命令。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
"files": [
|
|
17
17
|
"dist",
|
|
18
18
|
"webui-dist",
|
|
19
|
-
"bench"
|
|
19
|
+
"bench",
|
|
20
|
+
"scripts"
|
|
20
21
|
],
|
|
21
22
|
"engines": {
|
|
22
23
|
"node": ">=22.19.0"
|
|
@@ -26,6 +27,7 @@
|
|
|
26
27
|
"dev": "tsx src/index.ts",
|
|
27
28
|
"typecheck": "tsc --noEmit",
|
|
28
29
|
"prepublishOnly": "npm run build",
|
|
30
|
+
"postinstall": "node scripts/patch-pi.js",
|
|
29
31
|
"package": "bash ../../scripts/package.sh",
|
|
30
32
|
"upload-release": "bash ../../scripts/upload-release.sh",
|
|
31
33
|
"release": "npm run package && npm run upload-release"
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* postinstall 补丁:给 @earendil-works/pi-coding-agent 打「精简 UI」补丁。
|
|
3
|
+
*
|
|
4
|
+
* 为什么存在:仓库里用 pnpm patch(patches/*.patch)对开发态生效,但终端用户
|
|
5
|
+
* 是 `npm i -g u1s1-cli` 装的,npm 不认 pnpm 的补丁机制。这个脚本在用户安装后
|
|
6
|
+
* 对 node_modules 里的 pi 做同样的字符串替换,保证空行修复对所有安装方式生效。
|
|
7
|
+
*
|
|
8
|
+
* 特性:
|
|
9
|
+
* - 纯 Node 实现,不依赖 patch(1),Windows 可跑
|
|
10
|
+
* - 幂等:已打过(含 pnpm patch 打过)直接跳过
|
|
11
|
+
* - 永不让安装失败:任何异常只提示,exit 0
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
15
|
+
import { join, dirname } from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
|
|
18
|
+
const pkgRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
19
|
+
const target = join(
|
|
20
|
+
pkgRoot,
|
|
21
|
+
"node_modules",
|
|
22
|
+
"@earendil-works",
|
|
23
|
+
"pi-coding-agent",
|
|
24
|
+
"dist",
|
|
25
|
+
"modes",
|
|
26
|
+
"interactive",
|
|
27
|
+
"components",
|
|
28
|
+
"assistant-message.js",
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
// 每条 [原文, 替换后];与 patches/@earendil-works__pi-coding-agent@0.84.2.patch 等效
|
|
32
|
+
const REPLACEMENTS = [
|
|
33
|
+
// 隐藏的思考块不算「可见内容」(消息开头不再加空行)
|
|
34
|
+
[
|
|
35
|
+
'|| (c.type === "thinking" && c.thinking.trim())',
|
|
36
|
+
'|| (c.type === "thinking" && !this.hideThinkingBlock && c.thinking.trim())',
|
|
37
|
+
],
|
|
38
|
+
// 折叠标签为空字符串时完全跳过(ANSI 包着的空串仍会画出一整行空白)
|
|
39
|
+
[
|
|
40
|
+
'this.contentContainer.addChild(new Text(theme.italic(theme.fg("thinkingText", this.hiddenThinkingLabel)), this.outputPad, 0));',
|
|
41
|
+
'if (this.hiddenThinkingLabel !== "") { this.contentContainer.addChild(new Text(theme.italic(theme.fg("thinkingText", this.hiddenThinkingLabel)), this.outputPad, 0)); }',
|
|
42
|
+
],
|
|
43
|
+
// 思考块完全隐形时,不再加它后面的间隔空行
|
|
44
|
+
[
|
|
45
|
+
"if (hasVisibleContentAfter) {",
|
|
46
|
+
'if (hasVisibleContentAfter && !(this.hideThinkingBlock && this.hiddenThinkingLabel === "")) {',
|
|
47
|
+
],
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
let text;
|
|
52
|
+
try {
|
|
53
|
+
text = readFileSync(target, "utf8");
|
|
54
|
+
} catch {
|
|
55
|
+
// 找不到 pi 就静默退出(不该发生:postinstall 在依赖装完后跑)
|
|
56
|
+
process.exit(0);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (text.includes("!this.hideThinkingBlock && c.thinking.trim()")) {
|
|
60
|
+
// 已打过(pnpm patch 或上次运行),幂等跳过
|
|
61
|
+
process.exit(0);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let applied = 0;
|
|
65
|
+
for (const [from, to] of REPLACEMENTS) {
|
|
66
|
+
if (text.includes(to)) {
|
|
67
|
+
applied++;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (!text.includes(from)) continue;
|
|
71
|
+
text = text.split(from).join(to);
|
|
72
|
+
applied++;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (applied < REPLACEMENTS.length) {
|
|
76
|
+
console.log(
|
|
77
|
+
`[u1s1] 提示: pi 版本可能已更新,精简 UI 空行补丁只应用了 ${applied}/${REPLACEMENTS.length} 处(不影响使用)`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
writeFileSync(target, text);
|
|
82
|
+
} catch (err) {
|
|
83
|
+
console.log(`[u1s1] 提示: 精简 UI 空行补丁未生效(${err?.message ?? err}),不影响使用`);
|
|
84
|
+
}
|
|
85
|
+
process.exit(0);
|