u1s1-cli 0.9.3 → 0.11.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 +32 -0
- package/dist/agent-setup.js +27 -0
- package/dist/api.d.ts +51 -0
- package/dist/brand.d.ts +13 -0
- package/dist/config.d.ts +77 -0
- package/dist/deploy.d.ts +2 -0
- package/dist/deploy.js +235 -0
- package/dist/embed.d.ts +11 -0
- package/dist/embed.js +11 -0
- package/dist/import/claude.d.ts +3 -0
- package/dist/import/codex.d.ts +3 -0
- package/dist/import/index.d.ts +1 -0
- package/dist/import/types.d.ts +104 -0
- package/dist/import/util.d.ts +26 -0
- package/dist/import/write.d.ts +4 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +10 -1
- package/dist/login.d.ts +16 -0
- package/dist/login.js +3 -3
- package/dist/model.d.ts +1 -0
- package/dist/shortcut.d.ts +1 -0
- package/dist/style.d.ts +7 -0
- package/dist/templates.d.ts +2 -0
- package/dist/templates.js +48 -0
- package/dist/tools.d.ts +18 -0
- package/dist/update.d.ts +8 -0
- package/dist/usage.d.ts +1 -0
- package/dist/web.d.ts +13 -0
- package/dist/web.js +45 -32
- package/dist/webui-brand.d.ts +9 -0
- package/dist/webui-brand.js +155 -10
- package/package.json +11 -3
- package/webui-dist/assets/index-Cz5vuDzv.js +128 -0
- package/webui-dist/assets/index-DICLz-bf.css +41 -0
- package/webui-dist/favicon.svg +4 -0
- package/webui-dist/icon.ico +0 -0
- package/webui-dist/index.html +15 -0
package/dist/login.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type CliConfig } from "./config.js";
|
|
2
|
+
/** baseUrl 形如 https://api.u1s1.io/v1;auth 路由挂在同一域名的根路径。 */
|
|
3
|
+
export declare function apiOrigin(cfg: CliConfig): string;
|
|
4
|
+
export interface DeviceStart {
|
|
5
|
+
verify_url: string;
|
|
6
|
+
poll_secret: string;
|
|
7
|
+
interval: number;
|
|
8
|
+
expires_in: number;
|
|
9
|
+
}
|
|
10
|
+
/** 发起浏览器登录;网关太老或连不上时返回 null,退回手动粘贴。 */
|
|
11
|
+
export declare function startDeviceLogin(origin: string): Promise<DeviceStart | null>;
|
|
12
|
+
/** 轮询等浏览器那边批准;拿到 key 返回,过期返回 null。 */
|
|
13
|
+
export declare function pollDeviceLogin(origin: string, start: DeviceStart): Promise<string | null>;
|
|
14
|
+
export declare function login(keyArg?: string): Promise<CliConfig>;
|
|
15
|
+
/** Returns a config that definitely has an apiKey, prompting the user if needed. */
|
|
16
|
+
export declare function ensureAuth(): Promise<CliConfig>;
|
package/dist/login.js
CHANGED
|
@@ -18,11 +18,11 @@ function tryOpenBrowser(url) {
|
|
|
18
18
|
}
|
|
19
19
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
20
20
|
/** baseUrl 形如 https://api.u1s1.io/v1;auth 路由挂在同一域名的根路径。 */
|
|
21
|
-
function apiOrigin(cfg) {
|
|
21
|
+
export function apiOrigin(cfg) {
|
|
22
22
|
return cfg.baseUrl.replace(/\/v1\/?$/, "");
|
|
23
23
|
}
|
|
24
24
|
/** 发起浏览器登录;网关太老或连不上时返回 null,退回手动粘贴。 */
|
|
25
|
-
async function startDeviceLogin(origin) {
|
|
25
|
+
export async function startDeviceLogin(origin) {
|
|
26
26
|
try {
|
|
27
27
|
const resp = await fetch(`${origin}/auth/device/start`, { method: "POST" });
|
|
28
28
|
if (!resp.ok)
|
|
@@ -42,7 +42,7 @@ async function startDeviceLogin(origin) {
|
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
/** 轮询等浏览器那边批准;拿到 key 返回,过期返回 null。 */
|
|
45
|
-
async function pollDeviceLogin(origin, start) {
|
|
45
|
+
export async function pollDeviceLogin(origin, start) {
|
|
46
46
|
const deadline = Date.now() + start.expires_in * 1000;
|
|
47
47
|
while (Date.now() < deadline) {
|
|
48
48
|
await sleep(start.interval * 1000);
|
package/dist/model.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function modelCommand(nameOrAlias?: string): Promise<void>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function createWebShortcut(): void;
|
package/dist/style.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
export declare function setUpdateNotice(notice: string): void;
|
|
3
|
+
/**
|
|
4
|
+
* Brand chrome is just the startup hero + window title; everything else
|
|
5
|
+
* (tool rows, thinking blocks, spinner) stays Pi's default UI.
|
|
6
|
+
*/
|
|
7
|
+
export declare function applyBrandUi(pi: ExtensionAPI, version: string): void;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { readdirSync } from "node:fs";
|
|
2
|
+
/**
|
|
3
|
+
* 空目录首跑模板(roadmap-next #3):小白第一次在空文件夹里启动 u1s1,
|
|
4
|
+
* 面对空白输入框往往想不出第一句说什么。弹一个选择器给几个现成的小项目,
|
|
5
|
+
* 选中后把完整提示词放进输入框——不直接替用户发送,让他看到「原来就是
|
|
6
|
+
* 说这样一句话」,想改就改,回车才开跑。选「我自己说」或按 Esc 都不打扰。
|
|
7
|
+
*/
|
|
8
|
+
const TEMPLATES = [
|
|
9
|
+
{
|
|
10
|
+
label: "🏠 个人主页——介绍我自己的网页",
|
|
11
|
+
prompt: "帮我做一个个人主页网页,介绍我自己,放上我的爱好和联系方式,风格简洁好看,手机和电脑打开都正常。做完教我怎么在浏览器里预览。",
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
label: "📅 倒数日——距离某天还有多少天",
|
|
15
|
+
prompt: "做一个倒数日网页,可以输入日期和事件名,大字显示距离那天还有多少天,设计得可爱一点。做完教我怎么在浏览器里预览。",
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
label: "🖼️ 电子相册——可以翻页的照片墙",
|
|
19
|
+
prompt: "帮我做一个电子相册网页,照片可以左右翻页,先放几张占位图,并告诉我之后怎么换成自己的照片。做完教我怎么在浏览器里预览。",
|
|
20
|
+
},
|
|
21
|
+
];
|
|
22
|
+
const SELF_DESCRIBE = "✍️ 不用了,我自己说";
|
|
23
|
+
/** 只看可见条目:.git / .DS_Store 这类隐藏文件不算「有东西」。读不了就当非空,不打扰。 */
|
|
24
|
+
function isEmptyDir(dir) {
|
|
25
|
+
try {
|
|
26
|
+
return readdirSync(dir).filter((name) => !name.startsWith(".")).length === 0;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
let offered = false;
|
|
33
|
+
export function offerStarterTemplates(pi) {
|
|
34
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
35
|
+
// /clear 等新会话也会触发 session_start,每次启动只弹一次
|
|
36
|
+
if (ctx.mode !== "tui" || offered)
|
|
37
|
+
return;
|
|
38
|
+
if (!isEmptyDir(process.cwd()))
|
|
39
|
+
return;
|
|
40
|
+
offered = true;
|
|
41
|
+
const choice = await ctx.ui.select("这个文件夹还是空的,挑一个现成的小项目直接开跑?", [...TEMPLATES.map((t) => t.label), SELF_DESCRIBE]);
|
|
42
|
+
const t = TEMPLATES.find((x) => x.label === choice);
|
|
43
|
+
if (!t)
|
|
44
|
+
return;
|
|
45
|
+
ctx.ui.setEditorText(t.prompt);
|
|
46
|
+
ctx.ui.notify("提示词放进输入框了:想改哪句直接改,按回车开跑", "info");
|
|
47
|
+
});
|
|
48
|
+
}
|
package/dist/tools.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import type { CliConfig } from "./config.js";
|
|
3
|
+
/** 联网搜索工具:走 u1s1 网关代理,上游 key 不落到用户机器上。 */
|
|
4
|
+
export declare function createSearchTool(cfg: Pick<CliConfig, "baseUrl" | "apiKey">): import("@earendil-works/pi-coding-agent").ToolDefinition<Type.TObject<{
|
|
5
|
+
query: Type.TString;
|
|
6
|
+
maxResults: Type.TOptional<Type.TNumber>;
|
|
7
|
+
}>, {
|
|
8
|
+
query: string;
|
|
9
|
+
count: number;
|
|
10
|
+
}, any> & import("@earendil-works/pi-coding-agent").ToolDefinition<any, any, any>;
|
|
11
|
+
/** 抓网页:纯客户端出网,不经过我们的服务器,也不额外计费。 */
|
|
12
|
+
export declare const webFetchTool: import("@earendil-works/pi-coding-agent").ToolDefinition<Type.TObject<{
|
|
13
|
+
url: Type.TString;
|
|
14
|
+
}>, {
|
|
15
|
+
url: string;
|
|
16
|
+
contentType: string;
|
|
17
|
+
chars: number;
|
|
18
|
+
}, any> & import("@earendil-works/pi-coding-agent").ToolDefinition<any, any, any>;
|
package/dist/update.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare const VERSION: string;
|
|
2
|
+
export declare const PACKAGE_NAME = "u1s1-cli";
|
|
3
|
+
/** Detect the package manager that installed u1s1. */
|
|
4
|
+
export declare function detectPackageManager(): string;
|
|
5
|
+
/** Fetch the latest published version from npm registry. */
|
|
6
|
+
export declare function getLatestVersion(): Promise<string | undefined>;
|
|
7
|
+
export declare function compareVersions(a: string, b: string): number;
|
|
8
|
+
export declare function update(): Promise<void>;
|
package/dist/usage.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function usage(): Promise<void>;
|
package/dist/web.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type CliConfig } from "./config.js";
|
|
2
|
+
/**
|
|
3
|
+
* `u1s1 web` — 浏览器网页版。薄包装 pi-web-ui 的服务器:注入我们的 agentDir
|
|
4
|
+
* (品牌 prompt / models.json / 会话与 TUI 共享)和登录 key,其余原样透传
|
|
5
|
+
* (--port / --cwd / --no-browser)。
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* 启动 pi-web-ui 服务器前的公共准备:品牌 prompt、模型列表、auth.json 凭据、
|
|
9
|
+
* 联网工具扩展、默认模型,并返回子进程需要的环境变量。CLI(`u1s1 web`)和
|
|
10
|
+
* 桌面版 App(packages/app,经 "u1s1-cli/embed" 引入)共用这一份逻辑。
|
|
11
|
+
*/
|
|
12
|
+
export declare function prepareWebEnv(cfg: CliConfig): Promise<Record<string, string>>;
|
|
13
|
+
export declare function webCommand(cfg: CliConfig, args: string[]): Promise<void>;
|
package/dist/web.js
CHANGED
|
@@ -2,9 +2,9 @@ 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, ensureBrandPrompt, ensureDefaultSettings, ensureProviderModels, writeWebToolsExtension, } from "./agent-setup.js";
|
|
5
|
+
import { cleanupBrandThemes, ensureAuthCredential, ensureBrandPrompt, ensureDefaultSettings, ensureProviderModels, writeWebToolsExtension, } from "./agent-setup.js";
|
|
6
6
|
import { agentDir, apiModelToDef, resolvePreferredModel, setModelsFromApi, u1s1Dir, writeAgentDefaultModel, } from "./config.js";
|
|
7
|
-
import { applyWebUiBranding } from "./webui-brand.js";
|
|
7
|
+
import { applyWebUiBranding, applyWebUiFrontend } from "./webui-brand.js";
|
|
8
8
|
import { fetchModels } from "./api.js";
|
|
9
9
|
const require = createRequire(import.meta.url);
|
|
10
10
|
/**
|
|
@@ -12,6 +12,43 @@ const require = createRequire(import.meta.url);
|
|
|
12
12
|
* (品牌 prompt / models.json / 会话与 TUI 共享)和登录 key,其余原样透传
|
|
13
13
|
* (--port / --cwd / --no-browser)。
|
|
14
14
|
*/
|
|
15
|
+
/**
|
|
16
|
+
* 启动 pi-web-ui 服务器前的公共准备:品牌 prompt、模型列表、auth.json 凭据、
|
|
17
|
+
* 联网工具扩展、默认模型,并返回子进程需要的环境变量。CLI(`u1s1 web`)和
|
|
18
|
+
* 桌面版 App(packages/app,经 "u1s1-cli/embed" 引入)共用这一份逻辑。
|
|
19
|
+
*/
|
|
20
|
+
export async function prepareWebEnv(cfg) {
|
|
21
|
+
cleanupBrandThemes();
|
|
22
|
+
ensureBrandPrompt();
|
|
23
|
+
ensureDefaultSettings();
|
|
24
|
+
// Fetch model list from server; fall back to built-in MODELS on error.
|
|
25
|
+
// 服务端没开搜索(或老网关没有 features 字段)时不注册 web_search
|
|
26
|
+
let webSearchEnabled = true;
|
|
27
|
+
try {
|
|
28
|
+
const { models, features } = await fetchModels(cfg);
|
|
29
|
+
setModelsFromApi(models.map(apiModelToDef));
|
|
30
|
+
webSearchEnabled = features.web_search !== false;
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
console.error(" 获取模型列表失败,使用内置列表:", e.message);
|
|
34
|
+
}
|
|
35
|
+
ensureProviderModels(cfg);
|
|
36
|
+
// pi-web-ui 靠 auth.json 判断「已配置」,否则网页会弹 pi 安装引导
|
|
37
|
+
ensureAuthCredential();
|
|
38
|
+
// 联网工具经 agentDir/extensions 投影,和 TUI 共用一份注册
|
|
39
|
+
writeWebToolsExtension(cfg, webSearchEnabled);
|
|
40
|
+
// 网页版新会话从 settings.json 的 defaultModel 取模型(TUI 是每次传 --model),
|
|
41
|
+
// 确保它有值;resolvePreferredModel 优先尊重已有的 in-session 选择,不会回退覆盖。
|
|
42
|
+
writeAgentDefaultModel(resolvePreferredModel(cfg.model));
|
|
43
|
+
const dataDir = join(u1s1Dir, "web");
|
|
44
|
+
mkdirSync(dataDir, { recursive: true });
|
|
45
|
+
return {
|
|
46
|
+
PI_CODING_AGENT_DIR: agentDir,
|
|
47
|
+
U1S1_API_KEY: cfg.apiKey,
|
|
48
|
+
U1S1_TOOLS_VIA_EXTENSION: "1",
|
|
49
|
+
PI_WEB_DATA_DIR: dataDir,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
15
52
|
export async function webCommand(cfg, args) {
|
|
16
53
|
// pi-web-ui 的 server install/shortcut 生成的服务定义只固化 PORT/CWD,不带
|
|
17
54
|
// PI_CODING_AGENT_DIR 和 U1S1_API_KEY —— 装出来的自启服务是坏的(丢登录态、
|
|
@@ -24,6 +61,7 @@ export async function webCommand(cfg, args) {
|
|
|
24
61
|
}
|
|
25
62
|
// pi-web-ui 是 optionalDependency:装 CLI 时它编译/下载失败不会拖垮整个安装,
|
|
26
63
|
// 代价是这里可能不存在 —— 给出人话指引而不是 Cannot find module。
|
|
64
|
+
// (桌面版 App 上线后这里的兜底会改为指向 App 下载,CLI 届时不再携带 pi-web-ui。)
|
|
27
65
|
let bin;
|
|
28
66
|
try {
|
|
29
67
|
bin = require.resolve("pi-web-ui/bin/pi-web-ui.mjs");
|
|
@@ -45,39 +83,14 @@ export async function webCommand(cfg, args) {
|
|
|
45
83
|
process.exitCode = 1;
|
|
46
84
|
return;
|
|
47
85
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
// 服务端没开搜索(或老网关没有 features 字段)时不注册 web_search
|
|
53
|
-
let webSearchEnabled = true;
|
|
54
|
-
try {
|
|
55
|
-
const { models, features } = await fetchModels(cfg);
|
|
56
|
-
setModelsFromApi(models.map(apiModelToDef));
|
|
57
|
-
webSearchEnabled = features.web_search !== false;
|
|
58
|
-
}
|
|
59
|
-
catch (e) {
|
|
60
|
-
console.error(" 获取模型列表失败,使用内置列表:", e.message);
|
|
61
|
-
}
|
|
62
|
-
ensureProviderModels(cfg);
|
|
63
|
-
// 联网工具经 agentDir/extensions 投影,和 TUI 共用一份注册
|
|
64
|
-
writeWebToolsExtension(cfg, webSearchEnabled);
|
|
65
|
-
// 网页版新会话从 settings.json 的 defaultModel 取模型(TUI 是每次传 --model),
|
|
66
|
-
// 确保它有值;resolvePreferredModel 优先尊重已有的 in-session 选择,不会回退覆盖。
|
|
67
|
-
writeAgentDefaultModel(resolvePreferredModel(cfg.model));
|
|
68
|
-
const dataDir = join(u1s1Dir, "web");
|
|
69
|
-
mkdirSync(dataDir, { recursive: true });
|
|
70
|
-
applyWebUiBranding(bin);
|
|
86
|
+
const webEnv = await prepareWebEnv(cfg);
|
|
87
|
+
// 首选 u1s1 自有前端(Codex 布局);产物缺失时回退为上游界面 + 品牌补丁
|
|
88
|
+
if (!applyWebUiFrontend(bin))
|
|
89
|
+
applyWebUiBranding(bin);
|
|
71
90
|
console.log("u1s1 网页版启动中… 浏览器会自动打开;关闭请回到这里按 Ctrl+C。");
|
|
72
91
|
const child = spawn(process.execPath, [bin, ...args], {
|
|
73
92
|
stdio: "inherit",
|
|
74
|
-
env: {
|
|
75
|
-
...process.env,
|
|
76
|
-
PI_CODING_AGENT_DIR: agentDir,
|
|
77
|
-
U1S1_API_KEY: cfg.apiKey,
|
|
78
|
-
U1S1_TOOLS_VIA_EXTENSION: "1",
|
|
79
|
-
PI_WEB_DATA_DIR: dataDir,
|
|
80
|
-
},
|
|
93
|
+
env: { ...process.env, ...webEnv },
|
|
81
94
|
});
|
|
82
95
|
// Ctrl+C 由子进程负责优雅停机(同进程组,信号会直达);父进程只等退出码,
|
|
83
96
|
// 避免父进程先死导致孤儿服务器。
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* u1s1 自有前端(packages/webui,Codex 布局复刻)整体替换 pi-web-ui 的
|
|
3
|
+
* web/dist。产物随 CLI 包发布在 <cliPkg>/webui-dist;index.html 内容一致
|
|
4
|
+
* 说明已同步过,跳过。目标目录先整体删除再拷贝 —— 断开 pnpm 硬链接,
|
|
5
|
+
* 也顺带清掉上游的旧 assets。返回 false 表示产物缺失(回退到品牌补丁)。
|
|
6
|
+
*/
|
|
7
|
+
export declare function applyWebUiFrontend(binPath: string): boolean;
|
|
8
|
+
/** binPath = <pkg>/bin/pi-web-ui.mjs → 前端产物在 <pkg>/web/dist。 */
|
|
9
|
+
export declare function applyWebUiBranding(binPath: string): void;
|
package/dist/webui-brand.js
CHANGED
|
@@ -1,15 +1,45 @@
|
|
|
1
|
-
import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { cpSync, existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
3
4
|
/**
|
|
4
|
-
* pi-web-ui
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* u1s1 自有前端(packages/webui,Codex 布局复刻)整体替换 pi-web-ui 的
|
|
6
|
+
* web/dist。产物随 CLI 包发布在 <cliPkg>/webui-dist;index.html 内容一致
|
|
7
|
+
* 说明已同步过,跳过。目标目录先整体删除再拷贝 —— 断开 pnpm 硬链接,
|
|
8
|
+
* 也顺带清掉上游的旧 assets。返回 false 表示产物缺失(回退到品牌补丁)。
|
|
9
|
+
*/
|
|
10
|
+
export function applyWebUiFrontend(binPath) {
|
|
11
|
+
const src = fileURLToPath(new URL("../webui-dist/", import.meta.url));
|
|
12
|
+
const srcIndex = join(src, "index.html");
|
|
13
|
+
if (!existsSync(srcIndex))
|
|
14
|
+
return false;
|
|
15
|
+
const dist = join(dirname(binPath), "..", "web", "dist");
|
|
16
|
+
const distIndex = join(dist, "index.html");
|
|
17
|
+
try {
|
|
18
|
+
if (existsSync(distIndex) &&
|
|
19
|
+
readFileSync(distIndex, "utf8") === readFileSync(srcIndex, "utf8")) {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
rmSync(dist, { recursive: true, force: true });
|
|
23
|
+
cpSync(src, dist, { recursive: true });
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
catch (e) {
|
|
27
|
+
console.error(" 前端替换失败,退回默认界面:", e.message);
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* pi-web-ui 前端产物的启动时品牌补丁。不 fork 上游,只在每次 `u1s1 web` /
|
|
33
|
+
* 桌面版启动前改它 web/dist 里的静态文件:重写 <title>、换 favicon、注入
|
|
34
|
+
* 品牌脚本和一份 Codex 风格的主题 CSS(u1s1-theme.css),把整个界面换成
|
|
35
|
+
* u1s1 的视觉:石墨底色、品牌蓝 #4ea1ff、幽灵化顶栏、居中内容列。
|
|
7
36
|
*
|
|
8
|
-
* -
|
|
37
|
+
* - 幂等 + 可升级:index.html 里有当前版本标记则跳过;版本变了先剥掉旧注入
|
|
38
|
+
* 再打新的;pi-web-ui 升级后文件是全新的,自动重打。
|
|
9
39
|
* - 写文件前先 rmSync 断开硬链接 —— pnpm 的 node_modules 是全局 store 的硬链接,
|
|
10
40
|
* 原地写会污染 store;unlink 后新写的是独立 inode,npm 平铺安装则无所谓。
|
|
11
41
|
*/
|
|
12
|
-
const PATCH_MARK = "u1s1-brand-
|
|
42
|
+
const PATCH_MARK = "u1s1-brand-v2";
|
|
13
43
|
const PAGE_TITLE = "u1s1 网页版 — 有一说一";
|
|
14
44
|
const FAVICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
|
15
45
|
<rect width="64" height="64" rx="14" fill="#101418"/>
|
|
@@ -25,11 +55,116 @@ const BRAND_JS = `(function(){
|
|
|
25
55
|
if (logo && logo.textContent !== LOGO) logo.textContent = LOGO;
|
|
26
56
|
var name = document.querySelector(".brand-name");
|
|
27
57
|
if (name && name.textContent !== NAME) name.textContent = NAME;
|
|
58
|
+
// 输入框占位符里的 pi 字样换成 u1s1(React 重渲染会改回去,观察到就再改)
|
|
59
|
+
var box = document.querySelector(".inputbox textarea");
|
|
60
|
+
if (box && box.placeholder && box.placeholder.indexOf("pi ") !== -1) {
|
|
61
|
+
box.placeholder = box.placeholder.replace(/pi /g, "u1s1 ");
|
|
62
|
+
}
|
|
63
|
+
// 消息里的助手角色标签 pi → u1s1(大写 PI 是 CSS text-transform 渲染的)
|
|
64
|
+
var roles = document.querySelectorAll(".msg-role, .msg-collapsed-role");
|
|
65
|
+
for (var i = 0; i < roles.length; i++) {
|
|
66
|
+
if (roles[i].textContent.trim().toLowerCase() === "pi") roles[i].textContent = NAME;
|
|
67
|
+
}
|
|
28
68
|
}
|
|
29
69
|
apply();
|
|
30
|
-
new MutationObserver(apply).observe(document.documentElement, { childList: true, subtree: true });
|
|
70
|
+
new MutationObserver(apply).observe(document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ["placeholder"] });
|
|
31
71
|
})();
|
|
32
72
|
`;
|
|
73
|
+
/**
|
|
74
|
+
* Codex mac app 风格的亮色主题覆盖层:暖白底、深灰字、浅灰气泡、极简边框,
|
|
75
|
+
* 视觉锚点只留品牌、模型和「新对话」。全部通过变量覆盖 + 定向规则实现,
|
|
76
|
+
* 上游 DOM/类名不动;上游写死的暗色(代码块 #0b0d12!important、hljs 深色
|
|
77
|
+
* token、黑色重阴影)逐条压回亮色。终端面板(xterm)保持深色不动。
|
|
78
|
+
*/
|
|
79
|
+
const THEME_CSS = `/* ${PATCH_MARK} — u1s1 light theme overlay (Codex mac style) */
|
|
80
|
+
:root{
|
|
81
|
+
--bg:#faf9f7;
|
|
82
|
+
--bg-elev:#f2f0ec;
|
|
83
|
+
--bg-elev1:#f4f2ef;
|
|
84
|
+
--bg-elev2:#ecebe6;
|
|
85
|
+
--border:#dcd8d0;
|
|
86
|
+
--border-soft:#e8e5df;
|
|
87
|
+
--text:#1f1e1b;
|
|
88
|
+
--text-dim:#57534c;
|
|
89
|
+
--text-faint:#8a857c;
|
|
90
|
+
--accent:#2f6fe0;
|
|
91
|
+
--accent-soft:rgba(47,111,224,.10);
|
|
92
|
+
--green:#1a7f37;
|
|
93
|
+
--green-soft:rgba(26,127,55,.12);
|
|
94
|
+
--red:#cf222e;
|
|
95
|
+
--red-soft:rgba(207,34,46,.10);
|
|
96
|
+
--amber:#9a6700;
|
|
97
|
+
}
|
|
98
|
+
html,body{background:var(--bg);color-scheme:light}
|
|
99
|
+
|
|
100
|
+
/* ---- 顶栏:去底色去边框,低频控件幽灵化,悬停才现形 ---- */
|
|
101
|
+
.topbar{background:transparent;border-bottom:1px solid var(--border-soft);padding:10px 18px}
|
|
102
|
+
.topbar-desktop .dropdown .chip{background:transparent;border-color:transparent;color:var(--text-dim)}
|
|
103
|
+
.topbar-desktop .dropdown .chip:hover{background:var(--bg-elev2);border-color:var(--border);color:var(--text)}
|
|
104
|
+
/* 模型选择是高频锚点,保留药丸形态 */
|
|
105
|
+
.topbar-desktop .dropdown:first-child .chip{background:#fff;border-color:var(--border);color:var(--text)}
|
|
106
|
+
.chip{background:#fff}
|
|
107
|
+
.chip.github{display:none}
|
|
108
|
+
.chip.newchat{background:var(--accent);border-color:transparent;color:#fff;font-weight:600}
|
|
109
|
+
.chip.newchat:hover{background:#2a63c8;border-color:transparent}
|
|
110
|
+
.view-switch{background:transparent;border-color:transparent;gap:2px}
|
|
111
|
+
.view-switch button{color:var(--text-dim)}
|
|
112
|
+
.view-switch button.active{background:var(--bg-elev2);border-radius:999px;color:var(--text)}
|
|
113
|
+
|
|
114
|
+
/* ---- 侧栏:层级靠底色深浅,不靠描边 ---- */
|
|
115
|
+
.panel{border-right:none;background:var(--bg-elev)}
|
|
116
|
+
.panel-right{border-left:1px solid var(--border-soft);background:var(--bg);width:216px}
|
|
117
|
+
.panel .panel-title,.panel .panel-section-title,.panel-header .panel-title{font-size:11px;letter-spacing:.8px;text-transform:uppercase;color:var(--text-faint)}
|
|
118
|
+
.session-item.active,.project-item.active{background:#fff;border-color:var(--border);box-shadow:0 1px 3px rgba(0,0,0,.06)}
|
|
119
|
+
.panel-crumbs .crumb{color:var(--text-faint)}
|
|
120
|
+
.panel-crumbs .crumb.active{color:var(--text-dim)}
|
|
121
|
+
|
|
122
|
+
/* ---- 对话流:收窄到 780px,和 composer 对齐 ---- */
|
|
123
|
+
.msg,.goalbar,.inputbox{max-width:780px}
|
|
124
|
+
.msg-text{line-height:1.75}
|
|
125
|
+
.messages{padding-top:28px}
|
|
126
|
+
/* markdown 表格降噪:去竖线只留横向分隔,表头不抢戏 */
|
|
127
|
+
.msg-text th,.msg-text td{border:none;border-bottom:1px solid var(--border-soft);padding:6px 12px}
|
|
128
|
+
.msg-text thead th{border-bottom:1px solid var(--border);background:transparent;color:var(--text-dim);font-weight:600;text-align:left}
|
|
129
|
+
.msg-text table{margin:10px 0;width:100%}
|
|
130
|
+
.msg-text hr{border-color:var(--border-soft)}
|
|
131
|
+
/* 用户消息:浅灰气泡靠右(Codex 同款),替换上游的紫色调 */
|
|
132
|
+
.msg-user .msg-body{background:var(--bg-elev2);border:1px solid transparent;border-radius:14px;margin-left:auto;width:fit-content;max-width:min(85%,620px)}
|
|
133
|
+
|
|
134
|
+
/* ---- 代码:亮色卡片 + GitHub-Light 配色(压过上游写死的 #0b0d12) ---- */
|
|
135
|
+
.codeblock pre{background:#f6f5f1!important;border:1px solid var(--border-soft)}
|
|
136
|
+
.termline{background:#f6f5f1;border-color:var(--border-soft)}
|
|
137
|
+
.toolcall-output pre{background:#f6f5f1;border-color:var(--border-soft);color:var(--text-dim)}
|
|
138
|
+
.fp-code{background:#f6f5f1}
|
|
139
|
+
.fp-code.dragging .fp-line.sel{background:var(--accent-soft)}
|
|
140
|
+
.thinking{background:#f7f5f1}
|
|
141
|
+
.hljs{color:#24292f;background:transparent}
|
|
142
|
+
.hljs-comment,.hljs-quote{color:#6e7781}
|
|
143
|
+
.hljs-keyword,.hljs-doctag,.hljs-template-tag,.hljs-template-variable,.hljs-type{color:#cf222e}
|
|
144
|
+
.hljs-title,.hljs-title.class_,.hljs-title.function_{color:#8250df}
|
|
145
|
+
.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#0550ae}
|
|
146
|
+
.hljs-string,.hljs-regexp{color:#0a3069}
|
|
147
|
+
.hljs-built_in,.hljs-symbol{color:#953800}
|
|
148
|
+
.hljs-name,.hljs-selector-tag,.hljs-selector-pseudo{color:#116329}
|
|
149
|
+
.hljs-section{color:#0550ae;font-weight:700}
|
|
150
|
+
.hljs-bullet{color:#953800}
|
|
151
|
+
.hljs-emphasis{font-style:italic}
|
|
152
|
+
.hljs-strong{font-weight:700}
|
|
153
|
+
.hljs-addition{color:#116329;background:#dafbe1}
|
|
154
|
+
.hljs-deletion{color:#82071e;background:#ffebe9}
|
|
155
|
+
|
|
156
|
+
/* ---- Composer:白色主舞台,轻投影 ---- */
|
|
157
|
+
.inputbar{padding:10px 24px 26px}
|
|
158
|
+
.inputbox{background:#fff;border-color:var(--border);border-radius:18px;box-shadow:0 1px 2px rgba(0,0,0,.04),0 6px 24px rgba(0,0,0,.07)}
|
|
159
|
+
.inputbox:focus-within{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft),0 6px 24px rgba(0,0,0,.07)}
|
|
160
|
+
|
|
161
|
+
/* ---- 状态栏与滚动条:退到最底的音量 ---- */
|
|
162
|
+
.statusbar{background:transparent;border-top:1px solid var(--border-soft)}
|
|
163
|
+
::-webkit-scrollbar{width:10px;height:10px}
|
|
164
|
+
::-webkit-scrollbar-thumb{background:#00000024;border-radius:5px;border:2px solid transparent;background-clip:content-box}
|
|
165
|
+
::-webkit-scrollbar-thumb:hover{background:#0000003d;border-radius:5px;border:2px solid transparent;background-clip:content-box}
|
|
166
|
+
::-webkit-scrollbar-track{background:transparent}
|
|
167
|
+
`;
|
|
33
168
|
function replaceFile(path, content) {
|
|
34
169
|
rmSync(path, { force: true });
|
|
35
170
|
writeFileSync(path, content);
|
|
@@ -40,13 +175,23 @@ export function applyWebUiBranding(binPath) {
|
|
|
40
175
|
const indexHtml = join(dist, "index.html");
|
|
41
176
|
if (!existsSync(indexHtml))
|
|
42
177
|
return; // 上游布局变了就跳过,页面保持原样也能用
|
|
178
|
+
// 资产文件每次启动重写:内容随 CLI 版本走,不依赖 html 的版本标记
|
|
179
|
+
replaceFile(join(dist, "u1s1-brand.js"), BRAND_JS);
|
|
180
|
+
replaceFile(join(dist, "u1s1-theme.css"), THEME_CSS);
|
|
181
|
+
replaceFile(join(dist, "favicon.svg"), FAVICON_SVG);
|
|
43
182
|
let html = readFileSync(indexHtml, "utf8");
|
|
44
183
|
if (html.includes(PATCH_MARK))
|
|
45
184
|
return;
|
|
46
|
-
|
|
47
|
-
|
|
185
|
+
// 老版本补丁(u1s1-brand-v1 等)的注入行先剥掉,避免升级后重复注入
|
|
186
|
+
html = html
|
|
187
|
+
.replace(/[ \t]*<!-- u1s1-brand-[^>]*-->\n?/g, "")
|
|
188
|
+
.replace(/[ \t]*<script defer src="\/u1s1-brand\.js"><\/script>\n?/g, "")
|
|
189
|
+
.replace(/[ \t]*<link rel="stylesheet" href="\/u1s1-theme\.css"\s*\/?>\n?/g, "");
|
|
48
190
|
html = html
|
|
49
191
|
.replace(/<title>[^<]*<\/title>/, `<title>${PAGE_TITLE}</title>`)
|
|
50
|
-
.replace("</head>", ` <!-- ${PATCH_MARK} -->\n
|
|
192
|
+
.replace("</head>", ` <!-- ${PATCH_MARK} -->\n` +
|
|
193
|
+
` <link rel="stylesheet" href="/u1s1-theme.css" />\n` +
|
|
194
|
+
` <script defer src="/u1s1-brand.js"></script>\n` +
|
|
195
|
+
` </head>`);
|
|
51
196
|
replaceFile(indexHtml, html);
|
|
52
197
|
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "u1s1-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "u1s1 — 有一说一,最省心的 AI 编程搭子。终端里用中文说需求,AI 帮你读文件、改代码、跑命令。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"u1s1": "dist/index.js"
|
|
8
8
|
},
|
|
9
|
+
"exports": {
|
|
10
|
+
"./embed": {
|
|
11
|
+
"types": "./dist/embed.d.ts",
|
|
12
|
+
"default": "./dist/embed.js"
|
|
13
|
+
},
|
|
14
|
+
"./package.json": "./package.json"
|
|
15
|
+
},
|
|
9
16
|
"files": [
|
|
10
|
-
"dist"
|
|
17
|
+
"dist",
|
|
18
|
+
"webui-dist"
|
|
11
19
|
],
|
|
12
20
|
"engines": {
|
|
13
21
|
"node": ">=22.19.0"
|
|
@@ -36,7 +44,7 @@
|
|
|
36
44
|
"typebox": "1.3.7"
|
|
37
45
|
},
|
|
38
46
|
"optionalDependencies": {
|
|
39
|
-
"pi-web-ui": "0.
|
|
47
|
+
"pi-web-ui": "0.26.0"
|
|
40
48
|
},
|
|
41
49
|
"devDependencies": {
|
|
42
50
|
"@types/node": "^22.15.0",
|