feat: 迁移aigc 的到养马岛

This commit is contained in:
zoujing committed 2026-09-22 14:36:51 +08:00
1 parent 340ecc9165
commit f19f748b05
103 files changed
+10475 -2150

No files matched your search

+191
View File
@@ -0,0 +1,191 @@
const assert = require("node:assert/strict");
const { readFileSync } = require("node:fs");
const path = require("node:path");
const vm = require("node:vm");
const { execFileSync } = require("node:child_process");
const { test } = require("node:test");
const root = path.resolve(__dirname, "..");
const read = (file) => readFileSync(path.join(root, file), "utf8");
// Execute the actual page handlers while substituting only platform/lifecycle APIs.
function loadScript(file, names, dependencies = {}) {
const source = read(file);
const script = file.endsWith(".vue")
? source.match(/<script setup[^>]*>([\s\S]*?)<\/script>/)[1]
: source;
const executable = script
.replace(/^import\s[\s\S]*?;\s*/gm, "")
.replace(/^export default .*;\s*$/gm, "")
.replace(/^export (?=const |function )/gm, "");
const context = vm.createContext({
console: { log() {}, warn() {}, error() {} },
ref: (value) => ({ value }),
computed: (getter) => ({ get value() { return getter(); } }),
onMounted() {},
...dependencies,
});
return vm.runInContext(
`${executable}\n;({${names.map((name) => `${name}: typeof ${name} === 'undefined' ? undefined : ${name}`).join(",")}})`,
context,
{ filename: file },
);
}
const { Command } = loadScript("src/model/ChatModel.js", ["Command"]);
const normalize = (value) => JSON.parse(JSON.stringify(value));
test("旅行记录 waits for authentication before opening the local AIGC home", async () => {
const destinations = [];
let loginChecks = 0;
let allowLogin;
const login = new Promise((resolve) => { allowLogin = resolve; });
const quick = loadScript("src/pages/ChatMain/ChatQuickAccess/index.vue", ["itemList", "sendReply"], {
Command,
SEND_MESSAGE_COMMAND_TYPE: "send-command",
checkToken: () => { loginChecks++; return login; },
uni: { navigateTo: ({ url }) => destinations.push(url), $emit() {} },
});
assert.equal(quick.itemList.value[0].title, "旅行记录");
assert.equal(quick.itemList.value[0].type, "Command.travelAIGC");
quick.sendReply(quick.itemList.value[0]);
assert.equal(loginChecks, 1);
assert.deepEqual(destinations, []);
allowLogin();
await login;
await Promise.resolve();
assert.deepEqual(destinations, ["/pages-aigc/home/home"]);
assert.deepEqual(quick.itemList.value.slice(1).map((item) => item.title).join(","), "探索发现,呼叫服务,更多");
});
function loadGenerator(toolCall) {
const navigation = [];
const component = loadScript("src/pages/ChatModule/GeneratorPhotoComponent/index.vue", ["componentDataMap", "jumpClick"], {
defineProps: () => ({ toolCall }),
getAccessToken: () => "test-token",
navigateTo: (...args) => navigation.push(args),
});
return { ...component, navigation };
}
test("generator card accepts both server payload formats and preserves their jump URL", () => {
for (const field of ["componentNameParams", "aigc"]) {
const generator = loadGenerator({ [field]: { title: "title.png", jumpUrl: "/pages-aigc/home/home" } });
assert.ok(generator.componentDataMap, "card needs normalized data for both server payloads");
assert.equal(generator.componentDataMap.value.title, "title.png");
generator.jumpClick();
assert.deepEqual(normalize(generator.navigation), [["/pages-aigc/home/home", { token: "test-token" }]]);
}
});
test("generator merges field fallbacks and safely ignores a missing link", () => {
const generator = loadGenerator({ componentNameParams: { title: "new.png" }, aigc: { title: "old.png", description: "description" } });
assert.ok(generator.componentDataMap);
assert.equal(generator.componentDataMap.value.title, "new.png");
assert.equal(generator.componentDataMap.value.description, "description");
generator.jumpClick();
assert.deepEqual(generator.navigation, []);
const empty = loadGenerator({});
assert.doesNotThrow(() => empty.jumpClick());
});
test("the actual router opens AIGC locally and keeps external generator pages in WebView", () => {
const { objectToUrlParams } = loadScript("src/utils/UrlParams.js", ["objectToUrlParams"]);
const destinations = [];
const { navigateTo } = loadScript("src/router/index.js", ["navigateTo"], {
objectToUrlParams,
uni: { navigateTo: ({ url }) => destinations.push(url) },
});
navigateTo("/pages-aigc/home/home", { token: "test-token" });
navigateTo("https://example.invalid/aigc/#/home", { token: "test-token" });
assert.equal(destinations[0], "/pages-aigc/home/home?token=test-token");
assert.equal(destinations[1], "/pages/webview/index?url=" + encodeURIComponent("https://example.invalid/aigc/#/home?token=test-token"));
});
test("shared work renders its initial empty state before the request completes", () => {
const { compile } = require("@vue/compiler-dom");
const Vue = require("vue");
const template = read("src/pages-aigc/sharedWork/sharedWork.vue").split("<template>")[1].split("<script setup>")[0].replace(/<\/template>\s*$/, "");
const render = new Function("Vue", compile(template).code)({ ...Vue, resolveComponent: (name) => name });
assert.doesNotThrow(() => render({
shareDetail: null, isVideo: false, videoResultUrl: "", imageResultUrl: "",
generatorTypeText: "", handleBack() {}, handleCreateOwn() {},
}, []));
});
test("cold share entry waits for the target service address before loading a public work", async () => {
let finishEnvironment;
const ready = new Promise((resolve) => { finishEnvironment = resolve; });
let requests = 0;
const page = loadScript("src/pages-aigc/sharedWork/sharedWork.vue", ["fetchShareDetail"], {
onLoad() {},
getEvnUrl: () => ready,
getAigcGeneratorTaskShareDetail: async () => { requests++; return { code: 0, data: { templateName: "旅行作品" } }; },
uni: { showLoading() {}, hideLoading() {}, showToast() {} },
});
const pending = page.fetchShareDetail();
assert.equal(requests, 0, "do not send the request while the app is still resolving its service address");
finishEnvironment();
await pending;
assert.equal(requests, 1);
});
function loadRequest({ statusCode = 200 } = {}) {
const logs = [];
const sent = [];
let removed = 0;
let logins = 0;
const { request } = loadScript("src/request/base/request.js", ["request"], {
getCurrentConfig: () => ({ clientId: "yangmadao-test-client" }),
useAppStore: () => ({ serverConfig: { baseUrl: "https://target.invalid" } }),
getAccessToken: () => "PRIVATE_TOKEN",
removeAccessToken: () => { removed++; },
goLogin: () => { logins++; },
NOTICE_EVENT_LOGOUT: "logout",
setTimeout: (callback) => callback(),
console: { log: (...args) => logs.push(args.join(" ")), error() {} },
uni: {
$emit() {},
request: (config) => {
sent.push(config);
config.success({ statusCode, data: { code: 0, data: { paySig: "PRIVATE_SIGNATURE" } } });
},
},
});
return { request, logs, sent, removed: () => removed, logins: () => logins };
}
test("AIGC sensitive requests retain target identity without logging credentials", async () => {
const client = loadRequest();
await client.request.post("/hotelBiz/credit/virtual-recharge/orders", { wxLoginCode: "PRIVATE_LOGIN_CODE" }, { sensitive: true });
assert.equal(client.sent[0].url, "https://target.invalid/hotelBiz/credit/virtual-recharge/orders");
assert.equal(client.sent[0].header.clientId, "yangmadao-test-client");
assert.equal(client.sent[0].header.Authorization, "Bearer PRIVATE_TOKEN");
assert.equal(client.sent[0].data.wxLoginCode, "PRIVATE_LOGIN_CODE");
assert.doesNotMatch(client.logs.join("\n"), /PRIVATE_TOKEN|PRIVATE_LOGIN_CODE|PRIVATE_SIGNATURE/);
assert.equal(client.sent[0].sensitive, undefined);
});
test("public share requests omit authentication and expiry still invokes target login", async () => {
const client = loadRequest();
await client.request.post("/hotelBiz/aigcGeneratorTask/share/detail", { shareKey: "public-share" }, { noToken: true });
assert.equal(client.sent[0].header.Authorization, undefined);
assert.equal(client.sent[0].header.clientId, "yangmadao-test-client");
const expired = loadRequest({ statusCode: 424 });
await expired.request.get("/hotelBiz/credit/current");
assert.equal(expired.removed(), 1);
assert.equal(expired.logins(), 1);
});
test("migration preserves the independent target client configuration byte for byte", () => {
const baseline = "340ecc9165684467c01a1f803e21040f86ba3a8f";
const files = [
"src/manifest.json", "project.config.json", "src/constant/base.js",
"src/request/base/config.js", "src/request/base/baseUrl.js", "src/uni.scss",
"client-configs.json", "scripts/update-appid.js", "src/constant/token.js",
];
for (const file of files) {
const original = execFileSync("git", ["show", `${baseline}:${file}`], { cwd: root });
assert.deepEqual(readFileSync(path.join(root, file)), original, `${file} must keep target client configuration`);
}
});