@aiti-ai/aiti-msg-nav 0.0.0-stage → 0.1.7
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/AGENTS.md +28 -0
- package/LICENSE +21 -0
- package/check-client.mjs +273 -0
- package/client/src/entry.tsx +26 -0
- package/client/src/host-shim.ts +107 -0
- package/client/src/jsx-runtime.ts +2 -0
- package/client/src/msg-nav-toolbar.tsx +871 -0
- package/deploy-commit.mjs +52 -0
- package/dev-pack.mjs +246 -0
- package/dist/aiti-msg-nav-client.mjs +841 -0
- package/icon.svg +7 -0
- package/index.ts +50 -0
- package/package.json +24 -4
- package/reload-instances.mjs +60 -0
- package/tools/build-client.mjs +24 -0
- package/tools/deploy.mjs +74 -0
- package/vite.config.ts +45 -0
- package/README.md +0 -3
package/icon.svg
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
2
|
+
<circle cx="12" cy="6" r="1.8"/>
|
|
3
|
+
<circle cx="12" cy="18" r="1.8"/>
|
|
4
|
+
<path d="M12 11v2"/>
|
|
5
|
+
<path d="M12 9h.01"/>
|
|
6
|
+
<path d="M12 15h.01"/>
|
|
7
|
+
</svg>
|
package/index.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* aiti-msg-nav · 消息滚动导航(到最早 / 到最新 / 上一个用户输入 / 下一个用户输入)
|
|
3
|
+
*
|
|
4
|
+
* 原为 pi-web 内置的 ScrollToolbar(components/ScrollToolbar.tsx),已整体抽离为
|
|
5
|
+
* 扩展:core 只留一个 chatOverlay 插槽挂载点(消息区覆盖层),全部交互/定位/滚动
|
|
6
|
+
* 逻辑在扩展 client bundle 里,靠两条 DOM 契约工作:
|
|
7
|
+
* - [data-chat-scroll] 消息滚动容器(core 给消息滚动 div 打的标记)
|
|
8
|
+
* - [data-chat-msg-role] 消息包装节点(role="user"/"assistant";用户消息
|
|
9
|
+
* 即 prev/next 的导航目标)
|
|
10
|
+
* 依赖核心懒分页:滚到顶部触发 sentinel → core 自动加载更早消息并保持视口,扩展
|
|
11
|
+
* 只需滚动到容器顶端并延时重试(不再需要 core 的 setVisibleCount)。
|
|
12
|
+
*
|
|
13
|
+
* 纯前端扩展:无服务端 API。features(extman 可管理):
|
|
14
|
+
* - show.toolbar 总开关(关闭 = 不渲染导航条)
|
|
15
|
+
* - behavior.keepAwake 长按「到最新」保持屏幕常亮(Screen Wake Lock API)
|
|
16
|
+
*/
|
|
17
|
+
export default {
|
|
18
|
+
id: "aiti-msg-nav",
|
|
19
|
+
name: "消息滚动导航",
|
|
20
|
+
version: "0.1.0",
|
|
21
|
+
icon: "icon.svg",
|
|
22
|
+
client: "dist/aiti-msg-nav-client.mjs",
|
|
23
|
+
|
|
24
|
+
ui: {
|
|
25
|
+
clientDependsOn: [],
|
|
26
|
+
i18n: {
|
|
27
|
+
"zh-CN": {
|
|
28
|
+
"nav.earliest": "到最早消息",
|
|
29
|
+
"nav.prevUser": "上一个用户输入",
|
|
30
|
+
"nav.nextUser": "下一个用户输入",
|
|
31
|
+
"nav.latestHint": "到最新消息(长按保持屏幕常亮)",
|
|
32
|
+
"nav.keepAwakeOn": "屏幕保持常亮",
|
|
33
|
+
},
|
|
34
|
+
"en": {
|
|
35
|
+
"nav.earliest": "Jump to earliest message",
|
|
36
|
+
"nav.prevUser": "Previous user message",
|
|
37
|
+
"nav.nextUser": "Next user message",
|
|
38
|
+
"nav.latestHint": "Jump to latest (long-press to keep the screen awake)",
|
|
39
|
+
"nav.keepAwakeOn": "Screen stays awake",
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
features: [
|
|
45
|
+
{ id: "show.toolbar", label: "显示消息滚动导航条", description: "消息区右侧的到最早/到最新/上一条/下一条按钮列", default: true },
|
|
46
|
+
{ id: "behavior.keepAwake", label: "长按『到最新』保持屏幕常亮", description: "agent 运行时长按最底部按钮可让屏幕保持常亮(Screen Wake Lock API,不支持时自动降级)", default: true },
|
|
47
|
+
],
|
|
48
|
+
|
|
49
|
+
server: {},
|
|
50
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiti-ai/aiti-msg-nav",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"version": "0.1.7",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "消息滚动导航扩展(到最早 / 到最新 / 上一个用户输入 / 下一个用户输入)—— JSX + vite 构建 client bundle",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build:client": "vite build --config vite.config.ts",
|
|
8
|
+
"deploy": "node tools/deploy.mjs"
|
|
9
|
+
},
|
|
10
|
+
"devDependencies": {
|
|
11
|
+
"vite": "^6.4.3"
|
|
12
|
+
},
|
|
13
|
+
"soul": "千楼万楼的聊天记录,电梯比楼梯好用。",
|
|
14
|
+
"aitiExtProtocol": "1.0",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"contentHash": "a296050c20106fde",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/aiti-ai/aiti-ai.git",
|
|
20
|
+
"directory": "extensions/aiti-msg-nav"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://github.com/aiti-ai/aiti-ai#readme",
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/aiti-ai/aiti-ai/issues"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* _shared/reload-instances.mjs · pi-web 双实例热重载(各扩展 deploy.mjs 共用)
|
|
3
|
+
*
|
|
4
|
+
* dev 3001 / 生产 3002;端口可经 AITI_DEV_PORT / AITI_PROD_PORT 覆盖;
|
|
5
|
+
* basePath 双 env(AITI_DEV_BASE_PATH / AITI_PROD_BASE_PATH)按机器设值,
|
|
6
|
+
* 推荐值不带斜杠(48:dev 与 空;155:系统级 wdev 与 win)——git-bash 启动
|
|
7
|
+
* 原生程序时会把 / 开头的 env 值做 MSYS 路径转换(/wdev → D:/apps/Git/wdev)
|
|
8
|
+
* 致 URL 非法;不带斜杠则不触发转换。此处自动补 /(旧 /xxx 格式仍兼容),
|
|
9
|
+
* 兼容不了时提示 MSYS_NO_PATHCONV=1。
|
|
10
|
+
* 避免「server 端代码改了、实例没 reload 导致跑旧逻辑」。
|
|
11
|
+
*
|
|
12
|
+
* 用法(静态 import 或顶层 await import 均可):
|
|
13
|
+
* import { reloadInstances } from "../../_shared/reload-instances.mjs";
|
|
14
|
+
* await reloadInstances(); // 默认 "[deploy] " 前缀
|
|
15
|
+
*/
|
|
16
|
+
const BASE_PATH_RE = /^\/[A-Za-z0-9._-]*$/;
|
|
17
|
+
|
|
18
|
+
function buildInstance(name, portEnv, portDefault, pathEnv, pathDefault) {
|
|
19
|
+
const origin = `http://127.0.0.1:${process.env[portEnv] || portDefault}`;
|
|
20
|
+
const raw = process.env[pathEnv];
|
|
21
|
+
if (raw === undefined || raw === null || String(raw).trim() === "") return { name, base: origin + pathDefault };
|
|
22
|
+
// 自动补 /:wdev → /wdev(推荐无斜杠格式,MSYS 不转换);/wdev → /wdev(旧格式兼容)
|
|
23
|
+
const p = "/" + String(raw).trim().replace(/^\/+|\/+$/g, "");
|
|
24
|
+
if (BASE_PATH_RE.test(p)) return { name, base: origin + p };
|
|
25
|
+
return { name, base: null, raw };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** 双实例清单(dev/prod 各 {name, base});reload-instances 与 reconcile-deps 等共用。 */
|
|
29
|
+
export function buildInstances() {
|
|
30
|
+
return [
|
|
31
|
+
buildInstance("dev", "AITI_DEV_PORT", 3001, "AITI_DEV_BASE_PATH", "/dev"),
|
|
32
|
+
buildInstance("prod", "AITI_PROD_PORT", 3002, "AITI_PROD_BASE_PATH", ""),
|
|
33
|
+
];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function reloadInstances(prefix = "[deploy] ") {
|
|
37
|
+
// 批次合并(deploy-all 场景):子条目部署脚本被注入 PI_WEB_DEFER_RELOAD=1 时不各自 reload,
|
|
38
|
+
// 由 deploy-all 在全部条目完成后统一执行一轮(N 条目 × 2 实例 → 2 次 POST)。
|
|
39
|
+
// 避免批量部署时的 reload 风暴(2026-09-15 实测:65 条目逐个 reload 把 dev 实例打满)。
|
|
40
|
+
// 单独跑某个扩展 deploy.mjs 不带该 env → 行为不变(部署完立即 reload)。
|
|
41
|
+
if (process.env.PI_WEB_DEFER_RELOAD === "1") {
|
|
42
|
+
console.log(`${prefix}reload 已 defer(PI_WEB_DEFER_RELOAD=1,由 deploy-all 收尾统一执行)`);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
// 允许失败:网络异常/非 2xx/服务未启动/env 非法一律只打提示,永不 throw——不影响 deploy 退出码
|
|
46
|
+
for (const inst of buildInstances()) {
|
|
47
|
+
if (inst.base === null) {
|
|
48
|
+
const msysHint = /[\\:]/.test(inst.raw) ? "——疑似 git-bash MSYS 路径转换,用 MSYS_NO_PATHCONV=1 前缀重跑" : "";
|
|
49
|
+
console.log(`${prefix}reload ${inst.name} 跳过:AITI_${inst.name.toUpperCase()}_BASE_PATH="${inst.raw}" 非法(须形如 /xxx 或留空)${msysHint}`);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
const r = await fetch(inst.base + "/api/extensions/reload", { method: "POST" });
|
|
54
|
+
console.log(`${prefix}reload ${inst.name} → HTTP ${r.status} ${r.ok ? "✓" : "⚠ 失败(请手动 POST /api/extensions/reload)"}`);
|
|
55
|
+
} catch (e) {
|
|
56
|
+
const cause = e && e.cause ? String(e.cause.code || e.cause.message || e.cause) : String((e && e.message) || e);
|
|
57
|
+
console.log(`${prefix}reload ${inst.name} 不可达(${cause})— 需手动 POST /api/extensions/reload`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* aiti-msg-nav · 构建 JSX client bundle(Vite lib mode → client/aiti-msg-nav-client.mjs)
|
|
3
|
+
*/
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { resolve } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
const root = resolve(fileURLToPath(new URL("..", import.meta.url)));
|
|
9
|
+
|
|
10
|
+
// Windows 上 .bin/vite(无扩展名 sh 脚本)spawn 会 status=null 静默失败;
|
|
11
|
+
// 跨平台统一 node 直跑 vite.js 入口(.bin 垫片的真实目标,npm scripts 内部同做法)。
|
|
12
|
+
const viteJs = resolve(root, "node_modules", "vite", "bin", "vite.js");
|
|
13
|
+
const viteBin = (await import("node:fs")).existsSync(viteJs) ? viteJs : "vite";
|
|
14
|
+
|
|
15
|
+
const r = spawnSync(process.execPath, [viteBin, "build", "--config", resolve(root, "vite.config.ts")], {
|
|
16
|
+
cwd: root,
|
|
17
|
+
stdio: "inherit",
|
|
18
|
+
shell: false,
|
|
19
|
+
});
|
|
20
|
+
if (r.status !== 0) {
|
|
21
|
+
console.error("[aiti-msg-nav] vite build 失败(exit " + r.status + ")。若提示找不到 vite:cd pi-web-ext/aiti-msg-nav && npm install");
|
|
22
|
+
process.exit(r.status ?? 1);
|
|
23
|
+
}
|
|
24
|
+
console.log("[aiti-msg-nav] JSX client bundle → client/aiti-msg-nav-client.mjs");
|
package/tools/deploy.mjs
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* aiti-msg-nav · 部署到 ~/.pi/web/extensions/ 和 ~/.pi/web-dev/extensions/
|
|
4
|
+
*
|
|
5
|
+
* 部署 = client bundle 构建(vite)→ 整目录同步到两个扩展根(dev/prod 隔离)→ git 提交部署状态。
|
|
6
|
+
* 用法:node tools/deploy.mjs
|
|
7
|
+
* 生效:POST /api/extensions/reload(或重启 pi-web);client no-store 刷新页面即新
|
|
8
|
+
*/
|
|
9
|
+
import { cpSync, rmSync, mkdirSync } from "node:fs";
|
|
10
|
+
import { resolve } from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
import { homedir } from "node:os";
|
|
13
|
+
import { execSync } from "node:child_process";
|
|
14
|
+
import { deployCommit } from "../../_shared/deploy-commit.mjs";
|
|
15
|
+
|
|
16
|
+
// client TS 残留检查(.mjs 里是合法 JS 语法检查不报、运行时必炸;FAIL 即部署失败)
|
|
17
|
+
{
|
|
18
|
+
const { checkClientDir } = await import("../../_shared/check-client.mjs");
|
|
19
|
+
checkClientDir(fileURLToPath(new URL("../client", import.meta.url)), { log: (m) => console.log("[deploy] " + m) });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const root = resolve(import.meta.dirname, ".."); // pi-web-ext/aiti-msg-nav
|
|
23
|
+
|
|
24
|
+
// 0) 客户端 bundle 构建(client/src/*.tsx → client/aiti-msg-nav-client.mjs)
|
|
25
|
+
console.log("[deploy] 构建 JSX client bundle…");
|
|
26
|
+
try {
|
|
27
|
+
execSync("node tools/build-client.mjs", { cwd: root, stdio: "inherit" });
|
|
28
|
+
} catch (e) {
|
|
29
|
+
console.error("[deploy] 构建失败,中止部署:", e.message);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const targets = [
|
|
34
|
+
resolve(homedir(), ".pi", "web", "extensions", "aiti-msg-nav"),
|
|
35
|
+
resolve(homedir(), ".pi", "web-dev", "extensions", "aiti-msg-nav"),
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
for (const extDir of targets) {
|
|
39
|
+
mkdirSync(extDir, { recursive: true });
|
|
40
|
+
rmSync(extDir, { recursive: true, force: true });
|
|
41
|
+
cpSync(root, extDir, {
|
|
42
|
+
recursive: true,
|
|
43
|
+
filter: (src) => {
|
|
44
|
+
const rel = src.slice(root.length + 1);
|
|
45
|
+
// 不打包构建工具/依赖;client/ 产物与源码都带上(manifest client 字段指产物)
|
|
46
|
+
if (rel.split("/")[0] === "tools") return false;
|
|
47
|
+
if (rel.split("/").includes("node_modules")) return false;
|
|
48
|
+
return true;
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
console.log("[deploy] → " + extDir);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 部署状态提交(生产根是 git 仓库;失败不影响部署)
|
|
55
|
+
const extRepo = resolve(homedir(), ".pi", "web", "extensions");
|
|
56
|
+
|
|
57
|
+
deployCommit({ repo: extRepo, name: "aiti-msg-nav", paths: ["aiti-msg-nav"], log: (m) => console.log("[deploy] " + m) });
|
|
58
|
+
console.log("[deploy] 完成。reload: POST /api/extensions/reload");
|
|
59
|
+
|
|
60
|
+
// ── 双实例热重载(basePath 双 env 按机器设值)——允许失败:服务未启动/共享模块缺失仅提示,不影响 deploy 退出码 ──
|
|
61
|
+
try {
|
|
62
|
+
const { reloadInstances } = await import("../../_shared/reload-instances.mjs");
|
|
63
|
+
await reloadInstances();
|
|
64
|
+
} catch (e) {
|
|
65
|
+
console.log("[deploy] 自动 reload 未执行(" + String((e && e.message) || e) + ")— 需手动 POST /api/extensions/reload");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ── dev-pack(方案 §3.2:源码+构建工具+AGENTS.md 直达部署目录,AI 可现场直改)——允许失败,不影响 deploy 退出码 ──
|
|
69
|
+
try {
|
|
70
|
+
const { packDevPack } = await import("../../_shared/dev-pack.mjs");
|
|
71
|
+
packDevPack({ extRoot: resolve(import.meta.dirname, ".."), log: (m) => console.log("[deploy] " + m) });
|
|
72
|
+
} catch (e) {
|
|
73
|
+
console.log("[deploy] dev-pack 未执行(" + String((e && e.message) || e) + ")");
|
|
74
|
+
}
|
package/vite.config.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* aiti-msg-nav · client 构建配置(Vite lib mode,参考 pi-web-ext/demo)。
|
|
3
|
+
*
|
|
4
|
+
* 把 client/src/entry.tsx(JSX + TS)打包成单个 ESM bundle:client/aiti-msg-nav-client.mjs
|
|
5
|
+
* (index.ts 的 client 字段指向它)。
|
|
6
|
+
* - lib mode(formats: ['es']):产物是一个 register(ctx) 函数,宿主 ExtSlots 动态 import。
|
|
7
|
+
* - react 重定向:JSX 自动运行时默认 import "react/jsx-runtime",扩展 client 不引入
|
|
8
|
+
* React——resolve.alias 指到 client/src/host-shim.ts / jsx-runtime.ts,所有 hooks 委托
|
|
9
|
+
* 宿主 register(ctx) 传入的 h/useState/…。产物零 React 引用。
|
|
10
|
+
*
|
|
11
|
+
* 构建:npm run build:client (或 node tools/build-client.mjs)
|
|
12
|
+
*/
|
|
13
|
+
import { defineConfig } from "vite";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
|
|
16
|
+
export default defineConfig({
|
|
17
|
+
esbuild: {
|
|
18
|
+
jsx: "automatic",
|
|
19
|
+
jsxImportSource: "react",
|
|
20
|
+
},
|
|
21
|
+
resolve: {
|
|
22
|
+
alias: [
|
|
23
|
+
// 正则精确匹配包名(字符串 alias 前缀匹配会吞掉 react/jsx-runtime)
|
|
24
|
+
{ find: /^react\/jsx-dev-runtime$/, replacement: fileURLToPath(new URL("client/src/jsx-runtime.ts", import.meta.url)) },
|
|
25
|
+
{ find: /^react\/jsx-runtime$/, replacement: fileURLToPath(new URL("client/src/jsx-runtime.ts", import.meta.url)) },
|
|
26
|
+
{ find: /^react$/, replacement: fileURLToPath(new URL("client/src/host-shim.ts", import.meta.url)) },
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
build: {
|
|
30
|
+
lib: {
|
|
31
|
+
entry: fileURLToPath(new URL("client/src/entry.tsx", import.meta.url)),
|
|
32
|
+
formats: ["es"],
|
|
33
|
+
fileName: () => "aiti-msg-nav-client.mjs",
|
|
34
|
+
},
|
|
35
|
+
outDir: "dist",
|
|
36
|
+
emptyOutDir: true, // 别清掉 client/src(源码)
|
|
37
|
+
target: "es2020",
|
|
38
|
+
minify: false,
|
|
39
|
+
sourcemap: false,
|
|
40
|
+
rollupOptions: {
|
|
41
|
+
// 本扩展无外部 npm 依赖;全部内联进单个 .mjs
|
|
42
|
+
external: [],
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
});
|
package/README.md
DELETED