8000 fix(collector): give development app builds their own bundle identifier by InfinityPacer · Pull Request #60 · InfinityPacer/meterleaf · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ Meterleaf 是独立 AI 用量账本,只读采集上游用量并在本地计价
- `bun run dev:server` 与 `bun run dev`:分别启动真实后端与前端。
- `bun run typecheck`:检查 TypeScript 类型;`bun test tests`:运行测试。
- `bun run build`:类型检查并构建前端;`bun run start`:启动应用。
- `bun run build:collector`:构建采集器命令行与 macOS 上的 `dist/Meterleaf.app`。
- `bun run build:collector`:构建采集器命令行与 macOS 上用于验证的 `dist/Meterleaf Dev.app`,它的应用标识与正式应用不同,不会接管本机已安装 Meterleaf 的后台任务。
- `bun run build:app`:构建用于安装的 `dist/Meterleaf.app`,只在要替换「应用程序」中的 Meterleaf 时使用,装好后删掉 `dist` 里的这份,避免系统把后台任务解析到它。

## 代码风格与注释

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ docker compose up -d --pull never

```sh
bun install --frozen-lockfile
bun run build:collector
bun run build:app
```

把 `dist/Meterleaf.app` 移到「应用程序」,然后生成写入密钥。`--server` 填这台 Mac 能访问到的 Meterleaf 地址,服务端不在本机时先按[部署指南](docs/deployment.md#接入本机采集器)开放访问:
Expand Down
4 changes: 2 additions & 2 deletions docs/collector.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,10 +184,10 @@ meterleaf-collector claude-profiles \

```sh
bun install --frozen-lockfile
bun run build:collector
bun run build:app
```

构建产物是 `dist/Meterleaf.app`,把它移到「应用程序」文件夹。以下命令中的 `meterleaf-collector` 指应用内的可执行文件。
构建产物是 `dist/Meterleaf.app`,把它移到「应用程序」文件夹,不要在 `dist` 里留一份。系统按应用标识找后台任务要运行的程序,同一标识有两份应用时,后台任务可能改跑 `dist` 里那份。开发验证用的 `bun run build:collector` 生成标识不同的 `Meterleaf Dev.app`,不受这个影响。以下命令中的 `meterleaf-collector` 指应用内的可执行文件。

```sh
alias meterleaf-collector="/Applications/Meterleaf.app/Contents/MacOS/meterleaf-collector"
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
"test": "bun test tests",
"test:browser": "bun tests/browser.smoke.ts",
"collector": "bun src/collector/main.ts",
"build:collector": "bun scripts/build-collector.ts"
"build:collector": "bun scripts/build-collector.ts",
"build:app": "bun scripts/build-collector.ts --release"
},
"dependencies": {
"@base-ui/react": "^1.8.0",
Expand Down
23 changes: 16 additions & 7 deletions scripts/build-collector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,22 @@ import {

/**
* 构建采集器:dist/meterleaf-collector 是通用命令行可执行文件;在 macOS 上另外打包
* dist/Meterleaf.app。包内带 SMAppService 注册助手与后台任务 plist,
* 应用包。包内带 SMAppService 注册助手与后台任务 plist,
* 让后台任务在“登录项与扩展”中显示 Meterleaf 名称与图标。
*
* 默认构建用于开发验证,生成标识不同的 `Meterleaf Dev.app`。系统按应用标识解析已登记的
* 后台任务,两个同标识的应用包同时存在时,后台任务可能改跑仓库里刚构建的那份。
* `--release` 才生成用于安装的 `Meterleaf.app`。
*/
const release = process.argv.includes("--release");
const bundleIdentifier = release
? BUNDLE_IDENTIFIER
: `${BUNDLE_IDENTIFIER}.dev`;
const displayName = release ? "Meterleaf" : "Meterleaf Dev";
const root = resolve(import.meta.dir, "..");
const dist = join(root, "dist");
const binary = join(dist, "meterleaf-collector");
const bundle = join(dist, "Meterleaf.app");
const bundle = join(dist, `${displayName}.app`);

function run(command: string[]) {
const result = Bun.spawnSync(command, {
Expand Down Expand Up @@ -55,11 +64,11 @@ function infoPlist(): string {
<plist version="1.0">
<dict>
<key>CFBundleIdentifier</key>
<string>${BUNDLE_IDENTIFIER}</string>
<string>${bundleIdentifier}</string>
<key>CFBundleName</key>
<string>Meterleaf</string>
<string>${displayName}</string>
<key>CFBundleDisplayName</key>
<string>Meterleaf</string>
<string>${displayName}</string>
<key>CFBundleExecutable</key>
<string>meterleaf-collector</string>
<key>CFBundleIconFile</key>
Expand Down Expand Up @@ -145,7 +154,7 @@ if (process.platform === "darwin") {
writeFileSync(join(contents, "Info.plist"), infoPlist());
copyFileSync(binary, join(macos, "meterleaf-collector"));
// 同步与 OTEL 中继各自一个 plist,安装时由注册助手按名称分别登记。
writeServicePlists(agents);
writeServicePlists(agents, bundleIdentifier);
// SMAppService 需要 macOS 13;目标版本与 Info.plist 的 LSMinimumSystemVersion 一致。
const arch = process.arch === "arm64" ? "arm64" : "x86_64";
run([
Expand All @@ -166,7 +175,7 @@ if (process.platform === "darwin") {
"--sign",
"-",
"--identifier",
`${BUNDLE_IDENTIFIER}.service`,
`${bundleIdentifier}.service`,
join(macos, SERVICE_HELPER),
]);
runQuiet(["/usr/bin/codesign", "--force", "--sign", "-", bundle]);
Expand Down
38 changes: 29 additions & 9 deletions src/collector/launchd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,14 +118,15 @@ function renderServiceAgent(
label: string,
command: string,
schedule: string,
bundleIdentifier: string,
): string {
return `${plistHeader}
<dict>
<key>Label</key>
<string>${label}</string>
<key>AssociatedBundleIdentifiers</key>
<array>
<string>${BUNDLE_IDENTIFIER}</string>
<string>${bundleIdentifier}</string>
</array>
<key>BundleProgram</key>
<string>Contents/MacOS/${SERVICE_HELPER}</string>
Expand All @@ -147,13 +148,27 @@ ${schedule}
* 应用包解析,因此应用移动后仍然有效;内容固定,不能携带环境变量。启动的是注册助手,
* 由它 exec 采集器,原因见 macos/meterleaf-service.swift。
*/
export function renderServicePlist(): string {
return renderServiceAgent(SERVICE_LABEL, "sync", syncSchedule);
export function renderServicePlist(
bundleIdentifier: string = BUNDLE_IDENTIFIER,
): string {
return renderServiceAgent(
SERVICE_LABEL,
"sync",
syncSchedule,
bundleIdentifier,
);
}

/** 常驻接收 Claude Code OTEL 日志的包内任务,与同步任务各自独立注册。 */
export function renderOtelRelayServicePlist(): string {
return renderServiceAgent(OTEL_RELAY_LABEL, "otel-relay", relaySchedule);
export function renderOtelRelayServicePlist(
bundleIdentifier: string = BUNDLE_IDENTIFIER,
): string {
return renderServiceAgent(
OTEL_RELAY_LABEL,
"otel-relay",
relaySchedule,
bundleIdentifier,
);
}

/** 旧式 LaunchAgent:可携带自定义环境变量,但登录项中显示为可执行文件名。 */
Expand Down Expand Up @@ -211,7 +226,7 @@ export interface ServiceAgent {
label: string;
plistName: string;
summary: string;
render(): string;
render(bundleIdentifier?: string): string;
}

/** 包内后台任务的完整清单;构建脚本按此写入 plist,安装与状态也按此遍历。 */
Expand All @@ -232,11 +247,16 @@ export const SERVICE_AGENTS: readonly ServiceAgent[] = [
},
];

/** 构建应用包时写入包内 LaunchAgents 目录。 */
export function writeServicePlists(dir: string): string[] {
/** 构建应用包时写入包内 LaunchAgents 目录,关联到该应用包自己的标识。 */
export function writeServicePlists(
dir: string,
bundleIdentifier: string = BUNDLE_IDENTIFIER,
): string[] {
mkdirSync(dir, { recursive: true });
return SERVICE_AGENTS.map((agent) => {
writeFileSync(join(dir, agent.plistName), agent.render(), { mode: 0o644 });
writeFileSync(join(dir, agent.plistName), agent.render(bundleIdentifier), {
mode: 0o644,
});
return agent.plistName;
});
}
Expand Down
8 changes: 8 additions & 0 deletions tests/collector-cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,14 @@ describe("采集器命令行", () => {
expect(readdirSync(agentDir).sort()).toEqual(
SERVICE_AGENTS.map((agent) => agent.plistName).sort(),
);
// 开发构建的应用包关联自己的标识,不会被系统当成已安装的 Meterleaf 来启动后台任务。
const devDir = join(root, "dev-bundle-agents");
writeServicePlists(devDir, "io.meterleaf.collector.dev");
for (const agent of SERVICE_AGENTS) {
const plist = readFileSync(join(devDir, agent.plistName), "utf8");
expect(plist).toContain("<string>io.meterleaf.collector.dev</string>");
expect(plist).not.toContain("<string>io.meterleaf.collector</string>");
}

if (process.platform === "darwin") {
for (const [name, plist] of [
Expand Down
Loading
0