refactor(core): migrate to Bun and converge acquisition policies and Agent boundaries

This commit is contained in:
root committed 2026-10-13 14:37:03 +08:00
1 parent 3f1c6d02f6
commit 27c0f27434
268 files changed
+7468 -7852

No files matched your search

+6 -6
View File
@@ -3,8 +3,8 @@
// → 移动 → 可选跨驱动复制 / 移动(Alist 异步任务)→ 下载回本地比对 sha256 → 不存在 ≠ 失败 → 清理。
//
// 用法:
// deno run -A scripts/alist-e2e.ts --url http://127.0.0.1:5244 --token <令牌> --root /s1 [--root2 /s2] [--size-mb 8]
// deno run -A scripts/alist-e2e.ts --url ... --user admin --password <密码> --root /s1
// bun scripts/alist-e2e.ts --url http://127.0.0.1:5244 --token <令牌> --root /s1 [--root2 /s2] [--size-mb 8]
// bun scripts/alist-e2e.ts --url ... --user admin --password <密码> --root /s1
// 也可用环境变量 UMT_ALIST_REAL_URL / _TOKEN / _USER / _PASSWORD / _ROOT / _ROOT2。退出码 0 = 全部通过。
import { createHash, randomBytes } from "node:crypto";
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
@@ -14,7 +14,7 @@ import { parseArgs } from "node:util";
import { type AlistAuth, AlistClient, AlistError, redactAlistText } from "../packages/adapters/src/alist/index.ts";
const { values: a } = parseArgs({
args: Deno.args,
args: process.argv.slice(2),
options: {
url: { type: "string" },
token: { type: "string" },
@@ -26,7 +26,7 @@ const { values: a } = parseArgs({
keep: { type: "boolean" },
},
});
const env = (k: string) => Deno.env.get(`UMT_ALIST_REAL_${k}`);
const env = (k: string) => process.env[`UMT_ALIST_REAL_${k}`];
const url = a.url ?? env("URL");
const token = a.token ?? env("TOKEN");
const user = a.user ?? env("USER");
@@ -37,7 +37,7 @@ if (!url || !root || (!token && !user)) {
console.error(
"用法:--url <地址> (--token <令牌> | --user <用户> --password <密码>) --root <可写目录> [--root2 <另一驱动目录>]",
);
Deno.exit(2);
process.exit(2);
}
const auth: AlistAuth = token
? { kind: "token", token }
@@ -137,4 +137,4 @@ if (!a.keep) {
}
await rm(dir, { recursive: true, force: true });
console.log(failed ? `\n${failed} 步失败` : "\n全部通过");
Deno.exit(failed ? 1 : 0);
process.exit(failed ? 1 : 0);
+8 -8
View File
@@ -1,10 +1,10 @@
// LIBV-1 回填已入库视频资产的版本规格(media_asset.delivery)。默认只读(dry-run),加 --apply 才改库。
//
// 用法(DATABASE_URL 必须显式给出,脚本不带默认库):
// DATABASE_URL=… deno run -A scripts/asset-spec-backfill.ts # 看清单:哪些资产会更新、前后规格
// DATABASE_URL=… deno run -A scripts/asset-spec-backfill.ts --entity <作品 id> # 只看一部作品
// DATABASE_URL=… deno run -A scripts/asset-spec-backfill.ts --probe # 对缺探测结果的本地文件跑 ffprobe(只读文件)
// DATABASE_URL=… deno run -A scripts/asset-spec-backfill.ts --apply # 写库
// DATABASE_URL=… bun scripts/asset-spec-backfill.ts # 看清单:哪些资产会更新、前后规格
// DATABASE_URL=… bun scripts/asset-spec-backfill.ts --entity <作品 id> # 只看一部作品
// DATABASE_URL=… bun scripts/asset-spec-backfill.ts --probe # 对缺探测结果的本地文件跑 ffprobe(只读文件)
// DATABASE_URL=… bun scripts/asset-spec-backfill.ts --apply # 写库
// --json 输出 JSON;--limit N 只处理前 N 个资产
//
// 规格来源:库内文件名解析 + 已有 media_info.probe + 入库时记下的资源解析(media_info._umt.quality)。
@@ -14,20 +14,20 @@ import { ffprobe } from "@umt/adapters";
import { createDb } from "@umt/db";
import { backfillVersionSpecs } from "../apps/server/src/modules/acquisition-organize/services/version-backfill.ts";
const argv = [...Deno.args];
const argv = [...process.argv.slice(2)];
const APPLY = argv.includes("--apply");
if (APPLY && argv.includes("--dry-run")) {
console.error("--apply 与 --dry-run 不能同时给出");
Deno.exit(2);
process.exit(2);
}
const arg = (k: string) => {
const i = argv.indexOf(k);
return i >= 0 ? argv[i + 1] : undefined;
};
const url = Deno.env.get("DATABASE_URL");
const url = process.env.DATABASE_URL;
if (!url) {
console.error("需要 DATABASE_URL(脚本不带默认库)");
Deno.exit(2);
process.exit(2);
}
const db = createDb({ connectionString: url, max: 2 });
try {
+6 -5
View File
@@ -1,11 +1,12 @@
// 文件操作基准(不进 ci):跨卷 move 1 GB、全量哈希 1 GB、10 万文件扫描;输出耗时与 MB/s。
// 用法:deno run -A scripts/bench/fs-bench.ts --base /opt/tmp-bench --other /dev/shm [--size-mb 1024] [--files 100000]
// 用法:bun scripts/bench/fs-bench.ts --base /opt/tmp-bench --other /dev/shm [--size-mb 1024] [--files 100000]
// base:被测盘(源文件、扫描树所在);other:跨卷 move 的目标(必须与 base 不同设备)
// 结果写进 docs/porting/moviepilot-rust-transfer.md「基准记录」。
import { randomBytes } from "node:crypto";
import { mkdir, mkdtemp, rm, stat, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { parseArgs } from "node:util";
import { Command, runtimeOpen } from "@umt/runtime";
import {
crossDeviceMove,
FsProxy,
@@ -17,7 +18,7 @@ import {
} from "../../packages/adapters/src/index.ts";
const { values } = parseArgs({
args: Deno.args,
args: process.argv.slice(2),
options: {
base: { type: "string" },
other: { type: "string", default: "/dev/shm" },
@@ -27,7 +28,7 @@ const { values } = parseArgs({
});
if (!values.base) {
console.error("需要 --base <被测目录>");
Deno.exit(2);
process.exit(2);
}
const MB = 1024 * 1024;
const sizeMb = Number(values["size-mb"]);
@@ -50,7 +51,7 @@ async function timed(name: string, bytes: number | null, fn: () => Promise<unkno
}
async function makeBig(p: string) {
const f = await Deno.open(p, { write: true, create: true, truncate: true });
const f = await runtimeOpen(p, { write: true, create: true, truncate: true });
const chunk = randomBytes(8 * MB);
for (let i = 0; i < sizeMb / 8; i++) await f.write(chunk);
f.close();
@@ -84,7 +85,7 @@ try {
const cp = join(work, "cp.bin");
await makeBig(cp);
await timed("对照:cp 跨卷", bytes, async () => {
const r = await new Deno.Command("cp", { args: [cp, join(other, "cp.bin")] }).output();
const r = await new Command("cp", { args: [cp, join(other, "cp.bin")] }).output();
if (!r.success) throw new Error("cp failed");
});
+2 -2
View File
@@ -9,9 +9,9 @@ services:
ports:
- "127.0.0.1:49377:9377"
mocknexus:
image: denoland/deno:2.9.7
image: oven/bun:1.4.3
working_dir: /repo
command: ["run", "--no-config", "--no-lock", "-A", "scripts/browser-verify/mock-site.ts"]
command: ["run", "scripts/browser-verify/mock-site.ts"]
volumes:
# 相对路径按项目目录(第一个 -f 文件所在的仓库根)解析
- ./:/repo:ro
+3 -3
View File
@@ -4,7 +4,7 @@
* 只输出布尔值与个数:不截图、不输出页面内容与 Cookie 值;对真实站点只有一次导航。
* 用法(只读数据库;camofox 需带 umt-cookies 插件,见 docs/runbook/browser.md §9):
* CAMOFOX_URL=http://127.0.0.1:49391 CAMOFOX_ACCESS_KEY=... DATABASE_URL=postgres://... SECRET_MASTER_KEY=... \
* deno run -A scripts/browser-verify/cookie-real.ts <site key>
* bun scripts/browser-verify/cookie-real.ts <site key>
* 密钥只从环境变量读,不写进文件。
*/
import {
@@ -20,11 +20,11 @@ import { createSecretStore } from "../../apps/server/src/modules/auth-settings/p
import { readAccountCookie } from "../../apps/server/src/modules/sources-identity/service/credentials.ts";
const env = (k: string) => {
const v = Deno.env.get(k);
const v = process.env[k];
if (!v) throw new Error(`缺少环境变量 ${k}`);
return v;
};
const siteKey = Deno.args[0] ?? "hhanclub";
const siteKey = process.argv.slice(2)[0] ?? "hhanclub";
const db = createDb({ connectionString: env("DATABASE_URL"), max: 2 });
const secrets = createSecretStore({ db, masterKey: env("SECRET_MASTER_KEY") });
const quiet = { info() {}, warn() {}, error() {}, debug() {} } as never;
+1 -1
View File
@@ -11,7 +11,7 @@ export CAMOFOX_BROWSER_IDLE_TIMEOUT_MS=${CAMOFOX_BROWSER_IDLE_TIMEOUT_MS:-30000}
export VERIFY_PG=postgres://umt:[email protected]:55447/umt
export OUT_DIR=${OUT_DIR:-$(pwd)/.browser-verify-out}
C="docker compose -p umtbr -f docker-compose.yml -f scripts/browser-verify/compose.yml"
V="deno run -A scripts/browser-verify/verify.ts"
V="bun scripts/browser-verify/verify.ts"
wait_healthy() {
for _ in $(seq 1 60); do
[ "$(docker inspect -f '{{.State.Health.Status}}' umtbr-camofox-1 2>/dev/null)" = healthy ] && return 0
+3 -3
View File
@@ -34,7 +34,7 @@ import { AUDIENCES_FIXTURE_RELEASES } from "../../packages/testkit/src/sites/nex
type Loose = any;
const env = (k: string, d?: string) => {
const v = Deno.env.get(k) ?? d;
const v = process.env[k] ?? d;
if (v === undefined) throw new Error(`缺少环境变量 ${k}`);
return v;
};
@@ -50,7 +50,7 @@ const EXPECTED_VERSION = env("VERIFY_CAMOFOX_VERSION", "1.18.0");
// SIGKILL 不执行 profile 落盘;新登录应明确识别为需登录,不能把它当作已恢复。
const KILL_LOGIN_REQUIRED = env("VERIFY_KILL_LOGIN_REQUIRED", "true") === "true";
const SCHEMA = "brverify";
const phase = Deno.args[0] ?? "a";
const phase = process.argv.slice(2)[0] ?? "a";
await mkdir(OUT, { recursive: true });
const results: Loose[] = [];
@@ -621,4 +621,4 @@ try {
await db.destroy();
await admin.destroy();
}
Deno.exit(results.length > 0 && results.every((r) => r.pass === true) ? 0 : 1);
process.exit(results.length > 0 && results.every((r) => r.pass === true) ? 0 : 1);
+5 -5
View File
@@ -2,7 +2,7 @@
* VIS-1 真机核验:对真实 camofox 打开 https://example.com/,截图 → 缩放成交给模型的图 → 在图上取链接坐标 →
* 按服务端同一套换算(coordinateSpace / screenToPage)点回页面 → 核对跳转。记录视口 / DPR / 截图尺寸。
* 用法(只读用现有容器,不改部署):
* CAMOFOX_URL=http://<camofox 容器 IP>:9377 CAMOFOX_ACCESS_KEY=... deno run -A scripts/browser-verify/vision-real.ts [out-dir]
* CAMOFOX_URL=http://<camofox 容器 IP>:9377 CAMOFOX_ACCESS_KEY=... bun scripts/browser-verify/vision-real.ts [out-dir]
* 访问密钥只从环境变量读,不写进文件。
*/
import { mkdir, writeFile } from "node:fs/promises";
@@ -11,9 +11,9 @@ import { createCamofoxBrowserProvider, imageDimensions, prepareImage, staticBrow
import type { NavigationBudget } from "@umt/contracts";
import { coordinateSpace, screenToPage } from "../../apps/server/src/modules/browser/service.ts";
const baseUrl = Deno.env.get("CAMOFOX_URL") ?? "http://camofox:9377";
const key = Deno.env.get("CAMOFOX_ACCESS_KEY") ?? "";
const out = Deno.args[0] ?? ".browser-verify-out/vision";
const baseUrl = process.env.CAMOFOX_URL ?? "http://camofox:9377";
const key = process.env.CAMOFOX_ACCESS_KEY ?? "";
const out = process.argv.slice(2)[0] ?? ".browser-verify-out/vision";
const PROVIDER = "00000000-0000-4000-8000-00000000f15a";
const budget: NavigationBudget = {
domainAllowlist: ["example.com", "iana.org"],
@@ -77,4 +77,4 @@ try {
await p.closeSession(opened.session.id).catch(() => {});
}
console.log(JSON.stringify(report, null, 2));
if (!report.pass) Deno.exit(1);
if (!report.pass) process.exit(1);
+8
View File
@@ -0,0 +1,8 @@
#!/bin/sh
set -eu
cd "$(dirname "$0")/.."
[ "$(uname -s)" = Linux ] || { echo '解析沙箱目前需要 Linux seccomp' >&2; exit 1; }
mkdir -p dist
cc -O2 -fPIC -shared -Wall -Wextra -Werror -Wno-unused-parameter \
-I "${NODE_INCLUDE_DIR:-/usr/include/node}" \
apps/server/src/sandbox/native/policy.c -o dist/sandbox-policy.node
+19 -14
View File
@@ -1,25 +1,30 @@
#!/bin/sh
# 编译产物自检(deno task ci 的最后一步):deno compile 的 --exclude-unused-npm 可能漏嵌 npm 包/子路径,
# 开发态与测试全绿但二进制启动即 ERR_MODULE_NOT_FOUND。这里用空环境跑 `--role preflight`:
# 它会先加载完整的服务端模块图(db/任务引擎/全部模块),然后才因缺配置以退出码 2 结束;
# 模块缺失会以退出码 1 + ERR_MODULE_NOT_FOUND 结束。
# 编译产物必须在空环境、其它工作目录下加载完整模块图与解析沙箱。
# preflight 缺配置以 2 退出;模块图加载失败以 1 退出。
set -u
bin="${1:-dist/umt-server}"
case "$bin" in /*) ;; *) bin="$PWD/$bin" ;; esac
[ -x "$bin" ] || { echo "check-compiled-binary: 找不到编译产物 $bin(先跑 deno task build)" >&2; exit 1; }
[ -x "$bin" ] || { echo "check-compiled-binary: 找不到编译产物 $bin(先跑 bun run build)" >&2; exit 1; }
out=$(cd /tmp && env -i PATH="$PATH" "$bin" --role preflight 2>&1)
code=$?
if [ "$code" -eq 2 ] && printf '%s' "$out" | grep -q '配置无效'; then
# 沙箱子进程:与 sandbox/client.ts 同样的环境(只有 PATH)拉起 `--role sandbox`,请求一个不存在的任务,
# 应回一行 {"ok":false,...};入口就崩(如 --allow-run 解析失败)会没有这行
sb=$(cd /tmp && printf '{"job":"__check__","input":null}' | env -i PATH="$PATH" "$bin" --role sandbox 2>&1)
if printf '%s' "$sb" | grep -q '"ok":false'; then
echo "check-compiled-binary: ok(模块图加载完整,沙箱子进程能启动)"
exit 0
# 正常任务与拒绝策略都必须在空环境、/tmp 下成立。
sb=$(cd /tmp && printf '{"id":"1","task":"self.test","input":{"mode":"ok"}}\n' | env -i PATH="$PATH" "$bin" --role sandbox 2>&1)
if ! printf '%s' "$sb" | grep -q '"type":"result"'; then
echo "check-compiled-binary: 沙箱子进程起不来:$sb" >&2
exit 1
fi
echo "check-compiled-binary: 沙箱子进程起不来:" >&2
printf '%s\n' "$sb" >&2
exit 1
for mode in io async-io write net exec; do
sb=$(cd /tmp && printf '{"id":"1","task":"self.test","input":{"mode":"%s"}}\n' "$mode" | env -i PATH="$PATH" "$bin" --role sandbox 2>&1)
# policy.lockdown 已直接核验 socket/openat/execve 返回 EPERM。
# Bun 编译产物的网络异常可能被归一化成 ECONNREFUSED。
if ! printf '%s' "$sb" | grep -q '"type":"error"' || ! printf '%s' "$sb" | grep -Eiq 'EPERM|operation not permitted|ECONNREFUSED'; then
echo "check-compiled-binary: 沙箱没有拒绝 $mode:$sb" >&2
exit 1
fi
done
echo "check-compiled-binary: ok(模块图加载完整,沙箱拒绝文件访问/网络/执行程序)"
exit 0
fi
echo "check-compiled-binary: 编译产物自检失败(退出码 $code):" >&2
printf '%s\n' "$out" >&2
+6 -6
View File
@@ -1,16 +1,16 @@
#!/usr/bin/env -S deno run -A
#!/usr/bin/env -S bun
// 文案标点与用词检查(规范见 docs/design/glossary.md「标点」与 .claude/skills/human-copy)。只看源码里的字符串字面量、模板字符串与 JSX 文本,
// 不看注释、类型字面量、对象键、比较/匹配用的字符串(includes / split / === 等)和像正则的字符串。
//
// deno run -A scripts/check-copy-punctuation.mjs # 报告,warn 模式(退出码 0)
// deno run -A scripts/check-copy-punctuation.mjs --strict # 有违规退出码 1
// deno run -A scripts/check-copy-punctuation.mjs --fix [文件…] # 自动改能确定的项(标点、省略号、括号、引号、空格)
// deno run -A scripts/check-copy-punctuation.mjs --by-file # 按文件汇总违规数
// bun scripts/check-copy-punctuation.mjs # 报告,warn 模式(退出码 0)
// bun scripts/check-copy-punctuation.mjs --strict # 有违规退出码 1
// bun scripts/check-copy-punctuation.mjs --fix [文件…] # 自动改能确定的项(标点、省略号、括号、引号、空格)
// bun scripts/check-copy-punctuation.mjs --by-file # 按文件汇总违规数
//
// 例外:字符串所在行或上一行写 `copy-allow: 原因`;整个文件在前 5 行写 `copy-allow-file: 原因`。
import { readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
import { join, relative } from "node:path";
import { parse } from "npm:@babel/parser@7.29.9";
import { parse } from "@babel/parser";
const ROOT = new URL("..", import.meta.url).pathname;
const SCOPES = ["apps/web/app", "apps/server/src", "packages/contracts/src"];
+44
View File
@@ -0,0 +1,44 @@
import { cp, mkdir, readdir, readFile } from "node:fs/promises";
import { resolve } from "node:path";
import { fileURLToPath } from "node:url";
const root = fileURLToPath(new URL("..", import.meta.url));
process.chdir(root);
const native = Bun.spawnSync(["sh", "scripts/build-sandbox.sh"], { stdout: "inherit", stderr: "inherit" });
if (native.exitCode !== 0) process.exit(native.exitCode);
await mkdir("dist", { recursive: true });
const migrations = (await readdir("packages/db/migrations")).filter((name) => name.endsWith(".ts")).sort();
const result = await Bun.build({
entrypoints: ["apps/server/src/main.ts"],
compile: { outfile: "dist/umt-server", autoloadDotenv: false, autoloadBunfig: false },
target: "bun",
plugins: [
{
name: "embedded-migrations",
setup(build) {
build.onLoad({ filter: /packages\/db\/src\/migrator\.ts$/ }, async ({ path }) => {
const source = await readFile(path, "utf8");
const imports = migrations
.map(
(name, i) =>
`import * as migration${i} from ${JSON.stringify(resolve(root, "packages/db/migrations", name))};`,
)
.join("\n");
const entries = migrations
.map((name, i) => `${JSON.stringify(name.slice(0, -3))}: migration${i}`)
.join(",\n");
return {
contents: `${imports}\n${source.replace("new FileMigrationProvider({ fs, path, migrationFolder: MIGRATIONS_DIR })", `{ getMigrations: async () => ({${entries}}) }`)}`,
loader: "ts",
};
});
},
},
],
});
if (!result.success) {
for (const log of result.logs) console.error(log);
process.exit(1);
}
await cp("packages/agent/skills", "dist/skills", { recursive: true, force: true });
console.log("Built dist/umt-server, dist/sandbox-policy.node and dist/skills");
+8 -8
View File
@@ -3,9 +3,9 @@
// 默认只读(dry-run),输出每张表将删的行数;--apply 才删除(分批、每批单独事务,删得多的表随后 ANALYZE)。
//
// 用法(DATABASE_URL 必须显式给出,脚本不带默认库):
// DATABASE_URL=postgres://umt:***@<host>:5432/umt deno run -A scripts/db-retention.ts [--dry-run] # 看每张表将删多少行
// DATABASE_URL=… deno run -A scripts/db-retention.ts --apply # 执行
// DATABASE_URL=… deno run -A scripts/db-retention.ts --only task_run,pipeline [--apply] # 只处理部分目标
// DATABASE_URL=postgres://umt:***@<host>:5432/umt bun scripts/db-retention.ts [--dry-run] # 看每张表将删多少行
// DATABASE_URL=… bun scripts/db-retention.ts --apply # 执行
// DATABASE_URL=… bun scripts/db-retention.ts --only task_run,pipeline [--apply] # 只处理部分目标
// ENGINE_SCHEMA=openworkflow(缺省)
//
// 不包含:进程心跳(system.heartbeat_cleanup)、图片缓存与 kv_cache(cache 模块)、ANALYZE 任务(--apply 结束时对删过的表各自 ANALYZE)。
@@ -15,20 +15,20 @@ import { withStorageDefaults } from "@umt/contracts";
import { createDb, sql } from "@umt/db";
import { previewJob, RETENTION_JOBS } from "../apps/server/src/modules/system/retention.ts";
const argv = [...Deno.args];
const argv = [...process.argv.slice(2)];
const APPLY = argv.includes("--apply");
if (APPLY && argv.includes("--dry-run")) {
console.error("--apply 与 --dry-run 不能同时给出");
Deno.exit(2);
process.exit(2);
}
const onlyArg = argv.indexOf("--only");
const only = onlyArg >= 0 ? new Set((argv[onlyArg + 1] ?? "").split(",").filter(Boolean)) : null;
const url = Deno.env.get("DATABASE_URL");
const url = process.env.DATABASE_URL;
if (!url) {
console.error("需要 DATABASE_URL(脚本不带默认库)");
Deno.exit(2);
process.exit(2);
}
const engineSchema = Deno.env.get("ENGINE_SCHEMA") ?? "openworkflow";
const engineSchema = process.env.ENGINE_SCHEMA ?? "openworkflow";
const db = createDb({ connectionString: url, max: 2 });
try {
+3 -3
View File
@@ -1,9 +1,9 @@
// 开发库清理:只针对 55433 开发库(docker umt-dev-postgres-1),清掉测试残留。
//
// 用法:
// deno run -A scripts/dev-db-clean.ts # 同 --dry-run:只打印清单,不改库
// deno run -A scripts/dev-db-clean.ts --yes # 真正删除
// DATABASE_URL=postgres://umt:umt@localhost:55433/umt deno run -A scripts/dev-db-clean.ts --dry-run
// bun scripts/dev-db-clean.ts # 同 --dry-run:只打印清单,不改库
// bun scripts/dev-db-clean.ts --yes # 真正删除
// DATABASE_URL=postgres://umt:umt@localhost:55433/umt bun scripts/dev-db-clean.ts --dry-run
//
// 清理范围:
// 1. 测试 schema:名字匹配 TEST_SCHEMA_PATTERNS(各测试 harness 建的带 pid+随机后缀 schema)。
+9 -9
View File
@@ -1,11 +1,11 @@
// 身份数据清理(FIX-5):删除冲突别名、合并重复的内容单元、删除孤立的临时内容单元。默认只读(dry-run),加 --apply 才改库。
//
// 用法(DATABASE_URL 必须显式给出,脚本不带默认库):
// DATABASE_URL=postgres://umt:***@<host>:5432/umt deno run -A scripts/identity-cleanup.ts [--dry-run] # 看清单(按类别列删除 / 保留)
// DATABASE_URL=… deno run -A scripts/identity-cleanup.ts --apply # 执行(单个事务)
// DATABASE_URL=… deno run -A scripts/identity-cleanup.ts --keep-orphans # 不删孤立单元
// DATABASE_URL=… deno run -A scripts/identity-cleanup.ts --entity <作品 id> # 只看 / 只处理一部作品
// DATABASE_URL=… deno run -A scripts/identity-cleanup.ts --merge-duplicates [--apply] # 只做第 2 步:合并重复单元(DH-1)
// DATABASE_URL=postgres://umt:***@<host>:5432/umt bun scripts/identity-cleanup.ts [--dry-run] # 看清单(按类别列删除 / 保留)
// DATABASE_URL=… bun scripts/identity-cleanup.ts --apply # 执行(单个事务)
// DATABASE_URL=… bun scripts/identity-cleanup.ts --keep-orphans # 不删孤立单元
// DATABASE_URL=… bun scripts/identity-cleanup.ts --entity <作品 id> # 只看 / 只处理一部作品
// DATABASE_URL=… bun scripts/identity-cleanup.ts --merge-duplicates [--apply] # 只做第 2 步:合并重复单元(DH-1)
//
// 1. 冲突别名:只处理自动识别登记的站点别名(title_alias.source = 'site',作品级)。以下任一成立即删除:
// - shared:同一归一化别名还挂在别的作品上,且它不是本作品自己的名字(主标题 / 原标题 / TMDB 译名与别名);
@@ -32,20 +32,20 @@ import {
type UnitMergeGroup,
} from "../apps/server/src/modules/sources-identity/service/unit-merge.ts";
const argv = [...Deno.args];
const argv = [...process.argv.slice(2)];
const APPLY = argv.includes("--apply");
if (APPLY && argv.includes("--dry-run")) {
console.error("--apply 与 --dry-run 不能同时给出");
Deno.exit(2);
process.exit(2);
}
const MERGE_ONLY = argv.includes("--merge-duplicates");
const KEEP_ORPHANS = argv.includes("--keep-orphans") || MERGE_ONLY;
const entityArg = argv.indexOf("--entity");
const ONLY_ENTITY = entityArg >= 0 ? (argv[entityArg + 1] ?? null) : null;
const url = Deno.env.get("DATABASE_URL");
const url = process.env.DATABASE_URL;
if (!url) {
console.error("需要 DATABASE_URL(脚本不带默认库)");
Deno.exit(2);
process.exit(2);
}
const db = new pg.Client({ connectionString: url });
await db.connect();
+6 -6
View File
@@ -1,9 +1,9 @@
// 身份识别重评(ID-2):匹配规则更新后,处理存量的待识别事项与错误映射。默认只读(dry-run),加 --apply 才改库。
//
// 用法(DATABASE_URL 必须显式给出,脚本不带默认库):
// DATABASE_URL=postgres://umt:***@<host>:5432/umt deno run -A scripts/identity-reevaluate.ts
// DATABASE_URL=… deno run -A scripts/identity-reevaluate.ts --rollback-wrong # 另查分类词别名造成的错误映射
// DATABASE_URL=… deno run -A scripts/identity-reevaluate.ts --rollback-wrong --apply # 真正执行
// DATABASE_URL=postgres://umt:***@<host>:5432/umt bun scripts/identity-reevaluate.ts
// DATABASE_URL=… bun scripts/identity-reevaluate.ts --rollback-wrong # 另查分类词别名造成的错误映射
// DATABASE_URL=… bun scripts/identity-reevaluate.ts --rollback-wrong --apply # 真正执行
//
// 1. open 的 identity_ambiguous:按新解析规则重新解析每个成员资源并给出预估(只用池里存的候选,不查 TMDB);
// --apply 时把这些池条目的 next_review_at 置为现在、attempts 清零,由运行中的服务(已部署新规则)的池复查
@@ -25,13 +25,13 @@ import {
} from "@umt/domain";
import pg from "pg";
const args = new Set(Deno.args);
const args = new Set(process.argv.slice(2));
const APPLY = args.has("--apply");
const ROLLBACK = args.has("--rollback-wrong");
const url = Deno.env.get("DATABASE_URL");
const url = process.env.DATABASE_URL;
if (!url) {
console.error("需要 DATABASE_URL(脚本不带默认库)");
Deno.exit(2);
process.exit(2);
}
const db = new pg.Client({ connectionString: url });
await db.connect();
+4 -4
View File
@@ -1,6 +1,6 @@
# scripts/mp-sync
跟随上游移植工具(说明与对照总表:`docs/porting/mp-sync-ledger.md`)。用 `deno run -A` 运行(`yaml` 经根 deno.json import map 解析),不依赖 deno.json 任务。
跟随上游移植工具(说明与对照总表:`docs/porting/mp-sync-ledger.md`)。用 `bun` 运行(`yaml` 经根 package.json 依赖解析),不依赖任务包装。
| 脚本 | 作用 |
| --- | --- |
@@ -9,9 +9,9 @@
| `movieclaw-import.mjs [--repo <MC 仓库>] [--ref v0.27.0] [--check]` | MovieClaw v0.27.0(MIT)站点 YAML → `packages/adapters/src/sites/definitions/*.json` + `manifest.generated.ts`;覆盖层 `movieclaw-overrides/<id>.json`;`--check` 只比较 |
```bash
deno run -A scripts/mp-sync/check.mjs --to <新提交>
deno run -A scripts/mp-sync/build-ledger.mjs --commit <新提交>
deno run -A scripts/mp-sync/movieclaw-import.mjs --check
bun scripts/mp-sync/check.mjs --to <新提交>
bun scripts/mp-sync/build-ledger.mjs --commit <新提交>
bun scripts/mp-sync/movieclaw-import.mjs --check
```
MovieClaw v0.27.0 之后为附条件许可:`movieclaw-import.mjs` 校验 ref→提交与 LICENSE=MIT,其他 ref 需 `--allow-ref` 并另行核对许可。
+2 -2
View File
@@ -1,8 +1,8 @@
#!/usr/bin/env -S deno run -A
#!/usr/bin/env -S bun
/**
* 生成 scripts/mp-sync/ledger.json 与 docs/porting/mp-sync-ledger.md 中的对照总表。
*
* deno run -A scripts/mp-sync/build-ledger.mjs [--mp /opt/moviepilot-plus/upstream/MoviePilot] [--commit e6820a8]
* bun scripts/mp-sync/build-ledger.mjs [--mp /opt/moviepilot-plus/upstream/MoviePilot] [--commit e6820a8]
*
* 数据来源:源码文件头 `@mp-source <上游路径> @ <提交>` 标签 + ledger.manual.json(未打标签的既有移植)。
* 每条记录保存移植时的上游提交与该文件内容 sha256,供 check.mjs 比较。
+3 -3
View File
@@ -1,8 +1,8 @@
#!/usr/bin/env -S deno run -A
#!/usr/bin/env -S bun
/**
* 比较 ledger.json 与 MoviePilot 目标提交,列出需要跟随移植的上游变化。
*
* deno run -A scripts/mp-sync/check.mjs [--mp <MP 仓库路径>] [--to <目标提交,默认 HEAD>] [--json]
* bun scripts/mp-sync/check.mjs [--mp <MP 仓库路径>] [--to <目标提交,默认 HEAD>] [--json]
*
* 输出:已变更 / 已删除 / 未变化 的 ledger 文件,监视目录(spider/、parser/)中 ledger 未收录的新文件,
* 以及逐文件 `git diff` 命令。存在变化时退出码为 1(可用于 CI 或定时提醒)。
@@ -59,7 +59,7 @@ if (args.json) {
console.log(`未变化:${unchanged.length}`);
if (changed.length || deleted.length || added.length) {
console.log(
"\n处理流程:按 diff 增量移植 → 把对应源码文件头 `@mp-source ... @ <提交>` 改为新提交(新文件新增标签)→ deno run -A scripts/mp-sync/build-ledger.mjs --commit <新提交> → 测试。",
"\n处理流程:按 diff 增量移植 → 把对应源码文件头 `@mp-source ... @ <提交>` 改为新提交(新文件新增标签)→ bun scripts/mp-sync/build-ledger.mjs --commit <新提交> → 测试。",
);
}
}
+2 -2
View File
@@ -13,7 +13,7 @@ meta:读 packages/domain/test/differential/meta-samples.json,分别以
accelerator=None(Python 参考实现)与 accelerator=rust(moviepilot_rust 快路径)
调用 MetaInfo / MetaInfoPath,写 meta-mp.json。两条路径不一致的样本记在 rustMismatch。
生成后执行 `deno task fmt`(biome 统一 JSON 格式),再跑差分测试;新出现的差异需人工分类后
生成后执行 `bun run fmt`(biome 统一 JSON 格式),再跑差分测试;新出现的差异需人工分类后
登记进对应 *-deviations.json / deviations 字段(kind 见 docs/porting/mp-rust-logic-parity.md)。
"""
from __future__ import annotations
@@ -354,7 +354,7 @@ def gen_filter_cmd() -> None:
# parse_indexer_subtitles` 打桩为 None 强制走 PyQuery 回退;再恢复原函数跑一次记录 `mp_rust`)。
# MP 自带站点定义在加密资源 `user.sites.v3.bin`(Cython `SitesHelper`),未认证用户 `get_indexer()` 返回 {},
# `get_indexsites()` 只含 id/name/url,因此 indexer 配置改用本项目 `definitions/*.json`(同 MP schema)。
# 输入先由 `deno run -A packages/adapters/test/differential/export-inputs.ts` 导出 tk-* fixture。
# 输入先由 `bun packages/adapters/test/differential/export-inputs.ts` 导出 tk-* fixture。
UMT_ROOT = REPO
DEFAULT_OUT = UMT_ROOT / "packages/adapters/test/differential"
+2 -2
View File
@@ -1,9 +1,9 @@
#!/usr/bin/env -S deno run -A
#!/usr/bin/env -S bun
/**
* MovieClaw v0.27.0(MIT)站点 YAML → 本项目 SiteDefinition JSON 转换器(可重复执行)。
*
* 用法:
* deno run -A scripts/mp-sync/movieclaw-import.mjs [--repo spikes/movieclaw-src/repo] [--ref v0.27.0] [--check]
* bun scripts/mp-sync/movieclaw-import.mjs [--repo spikes/movieclaw-src/repo] [--ref v0.27.0] [--check]
*
* - 只用 `git show <ref>:<path>` 读取文件(固定 tag,不读工作区,避免混入许可切换后的内容)。
* - 校验 <ref> 解析到的提交与 LICENSE 为 MIT;ref 不是 v0.27.0 时要求 --allow-ref(许可需另行核对)。
+8 -8
View File
@@ -3,10 +3,10 @@
// 默认只读(dry-run),加 --apply 才改库(单个事务,写一条审计 metadata.overview_recompute)。
//
// 用法(DATABASE_URL 必须显式给出,脚本不带默认库):
// DATABASE_URL=postgres://umt:***@<host>:5432/umt deno run -A scripts/overview-recompute.ts # 看清单
// DATABASE_URL=… deno run -A scripts/overview-recompute.ts --apply # 写库
// DATABASE_URL=… deno run -A scripts/overview-recompute.ts --entity <作品 id> # 只看 / 只处理一部作品
// DATABASE_URL=… deno run -A scripts/overview-recompute.ts --json # 清单输出 JSON
// DATABASE_URL=postgres://umt:***@<host>:5432/umt bun scripts/overview-recompute.ts # 看清单
// DATABASE_URL=… bun scripts/overview-recompute.ts --apply # 写库
// DATABASE_URL=… bun scripts/overview-recompute.ts --entity <作品 id> # 只看 / 只处理一部作品
// DATABASE_URL=… bun scripts/overview-recompute.ts --json # 清单输出 JSON
//
// 语言链与"简介是否允许回退到非中文"取库里的设置 metadata.languages / metadata.overviewNonChineseFallback。
// 规则与实现见 apps/server/src/modules/sources-identity/service/overview-recompute.ts、docs/runbook/metadata.md。
@@ -21,19 +21,19 @@ import {
planOverviewRecompute,
} from "../apps/server/src/modules/sources-identity/service/overview-recompute.ts";
const argv = [...Deno.args];
const argv = [...process.argv.slice(2)];
const APPLY = argv.includes("--apply");
const JSON_OUT = argv.includes("--json");
if (APPLY && argv.includes("--dry-run")) {
console.error("--apply 与 --dry-run 不能同时给出");
Deno.exit(2);
process.exit(2);
}
const entityArg = argv.indexOf("--entity");
const ONLY_ENTITY = entityArg >= 0 ? (argv[entityArg + 1] ?? null) : null;
const url = Deno.env.get("DATABASE_URL");
const url = process.env.DATABASE_URL;
if (!url) {
console.error("需要 DATABASE_URL(脚本不带默认库)");
Deno.exit(2);
process.exit(2);
}
const db = new Kysely<DB>({ dialect: new PostgresDialect({ pool: new pg.Pool({ connectionString: url, max: 2 }) }) });
+4 -4
View File
@@ -25,11 +25,11 @@ die() { echo "✗ $*" >&2; exit 1; }
[ -f "$ENV_FILE" ] || die "缺少 $ENV_FILE"
[ -z "$(git status --porcelain)" ] || die "工作区有未提交的改动,先提交再发布(git status)"
version=$(deno eval --quiet 'console.log(JSON.parse(Deno.readTextFileSync("deno.json")).version)')
version=$(bun -e 'console.log(JSON.parse(require("node:fs").readFileSync("package.json")).version)')
tag="v$version"
for f in apps/*/deno.json apps/web/package.json packages/*/deno.json; do
v=$(deno eval --quiet "console.log(JSON.parse(Deno.readTextFileSync('$f')).version ?? '')")
[ -z "$v" ] || [ "$v" = "$version" ] || die "$f 版本 $v 与根 deno.json $version 不一致"
for f in apps/*/package.json packages/*/package.json; do
v=$(bun -e "console.log(JSON.parse(require("node:fs").readFileSync('$f')).version ?? '')")
[ -z "$v" ] || [ "$v" = "$version" ] || die "$f 版本 $v 与根 package.json $version 不一致"
done
grep -q "^## \[$version\]" CHANGELOG.md || die "CHANGELOG.md 还没有 ## [$version] 条目"
+7 -11
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
# 全量测试:逐个包跑完(不在第一个失败处停),最后汇总;连不上测试库直接失败,不让数据库用例静默跳过。
# 用法:sh scripts/test-all.sh [包目录...] 缺省跑全部 workspace 成员
# web 用 Node 跑 vitest(Deno 跑 web 的 vitest 有兼容问题)。
# 全部包使用 Bun 运行 Vitest。
set -u
cd "$(dirname "$0")/.."
ROOT=$PWD
@@ -10,17 +10,17 @@ if [ -f .env ]; then set -a; . ./.env; set +a; fi
DB_URL=${DATABASE_URL:-postgres://umt:umt@localhost:55433/umt}
if [ "${UMT_SKIP_DB_TESTS:-}" != "1" ]; then
if ! deno eval --quiet "
import pg from 'npm:[email protected]';
const c = new pg.Client({ connectionString: '$DB_URL', connectionTimeoutMillis: 3000 });
if ! DATABASE_URL="$DB_URL" bun -e "
import pg from 'pg';
const c = new pg.Client({ connectionString: process.env.DATABASE_URL, connectionTimeoutMillis: 3000 });
await c.connect(); await c.end();" >/dev/null 2>&1; then
echo "✗ 连不上测试库($DB_URL)。先 deno task dev:up;确实不跑数据库用例时设 UMT_SKIP_DB_TESTS=1" >&2
echo "✗ 连不上测试库($DB_URL)。先 bun run dev:up;确实不跑数据库用例时设 UMT_SKIP_DB_TESTS=1" >&2
exit 2
fi
fi
members=("$@")
[ ${#members[@]} -eq 0 ] && members=(packages/adapters packages/agent packages/contracts packages/db packages/domain
[ ${#members[@]} -eq 0 ] && members=(packages/runtime packages/adapters packages/agent packages/contracts packages/db packages/domain
packages/mapping packages/tasks packages/testkit apps/server apps/web)
LOG_DIR=${TEST_LOG_DIR:-$ROOT/.local/test-logs}
@@ -30,11 +30,7 @@ summary=()
for m in "${members[@]}"; do
log="$LOG_DIR/$(echo "$m" | tr / -).log"
printf '▶ %-20s ' "$m"
if [ "$m" = apps/web ]; then
(cd "$m" && "$ROOT/node_modules/.bin/vitest" run) >"$log" 2>&1
else
(cd "$m" && deno run -A npm:vitest run) >"$log" 2>&1
fi
(cd "$m" && bun --bun "$ROOT/node_modules/vitest/vitest.mjs" run) >"$log" 2>&1
code=$?
line=$(sed 's/\x1b\[[0-9;]*m//g' "$log" | grep -E '^\s+Tests ' | tail -1 | tr -s ' ')
if [ $code -eq 0 ]; then echo "✓ ${line:-}"; else echo "✗ ${line:-退出码 $code}"; failed+=("$m"); fi
+9 -9
View File
@@ -3,10 +3,10 @@
// 默认只读(dry-run),加 --apply 才删(单个事务,写一条审计 metadata.tmdb_alias_language_cleanup)。
//
// 用法(DATABASE_URL 必须显式给出,脚本不带默认库):
// DATABASE_URL=postgres://umt:***@<host>:5432/umt deno run -A scripts/tmdb-alias-language-cleanup.ts # 看统计与样例
// DATABASE_URL=… deno run -A scripts/tmdb-alias-language-cleanup.ts --apply # 删除
// DATABASE_URL=… deno run -A scripts/tmdb-alias-language-cleanup.ts --entity <作品 id> # 只看 / 只处理一部作品
// DATABASE_URL=… deno run -A scripts/tmdb-alias-language-cleanup.ts --samples 20 # 每类样例条数(缺省 5)
// DATABASE_URL=postgres://umt:***@<host>:5432/umt bun scripts/tmdb-alias-language-cleanup.ts # 看统计与样例
// DATABASE_URL=… bun scripts/tmdb-alias-language-cleanup.ts --apply # 删除
// DATABASE_URL=… bun scripts/tmdb-alias-language-cleanup.ts --entity <作品 id> # 只看 / 只处理一部作品
// DATABASE_URL=… bun scripts/tmdb-alias-language-cleanup.ts --samples 20 # 每类样例条数(缺省 5)
//
// 别名(title_alias)判定——只动 source = 'tmdb' 且未锁定的行;人工(user)/ 站点(site)/ 智能体(agent)/ NFO / 导入来源与
// 锁定的行一律不动:
@@ -24,11 +24,11 @@ import { allowedLanguage, untaggedTextAllowed } from "@umt/adapters";
import { normalizeTitle } from "@umt/domain";
import pg from "pg";
const argv = [...Deno.args];
const argv = [...process.argv.slice(2)];
const APPLY = argv.includes("--apply");
if (APPLY && argv.includes("--dry-run")) {
console.error("--apply 与 --dry-run 不能同时给出");
Deno.exit(2);
process.exit(2);
}
const argOf = (k: string) => {
const i = argv.indexOf(k);
@@ -36,10 +36,10 @@ const argOf = (k: string) => {
};
const ONLY_ENTITY = argOf("--entity");
const SAMPLES = Number(argOf("--samples") ?? 5) || 5;
const url = Deno.env.get("DATABASE_URL");
const url = process.env.DATABASE_URL;
if (!url) {
console.error("需要 DATABASE_URL(脚本不带默认库)");
Deno.exit(2);
process.exit(2);
}
const db = new pg.Client({ connectionString: url });
await db.connect();
@@ -179,7 +179,7 @@ if (!APPLY) {
await db.query("rollback");
console.error("执行失败,已回滚:", (e as Error).message);
await db.end();
Deno.exit(1);
process.exit(1);
}
} else {
console.log(`\n没有要删除的别名`);