chore: release v1.39.0

This commit is contained in:
coso
2026-05-15 02:38:18 +08:00
parent 16d8bb5053
commit af6133164f
334 changed files with 44460 additions and 27233 deletions
+118 -46
View File
@@ -202,6 +202,17 @@ function readImageLaunch(metadata) {
return { launch, imageTask };
}
function isImageGenerateSkillExecution(invoke) {
return (
invoke?.cmd === "execute_skill" &&
invoke.args?.skillName === "image_generate"
);
}
function isLegacyImageRuntimeSubmit(invoke) {
return invoke?.cmd === "agent_runtime_submit_turn";
}
function parseInvokeRequest(request, invokeUrl) {
const requestUrl = request.url();
const normalizedInvokeUrl = invokeUrl.replace(/\/$/, "");
@@ -507,59 +518,110 @@ async function main() {
});
const submitInvoke = await waitForCondition(
"等待 @配图 提交 agent_runtime_submit_turn",
"等待 @配图 提交 image_generate 执行请求",
() =>
invokes
.slice(submitStart)
.find((item) => item.cmd === "agent_runtime_submit_turn") || null,
.find(
(item) =>
isImageGenerateSkillExecution(item) ||
isLegacyImageRuntimeSubmit(item),
) || null,
options.timeoutMs,
options.intervalMs,
);
const request = submitInvoke.args?.request || {};
const turnConfig = request.turn_config || {};
const metadata = turnConfig.metadata || {};
const { launch, imageTask } = readImageLaunch(metadata);
const runtimeContract = readNestedObject(imageTask, [
"runtime_contract",
"runtimeContract",
]);
const contractKey =
imageTask?.modality_contract_key ||
imageTask?.modalityContractKey ||
runtimeContract?.contract_key ||
runtimeContract?.contractKey;
assert(launch, "@配图 提交缺少 harness.image_skill_launch");
assert(imageTask, "@配图 提交缺少 image_task");
assert(
String(imageTask.prompt || "").includes(IMAGE_PROMPT),
"@配图 image_task.prompt 未保留用户输入",
);
assert(
contractKey === "image_generation",
`@配图 image_task 合同应为 image_generation,实际为 ${String(contractKey)}`,
);
assert(
turnConfig.provider_preference == null,
"@配图 不应把当前聊天 provider 提交为 request provider_preference",
);
assert(
turnConfig.model_preference == null,
"@配图 不应把当前聊天 model 提交为 request model_preference",
);
if (isImageGenerateSkillExecution(submitInvoke)) {
const args = submitInvoke.args || {};
const userInput = String(args.userInput || "");
assert(
userInput.includes("@配图"),
"@配图 execute_skill.userInput 应保留命令标签",
);
assert(
userInput.includes(IMAGE_PROMPT),
"@配图 execute_skill.userInput 未保留用户输入",
);
assert(
typeof args.executionId === "string" && args.executionId.trim(),
"@配图 execute_skill 缺少 executionId",
);
assert(
typeof args.sessionId === "string" && args.sessionId.trim(),
"@配图 execute_skill 缺少 sessionId",
);
summary.assertions.imageSkillLaunchSubmitted = true;
summary.assertions.imageGenerationContractPreserved = true;
summary.assertions.chatModelPreferenceSuppressed = true;
summary.submitRequest = {
message: request.message,
sessionId: request.session_id || null,
turnId: request.turn_id || null,
providerPreference: turnConfig.provider_preference ?? null,
modelPreference: turnConfig.model_preference ?? null,
contractKey,
imagePrompt: imageTask.prompt,
};
await page
.getByText("先执行技能 image_generate", { exact: false })
.first()
.waitFor({ state: "visible", timeout: options.timeoutMs });
await page
.getByText("图片生成", { exact: false })
.first()
.waitFor({ state: "visible", timeout: options.timeoutMs });
summary.assertions.imageSkillExecutionSubmitted = true;
summary.assertions.imageCommandTagPreserved = true;
summary.assertions.imagePromptPreserved = true;
summary.assertions.imageGenerateProcessVisible = true;
summary.submitRequest = {
routeMode: "skill_execution",
command: submitInvoke.cmd,
skillName: args.skillName,
userInput,
sessionId: args.sessionId || null,
executionId: args.executionId || null,
providerOverride: args.providerOverride ?? null,
modelOverride: args.modelOverride ?? null,
};
} else {
const request = submitInvoke.args?.request || {};
const turnConfig = request.turn_config || {};
const metadata = turnConfig.metadata || {};
const { launch, imageTask } = readImageLaunch(metadata);
const runtimeContract = readNestedObject(imageTask, [
"runtime_contract",
"runtimeContract",
]);
const contractKey =
imageTask?.modality_contract_key ||
imageTask?.modalityContractKey ||
runtimeContract?.contract_key ||
runtimeContract?.contractKey;
assert(launch, "@配图 提交缺少 harness.image_skill_launch");
assert(imageTask, "@配图 提交缺少 image_task");
assert(
String(imageTask.prompt || "").includes(IMAGE_PROMPT),
"@配图 image_task.prompt 未保留用户输入",
);
assert(
contractKey === "image_generation",
`@配图 image_task 合同应为 image_generation,实际为 ${String(contractKey)}`,
);
assert(
turnConfig.provider_preference == null,
"@配图 不应把当前聊天 provider 提交为 request provider_preference",
);
assert(
turnConfig.model_preference == null,
"@配图 不应把当前聊天 model 提交为 request model_preference",
);
summary.assertions.imageSkillLaunchSubmitted = true;
summary.assertions.imageGenerationContractPreserved = true;
summary.assertions.chatModelPreferenceSuppressed = true;
summary.submitRequest = {
routeMode: "agent_runtime_submit_turn",
message: request.message,
sessionId: request.session_id || null,
turnId: request.turn_id || null,
providerPreference: turnConfig.provider_preference ?? null,
modelPreference: turnConfig.model_preference ?? null,
contractKey,
imagePrompt: imageTask.prompt,
};
}
await page.screenshot({
path: path.join(
@@ -569,7 +631,17 @@ async function main() {
fullPage: true,
});
summary.cleanup = await interruptSubmittedTurn(options, request);
if (summary.submitRequest?.routeMode === "agent_runtime_submit_turn") {
summary.cleanup = await interruptSubmittedTurn(
options,
submitInvoke.args?.request || {},
);
} else {
summary.cleanup = {
attempted: false,
reason: "skill-execution-route",
};
}
const summaryPath = path.join(
options.evidenceDir,
+58
View File
@@ -312,10 +312,68 @@ function collectDefaultMockCommands() {
for (const match of objectBody.matchAll(/^ ([A-Za-z0-9_]+)\s*:/gm)) {
mockCommands.add(match[1]);
}
for (const spreadMatch of objectBody.matchAll(
/^ \.\.\.([A-Za-z0-9_]+),/gm,
)) {
const registryName = spreadMatch[1];
for (const command of collectSpreadMockRegistryCommands(
sourceCode,
registryName,
)) {
mockCommands.add(command);
}
}
return mockCommands;
}
function collectSpreadMockRegistryCommands(sourceCode, registryName) {
const importPattern =
/import\s+(?:type\s+)?\{([^}]*)\}\s+from\s+["'`]([^"'`]+)["'`]/g;
let importSource;
for (const importMatch of sourceCode.matchAll(importPattern)) {
const namedImports = importMatch[1];
if (new RegExp(`\\b${registryName}\\b`).test(namedImports)) {
importSource = importMatch[2];
break;
}
}
if (!importSource?.startsWith("./")) {
return [];
}
const registrySourcePath = path.resolve(
repoRoot,
"src/lib/tauri-mock",
`${importSource.replace(/^\.\//, "")}.ts`,
);
if (!fs.existsSync(registrySourcePath)) {
throw new Error(`未找到 spread mock registry 文件: ${registrySourcePath}`);
}
const registrySourceCode = fs.readFileSync(registrySourcePath, "utf8");
const markerPattern = new RegExp(
`export\\s+const\\s+${registryName}\\b[\\s\\S]*?=\\s*\\{`,
);
const markerMatch = markerPattern.exec(registrySourceCode);
if (!markerMatch) {
throw new Error(`未找到 spread mock registry 定义: ${registryName}`);
}
const braceStart = markerMatch.index + markerMatch[0].length - 1;
const registryBody = extractBalancedBlock(
registrySourceCode,
braceStart,
"{",
"}",
);
const commands = [];
for (const match of registryBody.matchAll(/^ ([A-Za-z0-9_]+)\s*:/gm)) {
commands.push(match[1]);
}
return commands;
}
function readAgentCommandCatalog() {
const catalogPath = path.join(
repoRoot,
+127 -2
View File
@@ -17,6 +17,28 @@ const PLATFORM_BY_TARGET_TRIPLE = {
"x86_64-pc-windows-msvc": "windows-x86_64",
};
const TARGET_TRIPLE_BY_PLATFORM = Object.fromEntries(
Object.entries(PLATFORM_BY_TARGET_TRIPLE).map(([targetTriple, platform]) => [
platform,
targetTriple,
]),
);
const INSTALLER_ASSET_BY_PLATFORM = {
"darwin-aarch64": {
required: true,
assetName: (version) => `Lime_${version}_aarch64.dmg`,
},
"darwin-x86_64": {
required: true,
assetName: (version) => `Lime_${version}_x64.dmg`,
},
"windows-x86_64": {
required: false,
assetName: (version) => `Lime_${version}_x64-setup.exe`,
},
};
function parseArgs(argv) {
const args = {};
for (let index = 0; index < argv.length; index += 1) {
@@ -176,6 +198,86 @@ function platformKeyFromAssetFile(assetsDir, assetFile) {
return PLATFORM_BY_TARGET_TRIPLE[targetTriple] || "";
}
function targetTripleFromAssetFile(assetsDir, assetFile) {
const relativeDir = path.relative(assetsDir, path.dirname(assetFile));
return relativeDir.split(path.sep).find(Boolean) || "";
}
function resolveInstallerAssetFile({
assetsDir,
filesByBasename,
platformKey,
referenceFile,
version,
}) {
const spec = INSTALLER_ASSET_BY_PLATFORM[platformKey];
if (!spec) {
return null;
}
const assetName = spec.assetName(version);
const candidates = filesByBasename.get(assetName) || [];
if (candidates.length === 0) {
if (spec.required) {
throw new Error(
`platform ${platformKey} is missing installer asset: ${assetName}`,
);
}
return null;
}
const expectedTargetTriple = TARGET_TRIPLE_BY_PLATFORM[platformKey];
return (
(referenceFile &&
candidates.find(
(candidate) => path.dirname(candidate) === path.dirname(referenceFile),
)) ||
candidates.find(
(candidate) =>
targetTripleFromAssetFile(assetsDir, candidate) === expectedTargetTriple,
) ||
candidates[0]
);
}
function addReferencedFile(referencedFiles, item) {
referencedFiles.set(item.targetPath, item);
}
function addInstallerUrlIfAvailable({
assetsDir,
baseUrl,
channel,
filesByBasename,
platform,
platformKey,
referenceFile,
referencedFiles,
version,
versionTag,
}) {
const installerFile = resolveInstallerAssetFile({
assetsDir,
filesByBasename,
platformKey,
referenceFile,
version,
});
if (!installerFile) {
return;
}
const installerName = path.basename(installerFile);
const installerAssetPath = buildPlatformAssetPath(platformKey, installerName);
platform.installer_url = `${baseUrl}/lime/${channel}/${versionTag}/${installerAssetPath}`;
addReferencedFile(referencedFiles, {
assetName: installerName,
file: installerFile,
platformKey,
targetPath: installerAssetPath,
});
}
function collectSignedUpdaterArtifacts(files, assetsDir) {
const fileSet = new Set(files);
return files
@@ -277,12 +379,24 @@ function collectUpdaterManifest(options) {
signature,
url: targetUrl,
};
referencedFiles.set(platformAssetPath, {
addReferencedFile(referencedFiles, {
assetName,
file: assetFile,
platformKey,
targetPath: platformAssetPath,
});
addInstallerUrlIfAvailable({
assetsDir,
baseUrl,
channel,
filesByBasename,
platform: platforms[platformKey],
platformKey,
referenceFile: manifestFile,
referencedFiles,
version,
versionTag,
});
}
}
} else {
@@ -306,12 +420,23 @@ function collectUpdaterManifest(options) {
signature: artifact.signature,
url: targetUrl,
};
referencedFiles.set(platformAssetPath, {
addReferencedFile(referencedFiles, {
assetName: artifact.assetName,
file: artifact.assetFile,
platformKey: artifact.platformKey,
targetPath: platformAssetPath,
});
addInstallerUrlIfAvailable({
assetsDir,
baseUrl,
channel,
filesByBasename,
platform: platforms[artifact.platformKey],
platformKey: artifact.platformKey,
referencedFiles,
version,
versionTag,
});
}
}
+82 -2
View File
@@ -26,9 +26,15 @@ describe("release updater manifest", () => {
const assetsDir = path.join(root, "release-assets");
writeFile(path.join(assetsDir, "aarch64-apple-darwin", "Lime.app.tar.gz"));
writeFile(
path.join(assetsDir, "aarch64-apple-darwin", "Lime_1.20.0_aarch64.dmg"),
);
writeFile(
path.join(assetsDir, "x86_64-apple-darwin", "Lime-x64.app.tar.gz"),
);
writeFile(
path.join(assetsDir, "x86_64-apple-darwin", "Lime_1.20.0_x64.dmg"),
);
writeFile(path.join(assetsDir, "x86_64-pc-windows-msvc", "Lime.nsis.zip"));
writeJson(path.join(assetsDir, "aarch64-apple-darwin", "latest.json"), {
@@ -78,7 +84,19 @@ describe("release updater manifest", () => {
expect(result.manifest.platforms["darwin-aarch64"].url).toBe(
"https://updates.limecloud.com/lime/stable/v1.20.0/darwin-aarch64/Lime.app.tar.gz",
);
expect(result.r2UploadPlan).toHaveLength(3);
expect(result.manifest.platforms["darwin-aarch64"].installer_url).toBe(
"https://updates.limecloud.com/lime/stable/v1.20.0/darwin-aarch64/Lime_1.20.0_aarch64.dmg",
);
expect(result.manifest.platforms["darwin-x86_64"].installer_url).toBe(
"https://updates.limecloud.com/lime/stable/v1.20.0/darwin-x86_64/Lime_1.20.0_x64.dmg",
);
expect(result.r2UploadPlan.map((item) => item.key).sort()).toEqual([
"lime/stable/v1.20.0/darwin-aarch64/Lime.app.tar.gz",
"lime/stable/v1.20.0/darwin-aarch64/Lime_1.20.0_aarch64.dmg",
"lime/stable/v1.20.0/darwin-x86_64/Lime-x64.app.tar.gz",
"lime/stable/v1.20.0/darwin-x86_64/Lime_1.20.0_x64.dmg",
"lime/stable/v1.20.0/windows-x86_64/Lime.nsis.zip",
]);
const output = writeOutputs(result, path.join(root, "out"), "stable");
expect(fs.existsSync(output.latestPath)).toBe(true);
@@ -91,6 +109,9 @@ describe("release updater manifest", () => {
);
const assetsDir = path.join(root, "release-assets");
writeFile(path.join(assetsDir, "aarch64-apple-darwin", "Lime.app.tar.gz"));
writeFile(
path.join(assetsDir, "aarch64-apple-darwin", "Lime_1.20.0_aarch64.dmg"),
);
writeJson(path.join(assetsDir, "aarch64-apple-darwin", "latest.json"), {
version: "1.20.0",
platforms: {
@@ -147,10 +168,18 @@ describe("release updater manifest", () => {
path.join(assetsDir, "aarch64-apple-darwin", "Lime.app.tar.gz"),
"arm",
);
writeFile(
path.join(assetsDir, "aarch64-apple-darwin", "Lime_1.20.0_aarch64.dmg"),
"arm-dmg",
);
writeFile(
path.join(assetsDir, "x86_64-apple-darwin", "Lime.app.tar.gz"),
"x64",
);
writeFile(
path.join(assetsDir, "x86_64-apple-darwin", "Lime_1.20.0_x64.dmg"),
"x64-dmg",
);
writeJson(path.join(assetsDir, "aarch64-apple-darwin", "latest.json"), {
version: "1.20.0",
@@ -185,15 +214,23 @@ describe("release updater manifest", () => {
expect(result.manifest.platforms["darwin-x86_64"].url).toBe(
"https://updates.limecloud.com/lime/stable/v1.20.0/darwin-x86_64/Lime.app.tar.gz",
);
expect(result.manifest.platforms["darwin-aarch64"].installer_url).toBe(
"https://updates.limecloud.com/lime/stable/v1.20.0/darwin-aarch64/Lime_1.20.0_aarch64.dmg",
);
expect(result.manifest.platforms["darwin-x86_64"].installer_url).toBe(
"https://updates.limecloud.com/lime/stable/v1.20.0/darwin-x86_64/Lime_1.20.0_x64.dmg",
);
expect(result.r2UploadPlan.map((item) => item.key).sort()).toEqual([
"lime/stable/v1.20.0/darwin-aarch64/Lime.app.tar.gz",
"lime/stable/v1.20.0/darwin-aarch64/Lime_1.20.0_aarch64.dmg",
"lime/stable/v1.20.0/darwin-x86_64/Lime.app.tar.gz",
"lime/stable/v1.20.0/darwin-x86_64/Lime_1.20.0_x64.dmg",
]);
expect(
result.r2UploadPlan
.map((item) => fs.readFileSync(item.file, "utf8"))
.sort(),
).toEqual(["arm", "x64"]);
).toEqual(["arm", "arm-dmg", "x64", "x64-dmg"]);
});
it("没有 latest.json 时应从 Tauri 签名产物生成清单", () => {
@@ -216,11 +253,17 @@ describe("release updater manifest", () => {
path.join(assetsDir, "aarch64-apple-darwin", "Lime.app.tar.gz.sig"),
encodedSignature,
);
writeFile(
path.join(assetsDir, "aarch64-apple-darwin", "Lime_1.20.0_aarch64.dmg"),
);
writeFile(path.join(assetsDir, "x86_64-apple-darwin", "Lime.app.tar.gz"));
writeFile(
path.join(assetsDir, "x86_64-apple-darwin", "Lime.app.tar.gz.sig"),
rawSignature,
);
writeFile(
path.join(assetsDir, "x86_64-apple-darwin", "Lime_1.20.0_x64.dmg"),
);
writeFile(
path.join(
assetsDir,
@@ -257,12 +300,49 @@ describe("release updater manifest", () => {
expect(result.manifest.platforms["windows-x86_64"].url).toBe(
"https://updates.limecloud.com/lime/stable/v1.20.0/windows-x86_64/Lime_1.20.0_x64-setup.exe",
);
expect(result.manifest.platforms["darwin-aarch64"].installer_url).toBe(
"https://updates.limecloud.com/lime/stable/v1.20.0/darwin-aarch64/Lime_1.20.0_aarch64.dmg",
);
expect(result.manifest.platforms["darwin-x86_64"].installer_url).toBe(
"https://updates.limecloud.com/lime/stable/v1.20.0/darwin-x86_64/Lime_1.20.0_x64.dmg",
);
expect(result.manifest.platforms["windows-x86_64"].installer_url).toBe(
"https://updates.limecloud.com/lime/stable/v1.20.0/windows-x86_64/Lime_1.20.0_x64-setup.exe",
);
expect(result.r2UploadPlan.map((item) => item.key).sort()).toEqual([
"lime/stable/v1.20.0/darwin-aarch64/Lime.app.tar.gz",
"lime/stable/v1.20.0/darwin-aarch64/Lime_1.20.0_aarch64.dmg",
"lime/stable/v1.20.0/darwin-x86_64/Lime.app.tar.gz",
"lime/stable/v1.20.0/darwin-x86_64/Lime_1.20.0_x64.dmg",
"lime/stable/v1.20.0/windows-x86_64/Lime_1.20.0_x64-setup.exe",
]);
});
it("macOS 平台缺少 DMG 时失败,避免官网回退下载 updater 包", () => {
const root = fs.mkdtempSync(
path.join(os.tmpdir(), "lime-release-missing-dmg-"),
);
const assetsDir = path.join(root, "release-assets");
writeFile(path.join(assetsDir, "aarch64-apple-darwin", "Lime.app.tar.gz"));
writeJson(path.join(assetsDir, "aarch64-apple-darwin", "latest.json"), {
version: "1.20.0",
platforms: {
"darwin-aarch64": {
signature: "sig-arm",
url: "https://example.com/Lime.app.tar.gz",
},
},
});
expect(() =>
collectUpdaterManifest({
assetsDir,
baseUrl: "https://updates.limecloud.com",
requiredPlatforms: ["darwin-aarch64"],
version: "v1.20.0",
}),
).toThrow(/missing installer asset/);
});
});
describe("R2 release cleanup", () => {