u1s1-cli 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-setup.d.ts +19 -1
- package/dist/agent-setup.js +49 -10
- package/dist/api.d.ts +24 -0
- package/dist/api.js +34 -1
- package/dist/config.d.ts +58 -5
- package/dist/config.js +103 -13
- package/dist/import/write.js +3 -2
- package/dist/index.js +26 -20
- package/dist/model.js +31 -7
- package/dist/search-tools.d.ts +3 -0
- package/dist/search-tools.js +153 -0
- package/dist/web.js +13 -3
- package/package.json +1 -1
package/dist/agent-setup.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type CliConfig } from "./config.js";
|
|
1
|
+
import { type CliConfig, type CustomEndpoint, type ModelDef } from "./config.js";
|
|
2
2
|
/** pi auto-appends <agentDir>/APPEND_SYSTEM.md to its system prompt — our branding hook. */
|
|
3
3
|
export declare function ensureBrandPrompt(): void;
|
|
4
4
|
/** Defaults that don't overwrite values the user already set. */
|
|
@@ -29,4 +29,22 @@ export declare function writeWebToolsExtension(cfg: CliConfig, webSearchEnabled:
|
|
|
29
29
|
* 其他 provider 的凭据原样保留;文件损坏时不动它,交给 SDK 自己报错。
|
|
30
30
|
*/
|
|
31
31
|
export declare function ensureAuthCredential(): void;
|
|
32
|
+
/** pi provider 条目里的模型形状(models.json 与 registerProvider 共用)。 */
|
|
33
|
+
export declare function toProviderModels(models: ModelDef[]): {
|
|
34
|
+
id: string;
|
|
35
|
+
name: string;
|
|
36
|
+
reasoning: boolean;
|
|
37
|
+
input: ("text" | "image")[];
|
|
38
|
+
cost: ModelDef["cost"];
|
|
39
|
+
contextWindow: number;
|
|
40
|
+
maxTokens: number;
|
|
41
|
+
}[];
|
|
42
|
+
/**
|
|
43
|
+
* 自定义端点 → pi provider 条目。密钥走环境变量引用(启动器已 set,见
|
|
44
|
+
* applyEndpointKeyEnv),文件里不落明文;没配密钥的端点(本机 Ollama 等)
|
|
45
|
+
* 直接不写 apiKey 字段。
|
|
46
|
+
*/
|
|
47
|
+
export declare function endpointProviderEntry(ep: CustomEndpoint): Record<string, unknown>;
|
|
48
|
+
/** 把各端点密钥放进环境(TUI 直接 process.env;web 传给子进程)。 */
|
|
49
|
+
export declare function endpointKeyEnv(): Record<string, string>;
|
|
32
50
|
export declare function ensureProviderModels(cfg: CliConfig): void;
|
package/dist/agent-setup.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { agentDir, MODELS, PROVIDER_ID, VERSION } from "./config.js";
|
|
3
|
+
import { agentDir, CUSTOM_ENDPOINTS, ENDPOINT_ID_RE, endpointKeyEnvName, MODELS, PROVIDER_ID, VERSION, } from "./config.js";
|
|
4
4
|
const BRAND_APPEND = `## u1s1
|
|
5
5
|
|
|
6
6
|
你是 u1s1(有一说一) —— 说人话的 AI 编程搭子,一个面向编程新手的中文 AI 编程助手。用户很可能不熟悉编程术语:
|
|
@@ -133,6 +133,43 @@ export function ensureAuthCredential() {
|
|
|
133
133
|
root[PROVIDER_ID] = { type: "api_key", key: "$U1S1_API_KEY" };
|
|
134
134
|
writeFileSync(p, JSON.stringify(root, null, 2) + "\n", { mode: 0o600 });
|
|
135
135
|
}
|
|
136
|
+
/** pi provider 条目里的模型形状(models.json 与 registerProvider 共用)。 */
|
|
137
|
+
export function toProviderModels(models) {
|
|
138
|
+
return models.map((m) => ({
|
|
139
|
+
id: m.id,
|
|
140
|
+
name: m.name,
|
|
141
|
+
reasoning: m.reasoning,
|
|
142
|
+
input: ["text"],
|
|
143
|
+
cost: m.cost,
|
|
144
|
+
contextWindow: m.contextWindow,
|
|
145
|
+
maxTokens: m.maxTokens,
|
|
146
|
+
}));
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* 自定义端点 → pi provider 条目。密钥走环境变量引用(启动器已 set,见
|
|
150
|
+
* applyEndpointKeyEnv),文件里不落明文;没配密钥的端点(本机 Ollama 等)
|
|
151
|
+
* 直接不写 apiKey 字段。
|
|
152
|
+
*/
|
|
153
|
+
export function endpointProviderEntry(ep) {
|
|
154
|
+
const entry = {
|
|
155
|
+
name: ep.name,
|
|
156
|
+
baseUrl: ep.baseUrl,
|
|
157
|
+
api: ep.api,
|
|
158
|
+
models: toProviderModels(ep.models),
|
|
159
|
+
};
|
|
160
|
+
if (ep.apiKey)
|
|
161
|
+
entry["apiKey"] = `$${endpointKeyEnvName(ep.id)}`;
|
|
162
|
+
return entry;
|
|
163
|
+
}
|
|
164
|
+
/** 把各端点密钥放进环境(TUI 直接 process.env;web 传给子进程)。 */
|
|
165
|
+
export function endpointKeyEnv() {
|
|
166
|
+
const env = {};
|
|
167
|
+
for (const ep of CUSTOM_ENDPOINTS) {
|
|
168
|
+
if (ep.apiKey)
|
|
169
|
+
env[endpointKeyEnvName(ep.id)] = ep.apiKey;
|
|
170
|
+
}
|
|
171
|
+
return env;
|
|
172
|
+
}
|
|
136
173
|
export function ensureProviderModels(cfg) {
|
|
137
174
|
mkdirSync(agentDir, { recursive: true });
|
|
138
175
|
const p = join(agentDir, "models.json");
|
|
@@ -155,16 +192,18 @@ export function ensureProviderModels(cfg) {
|
|
|
155
192
|
apiKey: "$U1S1_API_KEY",
|
|
156
193
|
// 网关按这个头识别客户端版本;不带头的旧版会在会话首轮被追加升级提示
|
|
157
194
|
headers: { "x-u1s1-version": VERSION },
|
|
158
|
-
models: MODELS
|
|
159
|
-
id: m.id,
|
|
160
|
-
name: m.name,
|
|
161
|
-
reasoning: m.reasoning,
|
|
162
|
-
input: ["text"],
|
|
163
|
-
cost: m.cost,
|
|
164
|
-
contextWindow: m.contextWindow,
|
|
165
|
-
maxTokens: m.maxTokens,
|
|
166
|
-
})),
|
|
195
|
+
models: toProviderModels(MODELS),
|
|
167
196
|
};
|
|
197
|
+
// 自定义端点:先清掉我们此前写入、如今已删除的条目(键形状 ep+32hex,
|
|
198
|
+
// 用户手工加的其他 provider 不受影响),再写当前列表
|
|
199
|
+
for (const key of Object.keys(providers)) {
|
|
200
|
+
if (ENDPOINT_ID_RE.test(key) && !CUSTOM_ENDPOINTS.some((e) => e.id === key)) {
|
|
201
|
+
delete providers[key];
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
for (const ep of CUSTOM_ENDPOINTS) {
|
|
205
|
+
providers[ep.id] = endpointProviderEntry(ep);
|
|
206
|
+
}
|
|
168
207
|
root["providers"] = providers;
|
|
169
208
|
writeFileSync(p, JSON.stringify(root, null, 2) + "\n");
|
|
170
209
|
}
|
package/dist/api.d.ts
CHANGED
|
@@ -36,6 +36,30 @@ export interface ModelsResponse {
|
|
|
36
36
|
features: ApiFeatures;
|
|
37
37
|
}
|
|
38
38
|
export declare function fetchModels(cfg: CliConfig): Promise<ModelsResponse>;
|
|
39
|
+
export interface ApiEndpoint {
|
|
40
|
+
id: string;
|
|
41
|
+
name: string;
|
|
42
|
+
base_url: string;
|
|
43
|
+
api: string;
|
|
44
|
+
api_key: string | null;
|
|
45
|
+
models: {
|
|
46
|
+
id: string;
|
|
47
|
+
name?: string;
|
|
48
|
+
reasoning?: boolean;
|
|
49
|
+
context_window?: number;
|
|
50
|
+
max_tokens?: number;
|
|
51
|
+
}[];
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* 拉取用户在云端配置的自定义模型端点(dashboard「自定义模型端点」卡片)。
|
|
55
|
+
* 老网关没有这个路由(GET 落到静态资产 404),调用方失败时回退本地缓存。
|
|
56
|
+
*/
|
|
57
|
+
export declare function fetchUserEndpoints(cfg: CliConfig): Promise<ApiEndpoint[]>;
|
|
58
|
+
/**
|
|
59
|
+
* 拉取云端配置的自定义端点并装载进 CUSTOM_ENDPOINTS;失败(离线/老网关)回退
|
|
60
|
+
* 上次缓存,保证本地端点断网时依旧可用。成功时刷新缓存(~/.u1s1/endpoints.json)。
|
|
61
|
+
*/
|
|
62
|
+
export declare function loadCustomEndpoints(cfg: CliConfig): Promise<void>;
|
|
39
63
|
export interface SearchResult {
|
|
40
64
|
title: string;
|
|
41
65
|
url: string;
|
package/dist/api.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { VERSION } from "./config.js";
|
|
1
|
+
import { apiEndpointToCustom, loadEndpointsCache, saveEndpointsCache, setCustomEndpoints, VERSION, } from "./config.js";
|
|
2
2
|
/** 网关按 x-u1s1-version 识别客户端版本(旧版 CLI 不带,提示升级)。 */
|
|
3
3
|
function authHeaders(apiKey) {
|
|
4
4
|
return { authorization: `Bearer ${apiKey}`, "x-u1s1-version": VERSION };
|
|
@@ -20,6 +20,39 @@ export async function fetchModels(cfg) {
|
|
|
20
20
|
const body = (await resp.json());
|
|
21
21
|
return { models: body.data, features: body.features ?? {} };
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* 拉取用户在云端配置的自定义模型端点(dashboard「自定义模型端点」卡片)。
|
|
25
|
+
* 老网关没有这个路由(GET 落到静态资产 404),调用方失败时回退本地缓存。
|
|
26
|
+
*/
|
|
27
|
+
export async function fetchUserEndpoints(cfg) {
|
|
28
|
+
let resp;
|
|
29
|
+
try {
|
|
30
|
+
resp = await fetch(`${cfg.baseUrl}/endpoints`, {
|
|
31
|
+
headers: authHeaders(cfg.apiKey),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
throw new Error(`连不上 ${cfg.baseUrl}`);
|
|
36
|
+
}
|
|
37
|
+
if (!resp.ok)
|
|
38
|
+
throw new Error(`服务端返回 ${resp.status}`);
|
|
39
|
+
const body = (await resp.json());
|
|
40
|
+
return Array.isArray(body.endpoints) ? body.endpoints : [];
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* 拉取云端配置的自定义端点并装载进 CUSTOM_ENDPOINTS;失败(离线/老网关)回退
|
|
44
|
+
* 上次缓存,保证本地端点断网时依旧可用。成功时刷新缓存(~/.u1s1/endpoints.json)。
|
|
45
|
+
*/
|
|
46
|
+
export async function loadCustomEndpoints(cfg) {
|
|
47
|
+
try {
|
|
48
|
+
const endpoints = (await fetchUserEndpoints(cfg)).map(apiEndpointToCustom);
|
|
49
|
+
setCustomEndpoints(endpoints);
|
|
50
|
+
saveEndpointsCache(endpoints);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
setCustomEndpoints(loadEndpointsCache());
|
|
54
|
+
}
|
|
55
|
+
}
|
|
23
56
|
/** 联网搜索走网关代理(上游 key 只在服务端)。maxResults 不传时由服务端决定默认值。 */
|
|
24
57
|
export async function searchWeb(cfg, query, maxResults, signal) {
|
|
25
58
|
if (!cfg.apiKey)
|
package/dist/config.d.ts
CHANGED
|
@@ -45,11 +45,61 @@ export declare const MODELS: ModelDef[];
|
|
|
45
45
|
export declare function setModelsFromApi(apiModels: ModelDef[]): void;
|
|
46
46
|
export declare const DEFAULT_MODEL_ID: string;
|
|
47
47
|
export declare function resolveModel(nameOrAlias: string): ModelDef | undefined;
|
|
48
|
+
/** provider 注册键就是云端的端点 id(ep+uuid);models.json 清理旧条目按此形状识别。 */
|
|
49
|
+
export declare const ENDPOINT_ID_RE: RegExp;
|
|
50
|
+
export interface CustomEndpoint {
|
|
51
|
+
/** 云端端点 id,同时作为 pi provider 的注册键 */
|
|
52
|
+
id: string;
|
|
53
|
+
name: string;
|
|
54
|
+
baseUrl: string;
|
|
55
|
+
/** pi 协议类型:openai-completions / anthropic-messages */
|
|
56
|
+
api: string;
|
|
57
|
+
apiKey?: string;
|
|
58
|
+
models: ModelDef[];
|
|
59
|
+
}
|
|
60
|
+
export declare const CUSTOM_ENDPOINTS: CustomEndpoint[];
|
|
61
|
+
export declare function setCustomEndpoints(endpoints: CustomEndpoint[]): void;
|
|
62
|
+
/** 端点密钥不写进 models.json,走这个环境变量引用(pi 读取时解析)。 */
|
|
63
|
+
export declare function endpointKeyEnvName(endpointId: string): string;
|
|
64
|
+
/** 服务端 /v1/endpoints 的一条记录 → CustomEndpoint(补默认值、生成别名)。 */
|
|
65
|
+
export declare function apiEndpointToCustom(e: {
|
|
66
|
+
id: string;
|
|
67
|
+
name: string;
|
|
68
|
+
base_url: string;
|
|
69
|
+
api: string;
|
|
70
|
+
api_key: string | null;
|
|
71
|
+
models: {
|
|
72
|
+
id: string;
|
|
73
|
+
name?: string;
|
|
74
|
+
reasoning?: boolean;
|
|
75
|
+
context_window?: number;
|
|
76
|
+
max_tokens?: number;
|
|
77
|
+
}[];
|
|
78
|
+
}): CustomEndpoint;
|
|
79
|
+
/** 模型引用 = provider 注册键 + 模型 id;provider 是 u1s1 或某个端点 id。 */
|
|
80
|
+
export interface ModelRef {
|
|
81
|
+
provider: string;
|
|
82
|
+
id: string;
|
|
83
|
+
}
|
|
84
|
+
export interface ResolvedModel extends ModelRef {
|
|
85
|
+
/** 展示用:u1s1 或端点名 */
|
|
86
|
+
providerName: string;
|
|
87
|
+
model: ModelDef;
|
|
88
|
+
}
|
|
89
|
+
/** ref 指向的模型仍然存在吗(服务端下架/端点删除后失效)。 */
|
|
90
|
+
export declare function refValid(ref: ModelRef): boolean;
|
|
91
|
+
/**
|
|
92
|
+
* 按名字/别名找模型,支持三种写法:模型 id、别名、`端点名:模型id`。
|
|
93
|
+
* u1s1 官方模型优先,再按端点顺序找,重名取先命中。
|
|
94
|
+
*/
|
|
95
|
+
export declare function findModel(query: string): ResolvedModel | undefined;
|
|
48
96
|
export interface CliConfig {
|
|
49
97
|
apiKey?: string;
|
|
50
98
|
baseUrl: string;
|
|
51
|
-
/** preferred model id
|
|
99
|
+
/** preferred model id (within modelProvider) */
|
|
52
100
|
model?: string;
|
|
101
|
+
/** preferred model 所属 provider;缺省 = u1s1(老配置兼容) */
|
|
102
|
+
modelProvider?: string;
|
|
53
103
|
}
|
|
54
104
|
export declare const u1s1Dir: string;
|
|
55
105
|
/** pi keeps auth/models/settings/sessions under this dir — isolated from any real pi install. */
|
|
@@ -63,15 +113,18 @@ export interface AgentSettings {
|
|
|
63
113
|
/** Read u1s1 agent settings (settings.json). Returns empty object if missing or invalid. */
|
|
64
114
|
export declare function readSettings(): AgentSettings;
|
|
65
115
|
/** Model last chosen in-session via /model (pi writes this). */
|
|
66
|
-
export declare function readAgentDefaultModel():
|
|
116
|
+
export declare function readAgentDefaultModel(): ModelRef | undefined;
|
|
67
117
|
/** Keep pi's settings.json in sync so /model and `u1s1 model` share one default. */
|
|
68
|
-
export declare function writeAgentDefaultModel(modelId: string): void;
|
|
118
|
+
export declare function writeAgentDefaultModel(provider: string, modelId: string): void;
|
|
69
119
|
/**
|
|
70
120
|
* After this fix both stores stay in sync. If they still disagree (old installs),
|
|
71
121
|
* prefer the in-session /model value — that's the one users thought they had set.
|
|
122
|
+
* 校验依赖 CUSTOM_ENDPOINTS,须在 setCustomEndpoints 之后调用才认得端点模型。
|
|
72
123
|
*/
|
|
73
|
-
export declare function resolvePreferredModel(
|
|
124
|
+
export declare function resolvePreferredModel(cfg: Pick<CliConfig, "model" | "modelProvider">): ModelRef;
|
|
74
125
|
export declare function loadConfig(): CliConfig;
|
|
75
126
|
export declare function saveConfig(cfg: CliConfig): void;
|
|
76
127
|
/** Persist the user's preferred model to both stores. */
|
|
77
|
-
export declare function persistPreferredModel(cfg: CliConfig, modelId: string): CliConfig;
|
|
128
|
+
export declare function persistPreferredModel(cfg: CliConfig, provider: string, modelId: string): CliConfig;
|
|
129
|
+
export declare function saveEndpointsCache(endpoints: CustomEndpoint[]): void;
|
|
130
|
+
export declare function loadEndpointsCache(): CustomEndpoint[];
|
package/dist/config.js
CHANGED
|
@@ -81,6 +81,71 @@ export function resolveModel(nameOrAlias) {
|
|
|
81
81
|
const q = nameOrAlias.trim().toLowerCase();
|
|
82
82
|
return MODELS.find((m) => m.id.toLowerCase() === q || m.aliases.includes(q));
|
|
83
83
|
}
|
|
84
|
+
// ---- 用户自定义端点(云端 dashboard 配置,启动时经 /v1/endpoints 拉取)----
|
|
85
|
+
/** provider 注册键就是云端的端点 id(ep+uuid);models.json 清理旧条目按此形状识别。 */
|
|
86
|
+
export const ENDPOINT_ID_RE = /^ep[0-9a-f]{32}$/;
|
|
87
|
+
export const CUSTOM_ENDPOINTS = [];
|
|
88
|
+
export function setCustomEndpoints(endpoints) {
|
|
89
|
+
CUSTOM_ENDPOINTS.length = 0;
|
|
90
|
+
CUSTOM_ENDPOINTS.push(...endpoints);
|
|
91
|
+
}
|
|
92
|
+
/** 端点密钥不写进 models.json,走这个环境变量引用(pi 读取时解析)。 */
|
|
93
|
+
export function endpointKeyEnvName(endpointId) {
|
|
94
|
+
return `U1S1_EP_KEY_${endpointId}`;
|
|
95
|
+
}
|
|
96
|
+
/** 服务端 /v1/endpoints 的一条记录 → CustomEndpoint(补默认值、生成别名)。 */
|
|
97
|
+
export function apiEndpointToCustom(e) {
|
|
98
|
+
return {
|
|
99
|
+
id: e.id,
|
|
100
|
+
name: e.name,
|
|
101
|
+
baseUrl: e.base_url,
|
|
102
|
+
api: e.api,
|
|
103
|
+
apiKey: e.api_key ?? undefined,
|
|
104
|
+
models: e.models.map((m) => ({
|
|
105
|
+
id: m.id,
|
|
106
|
+
name: m.name || m.id,
|
|
107
|
+
aliases: defaultAliases(m.id),
|
|
108
|
+
reasoning: !!m.reasoning,
|
|
109
|
+
contextWindow: m.context_window ?? 128_000,
|
|
110
|
+
maxTokens: m.max_tokens ?? 16_384,
|
|
111
|
+
// 自有端点不经网关计费,价格未知,展示按 0 处理
|
|
112
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
113
|
+
note: "",
|
|
114
|
+
})),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/** ref 指向的模型仍然存在吗(服务端下架/端点删除后失效)。 */
|
|
118
|
+
export function refValid(ref) {
|
|
119
|
+
if (ref.provider === PROVIDER_ID)
|
|
120
|
+
return MODELS.some((m) => m.id === ref.id);
|
|
121
|
+
const ep = CUSTOM_ENDPOINTS.find((e) => e.id === ref.provider);
|
|
122
|
+
return !!ep && ep.models.some((m) => m.id === ref.id);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* 按名字/别名找模型,支持三种写法:模型 id、别名、`端点名:模型id`。
|
|
126
|
+
* u1s1 官方模型优先,再按端点顺序找,重名取先命中。
|
|
127
|
+
*/
|
|
128
|
+
export function findModel(query) {
|
|
129
|
+
const q = query.trim();
|
|
130
|
+
const colon = q.indexOf(":");
|
|
131
|
+
if (colon > 0) {
|
|
132
|
+
const epName = q.slice(0, colon).toLowerCase();
|
|
133
|
+
const rest = q.slice(colon + 1).trim().toLowerCase();
|
|
134
|
+
const ep = CUSTOM_ENDPOINTS.find((e) => e.name.toLowerCase() === epName);
|
|
135
|
+
const m = ep?.models.find((x) => x.id.toLowerCase() === rest || x.aliases.includes(rest));
|
|
136
|
+
return ep && m ? { provider: ep.id, providerName: ep.name, id: m.id, model: m } : undefined;
|
|
137
|
+
}
|
|
138
|
+
const builtin = resolveModel(q);
|
|
139
|
+
if (builtin)
|
|
140
|
+
return { provider: PROVIDER_ID, providerName: "u1s1", id: builtin.id, model: builtin };
|
|
141
|
+
const lower = q.toLowerCase();
|
|
142
|
+
for (const ep of CUSTOM_ENDPOINTS) {
|
|
143
|
+
const m = ep.models.find((x) => x.id.toLowerCase() === lower || x.aliases.includes(lower));
|
|
144
|
+
if (m)
|
|
145
|
+
return { provider: ep.id, providerName: ep.name, id: m.id, model: m };
|
|
146
|
+
}
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
84
149
|
export const u1s1Dir = join(homedir(), ".u1s1");
|
|
85
150
|
const configFile = join(u1s1Dir, "config.json");
|
|
86
151
|
/** pi keeps auth/models/settings/sessions under this dir — isolated from any real pi install. */
|
|
@@ -105,35 +170,49 @@ export function readAgentDefaultModel() {
|
|
|
105
170
|
const settings = readJsonFile(agentSettingsFile);
|
|
106
171
|
if (!settings)
|
|
107
172
|
return undefined;
|
|
108
|
-
|
|
109
|
-
return undefined;
|
|
173
|
+
const provider = settings["defaultProvider"];
|
|
110
174
|
const id = settings["defaultModel"];
|
|
111
|
-
|
|
175
|
+
if (typeof provider !== "string" || typeof id !== "string")
|
|
176
|
+
return undefined;
|
|
177
|
+
if (provider !== PROVIDER_ID && !ENDPOINT_ID_RE.test(provider))
|
|
178
|
+
return undefined;
|
|
179
|
+
const ref = { provider, id };
|
|
180
|
+
return refValid(ref) ? ref : undefined;
|
|
112
181
|
}
|
|
113
182
|
/** Keep pi's settings.json in sync so /model and `u1s1 model` share one default. */
|
|
114
|
-
export function writeAgentDefaultModel(modelId) {
|
|
183
|
+
export function writeAgentDefaultModel(provider, modelId) {
|
|
115
184
|
mkdirSync(agentDir, { recursive: true });
|
|
116
185
|
const settings = readJsonFile(agentSettingsFile) ?? {};
|
|
117
|
-
if (settings["defaultProvider"] ===
|
|
186
|
+
if (settings["defaultProvider"] === provider && settings["defaultModel"] === modelId)
|
|
118
187
|
return;
|
|
119
|
-
settings["defaultProvider"] =
|
|
188
|
+
settings["defaultProvider"] = provider;
|
|
120
189
|
settings["defaultModel"] = modelId;
|
|
121
190
|
writeFileSync(agentSettingsFile, JSON.stringify(settings, null, 2) + "\n");
|
|
122
191
|
}
|
|
123
192
|
/**
|
|
124
193
|
* After this fix both stores stay in sync. If they still disagree (old installs),
|
|
125
194
|
* prefer the in-session /model value — that's the one users thought they had set.
|
|
195
|
+
* 校验依赖 CUSTOM_ENDPOINTS,须在 setCustomEndpoints 之后调用才认得端点模型。
|
|
126
196
|
*/
|
|
127
|
-
export function resolvePreferredModel(
|
|
128
|
-
const
|
|
129
|
-
|
|
197
|
+
export function resolvePreferredModel(cfg) {
|
|
198
|
+
const agentRef = readAgentDefaultModel();
|
|
199
|
+
if (agentRef)
|
|
200
|
+
return agentRef;
|
|
201
|
+
if (cfg.model) {
|
|
202
|
+
const ref = { provider: cfg.modelProvider ?? PROVIDER_ID, id: cfg.model };
|
|
203
|
+
if (refValid(ref))
|
|
204
|
+
return ref;
|
|
205
|
+
}
|
|
206
|
+
return { provider: PROVIDER_ID, id: DEFAULT_MODEL_ID };
|
|
130
207
|
}
|
|
131
208
|
export function loadConfig() {
|
|
132
209
|
const file = (readJsonFile(configFile) ?? {});
|
|
133
210
|
return {
|
|
134
211
|
apiKey: process.env["U1S1_API_KEY"] || file.apiKey,
|
|
135
212
|
baseUrl: process.env["U1S1_BASE_URL"] || file.baseUrl || DEFAULT_BASE_URL,
|
|
136
|
-
|
|
213
|
+
// 有效性不在这里裁决:端点列表可能还没拉回来,交给 resolvePreferredModel
|
|
214
|
+
model: file.model,
|
|
215
|
+
modelProvider: typeof file.modelProvider === "string" ? file.modelProvider : undefined,
|
|
137
216
|
};
|
|
138
217
|
}
|
|
139
218
|
export function saveConfig(cfg) {
|
|
@@ -142,9 +221,20 @@ export function saveConfig(cfg) {
|
|
|
142
221
|
chmodSync(configFile, 0o600);
|
|
143
222
|
}
|
|
144
223
|
/** Persist the user's preferred model to both stores. */
|
|
145
|
-
export function persistPreferredModel(cfg, modelId) {
|
|
146
|
-
const next = { ...cfg, model: modelId };
|
|
224
|
+
export function persistPreferredModel(cfg, provider, modelId) {
|
|
225
|
+
const next = { ...cfg, model: modelId, modelProvider: provider };
|
|
147
226
|
saveConfig(next);
|
|
148
|
-
writeAgentDefaultModel(modelId);
|
|
227
|
+
writeAgentDefaultModel(provider, modelId);
|
|
149
228
|
return next;
|
|
150
229
|
}
|
|
230
|
+
// ---- 端点本地缓存:离线/网关不可达时沿用上次拉到的配置(含密钥,0600)----
|
|
231
|
+
const endpointsCacheFile = join(u1s1Dir, "endpoints.json");
|
|
232
|
+
export function saveEndpointsCache(endpoints) {
|
|
233
|
+
mkdirSync(u1s1Dir, { recursive: true, mode: 0o700 });
|
|
234
|
+
writeFileSync(endpointsCacheFile, JSON.stringify(endpoints, null, 2) + "\n");
|
|
235
|
+
chmodSync(endpointsCacheFile, 0o600);
|
|
236
|
+
}
|
|
237
|
+
export function loadEndpointsCache() {
|
|
238
|
+
const raw = readJsonFile(endpointsCacheFile);
|
|
239
|
+
return Array.isArray(raw) ? raw : [];
|
|
240
|
+
}
|
package/dist/import/write.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
import { SessionManager } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import { agentDir, loadConfig,
|
|
4
|
+
import { agentDir, loadConfig, resolvePreferredModel } from "../config.js";
|
|
5
5
|
const EMPTY_USAGE = {
|
|
6
6
|
input: 0,
|
|
7
7
|
output: 0,
|
|
@@ -73,7 +73,8 @@ export function writeConvertedSession(converted, destCwd) {
|
|
|
73
73
|
if (title)
|
|
74
74
|
sm.appendSessionInfo(title);
|
|
75
75
|
// Resume should keep using u1s1 models, not the original Claude/Codex id.
|
|
76
|
-
|
|
76
|
+
const pref = resolvePreferredModel(loadConfig());
|
|
77
|
+
sm.appendModelChange(pref.provider, pref.id);
|
|
77
78
|
const dest = sm.getSessionFile();
|
|
78
79
|
if (!dest)
|
|
79
80
|
throw new Error("会话写盘失败");
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawnSync } from "node:child_process";
|
|
3
3
|
import { writeFileSync } from "node:fs";
|
|
4
|
-
import { cleanupBrandThemes, ensureBrandPrompt, ensureDefaultSettings, ensureProviderModels, writeWebToolsExtension, } from "./agent-setup.js";
|
|
4
|
+
import { cleanupBrandThemes, endpointKeyEnv, endpointProviderEntry, ensureBrandPrompt, ensureDefaultSettings, ensureProviderModels, toProviderModels, writeWebToolsExtension, } from "./agent-setup.js";
|
|
5
5
|
import { printConsoleBanner } from "./brand.js";
|
|
6
|
-
import { agentDir, apiModelToDef, isPortableInstall, loadConfig, MODELS, persistPreferredModel, PROVIDER_ID, readSettings, resolvePreferredModel, setModelsFromApi, VERSION, } from "./config.js";
|
|
6
|
+
import { agentDir, apiModelToDef, CUSTOM_ENDPOINTS, isPortableInstall, loadConfig, MODELS, persistPreferredModel, PROVIDER_ID, readSettings, refValid, resolvePreferredModel, setModelsFromApi, VERSION, } from "./config.js";
|
|
7
|
+
import { ensureSearchTools } from "./search-tools.js";
|
|
7
8
|
import { applyBrandUi, setUpdateNotice } from "./style.js";
|
|
8
9
|
import { offerStarterTemplates } from "./templates.js";
|
|
9
|
-
import { fetchModels } from "./api.js";
|
|
10
|
+
import { fetchModels, loadCustomEndpoints } from "./api.js";
|
|
10
11
|
const PACKAGE_NAME = "u1s1-cli";
|
|
11
12
|
/**
|
|
12
13
|
* 启动时自动检查 npm 最新版:autoUpdate 开着就静默安装,关着也在启动横幅的
|
|
@@ -117,9 +118,14 @@ async function runAgent(cfg, args) {
|
|
|
117
118
|
cleanupBrandThemes();
|
|
118
119
|
ensureBrandPrompt();
|
|
119
120
|
ensureDefaultSettings();
|
|
121
|
+
// 预 seed fd/rg(国内直连 GitHub 不通,pi 自己下不动);与取模型列表并行,
|
|
122
|
+
// 但必须在进 pi 之前就位,否则 pi 会自己去 GitHub 下载
|
|
123
|
+
const searchToolsReady = ensureSearchTools(cfg);
|
|
120
124
|
// Fetch model list from server; fall back to built-in MODELS on error.
|
|
121
125
|
// 服务端没开搜索(或老网关没有 features 字段)时不注册 web_search,模型就不会白调
|
|
126
|
+
// 自定义端点与模型列表并行拉取,失败各自兜底(内置列表 / 本地缓存)
|
|
122
127
|
let webSearchEnabled = true;
|
|
128
|
+
const endpointsReady = loadCustomEndpoints(cfg);
|
|
123
129
|
try {
|
|
124
130
|
const { models, features } = await fetchModels(cfg);
|
|
125
131
|
setModelsFromApi(models.map(apiModelToDef));
|
|
@@ -128,6 +134,7 @@ async function runAgent(cfg, args) {
|
|
|
128
134
|
catch (e) {
|
|
129
135
|
console.error(" 获取模型列表失败,使用内置列表:", e.message);
|
|
130
136
|
}
|
|
137
|
+
await endpointsReady;
|
|
131
138
|
ensureProviderModels(cfg);
|
|
132
139
|
// 联网工具经 agentDir/extensions 投影,TUI 和 u1s1 web 共用一份注册
|
|
133
140
|
writeWebToolsExtension(cfg, webSearchEnabled);
|
|
@@ -136,8 +143,11 @@ async function runAgent(cfg, args) {
|
|
|
136
143
|
process.env["PI_CODING_AGENT_DIR"] = agentDir;
|
|
137
144
|
process.env["U1S1_API_KEY"] = cfg.apiKey;
|
|
138
145
|
process.env["U1S1_TOOLS_VIA_EXTENSION"] = "1";
|
|
146
|
+
// 自定义端点的密钥走环境变量引用(models.json 里只有 $VAR,不落明文)
|
|
147
|
+
Object.assign(process.env, endpointKeyEnv());
|
|
139
148
|
// hide pi's own "Run pi update" banner; users should run `u1s1 update` instead
|
|
140
149
|
process.env["PI_SKIP_VERSION_CHECK"] = "1";
|
|
150
|
+
await searchToolsReady;
|
|
141
151
|
const { CustomEditor, main } = await import("@earendil-works/pi-coding-agent");
|
|
142
152
|
// 网页终端(如 Taikula)把 Shift+Enter 发成 ESC+CR(\x1b\r)。pi 在没开 Kitty
|
|
143
153
|
// 协议时把它当成 Alt+Enter:空闲就直接发送,忙碌才排队。先改成 CSI-u 的
|
|
@@ -192,25 +202,20 @@ async function runAgent(cfg, args) {
|
|
|
192
202
|
baseUrl: cfg.baseUrl,
|
|
193
203
|
api: "openai-completions",
|
|
194
204
|
apiKey: "$U1S1_API_KEY",
|
|
195
|
-
models: MODELS
|
|
196
|
-
id: m.id,
|
|
197
|
-
name: m.name,
|
|
198
|
-
reasoning: m.reasoning,
|
|
199
|
-
input: ["text"],
|
|
200
|
-
cost: m.cost,
|
|
201
|
-
contextWindow: m.contextWindow,
|
|
202
|
-
maxTokens: m.maxTokens,
|
|
203
|
-
})),
|
|
205
|
+
models: toProviderModels(MODELS),
|
|
204
206
|
});
|
|
207
|
+
// 用户在云端配置的自定义端点,一个端点一个 provider,/model 里即可切换
|
|
208
|
+
for (const ep of CUSTOM_ENDPOINTS) {
|
|
209
|
+
pi.registerProvider(ep.id, endpointProviderEntry(ep));
|
|
210
|
+
}
|
|
205
211
|
// /model and Ctrl+P already write pi settings; also keep ~/.u1s1/config.json in sync
|
|
206
212
|
pi.on("model_select", (event) => {
|
|
207
213
|
if (event.source === "restore")
|
|
208
214
|
return;
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
if (!MODELS.some((m) => m.id === event.model.id))
|
|
215
|
+
const ref = { provider: event.model.provider, id: event.model.id };
|
|
216
|
+
if (!refValid(ref))
|
|
212
217
|
return;
|
|
213
|
-
persistPreferredModel(loadConfig(),
|
|
218
|
+
persistPreferredModel(loadConfig(), ref.provider, ref.id);
|
|
214
219
|
});
|
|
215
220
|
pi.on("session_start", (_event, ctx) => {
|
|
216
221
|
const previous = ctx.ui.getEditorComponent();
|
|
@@ -228,10 +233,11 @@ async function runAgent(cfg, args) {
|
|
|
228
233
|
},
|
|
229
234
|
];
|
|
230
235
|
const hasModelArg = args.some((a) => a === "--model" || a.startsWith("--model=") || a === "--provider");
|
|
231
|
-
const
|
|
232
|
-
if (cfg.model !==
|
|
233
|
-
persistPreferredModel(cfg,
|
|
234
|
-
|
|
236
|
+
const pref = resolvePreferredModel(cfg);
|
|
237
|
+
if (cfg.model !== pref.id || (cfg.modelProvider ?? PROVIDER_ID) !== pref.provider) {
|
|
238
|
+
persistPreferredModel(cfg, pref.provider, pref.id);
|
|
239
|
+
}
|
|
240
|
+
const finalArgs = hasModelArg ? args : ["--model", `${pref.provider}/${pref.id}`, ...args];
|
|
235
241
|
await main(finalArgs, { extensionFactories: extension });
|
|
236
242
|
}
|
|
237
243
|
async function run() {
|
package/dist/model.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { loadCustomEndpoints } from "./api.js";
|
|
2
|
+
import { CUSTOM_ENDPOINTS, findModel, loadConfig, MODELS, persistPreferredModel, PROVIDER_ID, resolvePreferredModel, } from "./config.js";
|
|
2
3
|
export async function modelCommand(nameOrAlias) {
|
|
3
4
|
const cfg = loadConfig();
|
|
4
|
-
|
|
5
|
+
// 云端可能刚改过端点配置;拉一次(失败回退本地缓存),再解析当前默认
|
|
6
|
+
await loadCustomEndpoints(cfg);
|
|
7
|
+
const current = resolvePreferredModel(cfg);
|
|
5
8
|
if (!nameOrAlias) {
|
|
6
9
|
console.log("");
|
|
7
10
|
for (const m of MODELS) {
|
|
8
|
-
const mark = m.id === current ? "●" : " ";
|
|
11
|
+
const mark = current.provider === PROVIDER_ID && m.id === current.id ? "●" : " ";
|
|
9
12
|
console.log(` ${mark} ${m.aliases[0].padEnd(10)} ${m.name}`);
|
|
10
13
|
if (m.note) {
|
|
11
14
|
console.log(` ${m.note} · $${m.cost.input}/$${m.cost.output} 每百万 token`);
|
|
@@ -14,16 +17,37 @@ export async function modelCommand(nameOrAlias) {
|
|
|
14
17
|
console.log(` $${m.cost.input}/$${m.cost.output} 每百万 token`);
|
|
15
18
|
}
|
|
16
19
|
}
|
|
20
|
+
for (const ep of CUSTOM_ENDPOINTS) {
|
|
21
|
+
console.log("");
|
|
22
|
+
console.log(` ─ 自定义端点 ${ep.name}(${ep.baseUrl},走本地直连不计费)`);
|
|
23
|
+
for (const m of ep.models) {
|
|
24
|
+
const mark = current.provider === ep.id && m.id === current.id ? "●" : " ";
|
|
25
|
+
console.log(` ${mark} ${m.id.padEnd(10)} ${m.name === m.id ? "" : m.name}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
17
28
|
console.log("");
|
|
18
29
|
console.log(" 切换:u1s1 model grok / u1s1 model deepseek(对话里 /model 同样会记住)");
|
|
30
|
+
if (CUSTOM_ENDPOINTS.length) {
|
|
31
|
+
console.log(" 端点模型重名时可加限定:u1s1 model 端点名:模型id");
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
console.log(" 想接自己的 API(含本机 Ollama)?去 https://u1s1.io/dashboard 配「自定义模型端点」");
|
|
35
|
+
}
|
|
19
36
|
console.log("");
|
|
20
37
|
return;
|
|
21
38
|
}
|
|
22
|
-
const m =
|
|
39
|
+
const m = findModel(nameOrAlias);
|
|
23
40
|
if (!m) {
|
|
24
|
-
|
|
41
|
+
const custom = CUSTOM_ENDPOINTS.flatMap((e) => e.models.map((x) => x.id));
|
|
42
|
+
const all = [...MODELS.map((x) => x.aliases[0]), ...custom];
|
|
43
|
+
console.error(` 没有叫「${nameOrAlias}」的模型,可选:${all.join(" / ")}`);
|
|
25
44
|
process.exit(1);
|
|
26
45
|
}
|
|
27
|
-
persistPreferredModel(cfg, m.id);
|
|
28
|
-
|
|
46
|
+
persistPreferredModel(cfg, m.provider, m.id);
|
|
47
|
+
if (m.provider === PROVIDER_ID) {
|
|
48
|
+
console.log(` ✓ 默认模型已切到 ${m.model.name}${m.model.note ? `(${m.model.note})` : ""}`);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
console.log(` ✓ 默认模型已切到 ${m.model.name}(自定义端点 ${m.providerName},本地直连不计费)`);
|
|
52
|
+
}
|
|
29
53
|
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { chmodSync, createWriteStream, existsSync, mkdirSync, readdirSync, renameSync, rmSync } from "node:fs";
|
|
3
|
+
import { arch, platform } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { Readable } from "node:stream";
|
|
6
|
+
import { pipeline } from "node:stream/promises";
|
|
7
|
+
import { agentDir } from "./config.js";
|
|
8
|
+
/**
|
|
9
|
+
* 预 seed pi 的搜索组件(fd / ripgrep)到 ~/.u1s1/agent/bin。
|
|
10
|
+
*
|
|
11
|
+
* pi 首启发现缺 fd/rg 会从 GitHub 现下,国内直连基本不通:启动时连弹
|
|
12
|
+
* "Failed to download ... fetch failed",更糟的是 grep/find 工具运行时直接
|
|
13
|
+
* 报错,没有降级路径。这里在进 pi 之前从自家网关(/tools/<asset>,CF 边缘
|
|
14
|
+
* 代理 GitHub release,见 gateway/src/index.ts)把二进制放进 pi 的探测目录
|
|
15
|
+
* (探测顺序:agentDir/bin → PATH);探到本地文件后 pi 不再碰 GitHub。
|
|
16
|
+
* 任何失败都不阻塞启动——pi 自己的 GitHub 下载仍是兜底(有代理的用户能通)。
|
|
17
|
+
*
|
|
18
|
+
* 版本钉死而不是查 latest:免去对 GitHub API 的依赖,且 fd 10.4+ 不再发
|
|
19
|
+
* x86_64 macOS 产物,统一钉 10.3.0(与 pi 对 darwin-x64 的钉版一致)。
|
|
20
|
+
*/
|
|
21
|
+
const TOOLS = [
|
|
22
|
+
{ bin: "fd", version: "10.3.0", pathNames: ["fd", "fdfind"], asset: fdAsset },
|
|
23
|
+
{ bin: "rg", version: "15.2.0", pathNames: ["rg"], asset: rgAsset },
|
|
24
|
+
];
|
|
25
|
+
const binDir = join(agentDir, "bin");
|
|
26
|
+
const DOWNLOAD_TIMEOUT_MS = 60_000;
|
|
27
|
+
function archStr() {
|
|
28
|
+
const a = arch();
|
|
29
|
+
return a === "arm64" ? "aarch64" : a === "x64" ? "x86_64" : null;
|
|
30
|
+
}
|
|
31
|
+
function fdAsset(version) {
|
|
32
|
+
const a = archStr();
|
|
33
|
+
if (!a)
|
|
34
|
+
return null;
|
|
35
|
+
const p = platform();
|
|
36
|
+
if (p === "darwin")
|
|
37
|
+
return `fd-v${version}-${a}-apple-darwin.tar.gz`;
|
|
38
|
+
if (p === "linux")
|
|
39
|
+
return `fd-v${version}-${a}-unknown-linux-gnu.tar.gz`;
|
|
40
|
+
if (p === "win32")
|
|
41
|
+
return `fd-v${version}-${a}-pc-windows-msvc.zip`;
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
function rgAsset(version) {
|
|
45
|
+
const a = archStr();
|
|
46
|
+
if (!a)
|
|
47
|
+
return null;
|
|
48
|
+
const p = platform();
|
|
49
|
+
if (p === "darwin")
|
|
50
|
+
return `ripgrep-${version}-${a}-apple-darwin.tar.gz`;
|
|
51
|
+
// linux 上 arm64 用 gnu、x64 用 musl:与 pi 的资产选择保持一致
|
|
52
|
+
if (p === "linux") {
|
|
53
|
+
return a === "aarch64"
|
|
54
|
+
? `ripgrep-${version}-aarch64-unknown-linux-gnu.tar.gz`
|
|
55
|
+
: `ripgrep-${version}-x86_64-unknown-linux-musl.tar.gz`;
|
|
56
|
+
}
|
|
57
|
+
if (p === "win32")
|
|
58
|
+
return `ripgrep-${version}-${a}-pc-windows-msvc.zip`;
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
/** 与 pi 的 commandExists 同语义:能 spawn 就算在 PATH 里(fd 也认 Debian 名 fdfind)。 */
|
|
62
|
+
function inPath(names) {
|
|
63
|
+
return names.some((name) => {
|
|
64
|
+
const r = spawnSync(name, ["--version"], { stdio: "pipe" });
|
|
65
|
+
return r.error === undefined || r.error === null;
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
function run(command, cmdArgs) {
|
|
69
|
+
const r = spawnSync(command, cmdArgs, { stdio: "pipe" });
|
|
70
|
+
return !r.error && r.status === 0;
|
|
71
|
+
}
|
|
72
|
+
function extract(archive, dir) {
|
|
73
|
+
if (archive.endsWith(".zip")) {
|
|
74
|
+
// 只有 win32 会拿到 zip。System32 的 tar 是 bsdtar,认 zip;Git Bash 的
|
|
75
|
+
// GNU tar 不认——优先绝对路径,失败再退 powershell Expand-Archive。
|
|
76
|
+
const sysRoot = process.env["SystemRoot"] ?? process.env["WINDIR"];
|
|
77
|
+
const sysTar = sysRoot ? join(sysRoot, "System32", "tar.exe") : "tar.exe";
|
|
78
|
+
if (run(existsSync(sysTar) ? sysTar : "tar.exe", ["xf", archive, "-C", dir]))
|
|
79
|
+
return true;
|
|
80
|
+
return run("powershell.exe", [
|
|
81
|
+
"-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command",
|
|
82
|
+
"& { param($a,$d) $ErrorActionPreference='Stop'; Expand-Archive -LiteralPath $a -DestinationPath $d -Force }",
|
|
83
|
+
archive, dir,
|
|
84
|
+
]);
|
|
85
|
+
}
|
|
86
|
+
return run("tar", ["xzf", archive, "-C", dir]);
|
|
87
|
+
}
|
|
88
|
+
/** 资产里的二进制可能嵌在版本号目录下(fd 是,rg 也是),递归找。 */
|
|
89
|
+
function findBinary(root, name) {
|
|
90
|
+
const stack = [root];
|
|
91
|
+
while (stack.length > 0) {
|
|
92
|
+
const dir = stack.pop();
|
|
93
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
94
|
+
const full = join(dir, entry.name);
|
|
95
|
+
if (entry.isFile() && entry.name === name)
|
|
96
|
+
return full;
|
|
97
|
+
if (entry.isDirectory())
|
|
98
|
+
stack.push(full);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
async function install(tool, baseUrl, binName) {
|
|
104
|
+
const asset = tool.asset(tool.version);
|
|
105
|
+
if (!asset)
|
|
106
|
+
throw new Error(`不支持的平台 ${platform()}/${arch()}`);
|
|
107
|
+
mkdirSync(binDir, { recursive: true });
|
|
108
|
+
const res = await fetch(new URL(`/tools/${asset}`, baseUrl), {
|
|
109
|
+
signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS),
|
|
110
|
+
});
|
|
111
|
+
if (!res.ok || !res.body)
|
|
112
|
+
throw new Error(`HTTP ${res.status}`);
|
|
113
|
+
const archive = join(binDir, asset);
|
|
114
|
+
// fd/rg 并行安装,解压目录按工具名+pid 隔离,避免互踩
|
|
115
|
+
const extractDir = join(binDir, `preseed_${tool.bin}_${process.pid}`);
|
|
116
|
+
try {
|
|
117
|
+
await pipeline(Readable.fromWeb(res.body), createWriteStream(archive));
|
|
118
|
+
mkdirSync(extractDir, { recursive: true });
|
|
119
|
+
if (!extract(archive, extractDir))
|
|
120
|
+
throw new Error(`解压失败 ${asset}`);
|
|
121
|
+
const found = findBinary(extractDir, binName);
|
|
122
|
+
if (!found)
|
|
123
|
+
throw new Error(`包内没有 ${binName}`);
|
|
124
|
+
renameSync(found, join(binDir, binName));
|
|
125
|
+
if (platform() !== "win32")
|
|
126
|
+
chmodSync(join(binDir, binName), 0o755);
|
|
127
|
+
}
|
|
128
|
+
finally {
|
|
129
|
+
rmSync(archive, { force: true });
|
|
130
|
+
rmSync(extractDir, { recursive: true, force: true });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/** 缺哪个装哪个;都在(本地目录或 PATH)则秒退。失败只提示,不阻塞启动。 */
|
|
134
|
+
export async function ensureSearchTools(cfg) {
|
|
135
|
+
try {
|
|
136
|
+
const ext = platform() === "win32" ? ".exe" : "";
|
|
137
|
+
const missing = TOOLS.filter((t) => !existsSync(join(binDir, t.bin + ext)) && !inPath(t.pathNames));
|
|
138
|
+
if (missing.length === 0)
|
|
139
|
+
return;
|
|
140
|
+
console.log(` 正在安装搜索组件(${missing.map((t) => t.bin).join("/")})…`);
|
|
141
|
+
const results = await Promise.allSettled(missing.map((t) => install(t, cfg.baseUrl, t.bin + ext)));
|
|
142
|
+
for (let i = 0; i < results.length; i++) {
|
|
143
|
+
const r = results[i];
|
|
144
|
+
if (r.status === "rejected") {
|
|
145
|
+
const msg = r.reason instanceof Error ? r.reason.message : String(r.reason);
|
|
146
|
+
console.error(` ${missing[i].bin} 安装失败(${msg}),稍后将尝试 GitHub 直连`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
// 预 seed 只是加速,任何意外都不该挡住启动
|
|
152
|
+
}
|
|
153
|
+
}
|
package/dist/web.js
CHANGED
|
@@ -2,10 +2,11 @@ import { spawn, spawnSync } from "node:child_process";
|
|
|
2
2
|
import { mkdirSync } from "node:fs";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
|
-
import { cleanupBrandThemes, ensureAuthCredential, ensureBrandPrompt, ensureDefaultSettings, ensureProviderModels, writeWebToolsExtension, } from "./agent-setup.js";
|
|
5
|
+
import { cleanupBrandThemes, endpointKeyEnv, ensureAuthCredential, ensureBrandPrompt, ensureDefaultSettings, ensureProviderModels, writeWebToolsExtension, } from "./agent-setup.js";
|
|
6
6
|
import { agentDir, apiModelToDef, resolvePreferredModel, setModelsFromApi, u1s1Dir, writeAgentDefaultModel, } from "./config.js";
|
|
7
|
+
import { ensureSearchTools } from "./search-tools.js";
|
|
7
8
|
import { applyWebUiBranding, applyWebUiFrontend } from "./webui-brand.js";
|
|
8
|
-
import { fetchModels } from "./api.js";
|
|
9
|
+
import { fetchModels, loadCustomEndpoints } from "./api.js";
|
|
9
10
|
const require = createRequire(import.meta.url);
|
|
10
11
|
/**
|
|
11
12
|
* `u1s1 web` — 浏览器网页版。薄包装 pi-web-ui 的服务器:注入我们的 agentDir
|
|
@@ -21,9 +22,13 @@ export async function prepareWebEnv(cfg) {
|
|
|
21
22
|
cleanupBrandThemes();
|
|
22
23
|
ensureBrandPrompt();
|
|
23
24
|
ensureDefaultSettings();
|
|
25
|
+
// 网页版/App 的 agent 会话同样依赖 fd/rg(同一个 agentDir);与取模型列表并行
|
|
26
|
+
const searchToolsReady = ensureSearchTools(cfg);
|
|
24
27
|
// Fetch model list from server; fall back to built-in MODELS on error.
|
|
25
28
|
// 服务端没开搜索(或老网关没有 features 字段)时不注册 web_search
|
|
29
|
+
// 自定义端点与模型列表并行拉取,失败各自兜底(内置列表 / 本地缓存)
|
|
26
30
|
let webSearchEnabled = true;
|
|
31
|
+
const endpointsReady = loadCustomEndpoints(cfg);
|
|
27
32
|
try {
|
|
28
33
|
const { models, features } = await fetchModels(cfg);
|
|
29
34
|
setModelsFromApi(models.map(apiModelToDef));
|
|
@@ -32,6 +37,7 @@ export async function prepareWebEnv(cfg) {
|
|
|
32
37
|
catch (e) {
|
|
33
38
|
console.error(" 获取模型列表失败,使用内置列表:", e.message);
|
|
34
39
|
}
|
|
40
|
+
await endpointsReady;
|
|
35
41
|
ensureProviderModels(cfg);
|
|
36
42
|
// pi-web-ui 靠 auth.json 判断「已配置」,否则网页会弹 pi 安装引导
|
|
37
43
|
ensureAuthCredential();
|
|
@@ -39,14 +45,18 @@ export async function prepareWebEnv(cfg) {
|
|
|
39
45
|
writeWebToolsExtension(cfg, webSearchEnabled);
|
|
40
46
|
// 网页版新会话从 settings.json 的 defaultModel 取模型(TUI 是每次传 --model),
|
|
41
47
|
// 确保它有值;resolvePreferredModel 优先尊重已有的 in-session 选择,不会回退覆盖。
|
|
42
|
-
|
|
48
|
+
const pref = resolvePreferredModel(cfg);
|
|
49
|
+
writeAgentDefaultModel(pref.provider, pref.id);
|
|
43
50
|
const dataDir = join(u1s1Dir, "web");
|
|
44
51
|
mkdirSync(dataDir, { recursive: true });
|
|
52
|
+
await searchToolsReady;
|
|
45
53
|
return {
|
|
46
54
|
PI_CODING_AGENT_DIR: agentDir,
|
|
47
55
|
U1S1_API_KEY: cfg.apiKey,
|
|
48
56
|
U1S1_TOOLS_VIA_EXTENSION: "1",
|
|
49
57
|
PI_WEB_DATA_DIR: dataDir,
|
|
58
|
+
// 自定义端点的密钥经环境变量传给 web 子进程(models.json 里只有 $VAR 引用)
|
|
59
|
+
...endpointKeyEnv(),
|
|
50
60
|
};
|
|
51
61
|
}
|
|
52
62
|
export async function webCommand(cfg, args) {
|