cortico-world-image 0.2.0 → 0.3.1
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/ENV_PROMPT.md +2 -2
- package/README.md +77 -101
- package/describe.json +1 -1
- package/dist/webui.js +15 -13
- package/package.json +2 -2
- package/src/chat-image-patch.js +93 -262
- package/src/config.js +2 -2
- package/src/world.js +23 -11
- package/tools/check.mjs +67 -64
- package/tools/patch.mjs +178 -398
package/tools/check.mjs
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* node tools/check.mjs
|
|
8
8
|
*/
|
|
9
9
|
import { createHash } from 'node:crypto';
|
|
10
|
-
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
10
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
11
11
|
import { tmpdir } from 'node:os';
|
|
12
12
|
import { isAbsolute, join } from 'node:path';
|
|
13
13
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
@@ -16,7 +16,7 @@ import { inflateSync } from 'node:zlib';
|
|
|
16
16
|
const here = fileURLToPath(new URL('.', import.meta.url));
|
|
17
17
|
const pkgDir = join(here, '..');
|
|
18
18
|
|
|
19
|
-
//
|
|
19
|
+
// QQ 发图路径的自愈补丁会在 import src/world.js 时跑;检查脚本不该改安装目录,
|
|
20
20
|
// 所以关掉它(补丁本身由 tools/patch.mjs 在副本上验证)。
|
|
21
21
|
process.env.COOPANION_CHAT_IMAGE_PATCH = '0';
|
|
22
22
|
|
|
@@ -612,7 +612,7 @@ try {
|
|
|
612
612
|
section('契约与自描述(框架 0.2)');
|
|
613
613
|
const pkg = JSON.parse(readFileSync(join(pkgDir, 'package.json'), 'utf8'));
|
|
614
614
|
const manifest = pkg.cortico ?? {};
|
|
615
|
-
check(pkg.version
|
|
615
|
+
check(/^0\.3\.\d+$/.test(pkg.version), 'version 在 0.3.x 线上', String(pkg.version));
|
|
616
616
|
check(manifest.api === 6, 'cortico.api 是 6', String(manifest.api));
|
|
617
617
|
check(manifest.webUiClient === 'dist/webui.js', 'webUiClient 指向 dist/webui.js', String(manifest.webUiClient));
|
|
618
618
|
check(manifest.describe === 'describe.json', 'describe 指向 describe.json', String(manifest.describe));
|
|
@@ -746,27 +746,50 @@ try {
|
|
|
746
746
|
await tick(20);
|
|
747
747
|
check(status.state.toasts.some((t) => t.text.includes('没送到') || t.text.includes('检查模式')), '检查模式下点重启只是提示,不会真重启');
|
|
748
748
|
|
|
749
|
-
section('
|
|
749
|
+
section('QQ 发图路径补丁(纯逻辑)');
|
|
750
750
|
const patchMod = await import(pathToFileURL(join(pkgDir, 'src', 'chat-image-patch.js')).href);
|
|
751
|
-
for (const name of ['resolveAppRoot', 'patchSpecs', 'rulesFor', 'applyChatImagePatches', 'patchBackupStatus', 'restoreChatImagePatches', 'consolePort', 'requestConsoleRestart']) {
|
|
751
|
+
for (const name of ['resolveAppRoot', 'patchSpecs', 'rulesFor', 'applyChatImagePatches', 'patchBackupStatus', 'restoreChatImagePatches', 'revertObsoletePetPatches', 'consolePort', 'requestConsoleRestart']) {
|
|
752
752
|
check(typeof patchMod[name] === 'function', `导出 ${name}()`);
|
|
753
753
|
}
|
|
754
754
|
const specs = patchMod.patchSpecs('X:/not-a-real-app');
|
|
755
755
|
const specIds = specs.map((s) => s.id);
|
|
756
|
-
check(specIds.length ===
|
|
756
|
+
check(specIds.length === 2, '补丁一共 2 处,都在可选的 QQ 扩展上(桌宠包从 0.3.0 起不再打补丁)', String(specIds.length));
|
|
757
757
|
check(new Set(specIds).size === specIds.length, '补丁 id 不重复', specIds.join(','));
|
|
758
|
-
const wantIds = ['
|
|
759
|
-
check(wantIds.every((id) => specIds.includes(id)), '
|
|
758
|
+
const wantIds = ['qq-image-relpaths', 'qq-image-hints'];
|
|
759
|
+
check(wantIds.every((id) => specIds.includes(id)), '两处补丁 id 齐全', specIds.join(','));
|
|
760
760
|
check(specIds.every((id) => wantIds.includes(id)), '没有多余的补丁 id', specIds.join(','));
|
|
761
761
|
// 防回归:框架 0.1.23 起原生就能把 say 的图发到对话页(chat.ts 的 wireImages() + 客户端 chat-thumb 缩略图),
|
|
762
|
-
//
|
|
763
|
-
|
|
764
|
-
|
|
762
|
+
// 0.2.0 起 pet_say 自己收 images 句柄,0.3.0 起本扩展连 pet_show_image 都不再注入 ——
|
|
763
|
+
// 这些补丁都已经删掉,不许再回来(锚点早就不匹配,留着只会每次启动都记一条失败)。
|
|
764
|
+
for (const gone of [
|
|
765
|
+
'client-say-images', 'pet-say-images', 'pet-record-blobs', 'pet-say-type',
|
|
766
|
+
'pet-fs-import', 'pet-path-import', 'pet-show-image-tool', 'pet-workspace-option',
|
|
767
|
+
'pet-workspace-relpaths', 'pet-workspace-wire', 'pet-show-image-describe',
|
|
768
|
+
]) {
|
|
769
|
+
check(!specIds.includes(gone), `补丁 ${gone} 已删掉,不许再出现`, specIds.join(','));
|
|
765
770
|
}
|
|
766
771
|
for (const spec of specs) check(typeof spec.what === 'string' && spec.what.length > 4, `补丁 ${spec.id} 有说明`, spec.what);
|
|
767
772
|
const qqSpecs = specs.filter((s) => s.id.startsWith('qq-'));
|
|
768
773
|
check(qqSpecs.length === 2 && qqSpecs.every((s) => s.optional === true), 'QQ 扩展那两处是可选补丁(没装就不算失败)', JSON.stringify(qqSpecs.map((s) => ({ id: s.id, optional: s.optional }))));
|
|
769
774
|
check(qqSpecs.every((s) => s.file.includes('data') && s.file.includes('cortico-world-qq-better-alont1') && s.file.endsWith('world.ts')), 'QQ 补丁落在 <安装>/data/extensions/…/src/world.ts', JSON.stringify(qqSpecs.map((s) => s.file)));
|
|
775
|
+
// 补丁只为「正在跑的那个世界」打:切到官方 QQ 扩展(world qq)后 qqbot 关掉,两处该自动跳过。
|
|
776
|
+
check(qqSpecs.every((s) => s.worldId === 'qqbot'), '两处补丁都挂在 world「qqbot」上(旧版 QQ 扩展)', JSON.stringify(qqSpecs.map((s) => s.worldId)));
|
|
777
|
+
check(typeof patchMod.worldSwitch === 'function', '导出 worldSwitch()');
|
|
778
|
+
const swUnknown = patchMod.worldSwitch('X:/not-a-real-app', 'qqbot');
|
|
779
|
+
check(swUnknown.known === false && swUnknown.enabled === true, '读不到宿主配置时不猜,照旧打补丁', JSON.stringify(swUnknown));
|
|
780
|
+
const swDir = mkdtempSync(join(tmpdir(), 'image-switch-'));
|
|
781
|
+
try {
|
|
782
|
+
mkdirSync(join(swDir, 'data', 'home', 'companion'), { recursive: true });
|
|
783
|
+
writeFileSync(join(swDir, 'data', 'home', 'companion', 'config.json'), JSON.stringify({ worlds: { qqbot: { enabled: false }, qq: { enabled: true } } }));
|
|
784
|
+
const swOff = patchMod.worldSwitch(join(swDir, 'resources', 'app'), 'qqbot');
|
|
785
|
+
check(swOff.known === true && swOff.enabled === false, 'qqbot 关掉时 worldSwitch 认成「没启用」', JSON.stringify(swOff));
|
|
786
|
+
const swOn = patchMod.worldSwitch(join(swDir, 'resources', 'app'), 'qq');
|
|
787
|
+
check(swOn.known === true && swOn.enabled === true, 'qq(官方)开着时 worldSwitch 认成「启用」', JSON.stringify(swOn));
|
|
788
|
+
const swMissing = patchMod.worldSwitch(join(swDir, 'resources', 'app'), 'nope');
|
|
789
|
+
check(swMissing.known === false, '配置里没这个世界就当不知道', JSON.stringify(swMissing));
|
|
790
|
+
} finally {
|
|
791
|
+
rmSync(swDir, { recursive: true, force: true });
|
|
792
|
+
}
|
|
770
793
|
const qqRules = patchMod.rulesFor('qq-image-relpaths');
|
|
771
794
|
check(typeof qqRules?.done === 'function' && typeof qqRules?.apply === 'function', 'QQ 补丁有 done/apply');
|
|
772
795
|
const qqStranger = qqRules.apply('export const nope = 1;\n');
|
|
@@ -816,59 +839,39 @@ try {
|
|
|
816
839
|
check(Boolean(hintRules.apply('export const nope = 1;\n')?.error), '一句都认不出来时仍然报失败');
|
|
817
840
|
check(!hintRules.done('export const nope = 1;\n'), '文件整个换了写法时不谎报「已打过」');
|
|
818
841
|
|
|
819
|
-
//
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
const
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
const
|
|
845
|
-
check(
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
check(Boolean(petOpt.apply('export const nope = 1;\n')?.error), '桌宠世界选项换了写法就报失败,不瞎改');
|
|
849
|
-
const optPatched = petOpt.apply(petFixture);
|
|
850
|
-
check(!optPatched?.error && optPatched.text.includes('workspaceDir?: () => string;') && optPatched.text.includes('avatarFile?: string;'), '世界选项里补上 workspaceDir(原字段不动)', optPatched?.error ?? '');
|
|
851
|
-
check(petOpt.done(optPatched?.text ?? ''), '补完认成「已打过」(幂等)');
|
|
852
|
-
const petRel = patchMod.rulesFor('pet-workspace-relpaths');
|
|
853
|
-
check(Boolean(petRel.apply('export const nope = 1;\n')?.error), 'pet_show_image 换了写法就报失败,不瞎改');
|
|
854
|
-
const relPatched = petRel.apply(petFixture);
|
|
855
|
-
check(!relPatched?.error && relPatched.text.includes(' let raw = typeof args.path'), '路径那行改成 let(const 拼不上)', relPatched?.error ?? '');
|
|
856
|
-
check(!relPatched?.error && relPatched.text.includes("const ws = (this.opts.workspaceDir?.() ?? '').trim();") && relPatched.text.includes(' raw = join(ws, raw);'), '相对路径拼到桌宠工作区上', relPatched?.error ?? '');
|
|
857
|
-
check(!relPatched?.error && relPatched.text.includes("相对路径没有工作区可拼") && !relPatched.text.includes('需要完整路径(例如'), '没给工作区时仍报错,但换成「没有工作区可拼」');
|
|
858
|
-
check(!relPatched?.error && relPatched.text.includes("description: '图片路径:绝对路径,或相对桌宠工作区的路径(例如 blobs/xx.jpg)。'"), '工具说明改成「绝对路径或相对工作区」');
|
|
859
|
-
check(petRel.done(relPatched?.text ?? ''), '改完认成「已打过」(幂等)');
|
|
860
|
-
const petWire = patchMod.rulesFor('pet-workspace-wire');
|
|
861
|
-
check(Boolean(petWire.apply('export const nope = 1;\n')?.error), '装配层换了写法就报失败,不瞎改');
|
|
862
|
-
const wireFixture = ['export function makeWs(ctx: Ctx) {', ' return {', ' avatarFile: join(ctx.botDir, AVATAR_FILE),', ' };', '}', ''].join('\n');
|
|
863
|
-
const wirePatched = petWire.apply(wireFixture);
|
|
864
|
-
check(!wirePatched?.error && wirePatched.text.includes("workspaceDir: () => join(ctx.botDir, 'workspace'),"), '装配层把桌宠工作区传下去', wirePatched?.error ?? '');
|
|
865
|
-
check(wirePatched.text.indexOf('workspaceDir:') > wirePatched.text.indexOf('avatarFile:'), '接在 avatarFile 后面(不动别的行)');
|
|
866
|
-
check(petWire.done(wirePatched?.text ?? ''), '接线补完认成「已打过」(幂等)');
|
|
842
|
+
// 0.3.0 起不再往桌宠包里打补丁(改用官方 pet_say.images);早先版本打进去的旧补丁
|
|
843
|
+
// 靠 revertObsoletePetPatches() 按 .coopanion-orig 还原成上游原样。
|
|
844
|
+
const petWork = mkdtempSync(join(tmpdir(), 'image-revert-'));
|
|
845
|
+
const petApp = join(petWork, 'resources', 'app');
|
|
846
|
+
const petSrc = join(petApp, 'node_modules', 'cortico-world-desktop-pet', 'src');
|
|
847
|
+
mkdirSync(petSrc, { recursive: true });
|
|
848
|
+
mkdirSync(join(petApp, 'build', 'cortico', 'src'), { recursive: true });
|
|
849
|
+
writeFileSync(join(petApp, 'build', 'cortico', 'src', 'paths.ts'), 'export {};\n');
|
|
850
|
+
const petPristine = 'export const tools = () => [];\n';
|
|
851
|
+
const petFile = join(petSrc, 'world.ts');
|
|
852
|
+
const petBackup = `${petFile}.coopanion-orig`;
|
|
853
|
+
writeFileSync(petFile, `${petPristine}/* coopanion-chat-image: 旧补丁 */\n`);
|
|
854
|
+
writeFileSync(petBackup, petPristine);
|
|
855
|
+
const reverted = patchMod.revertObsoletePetPatches(petApp, {});
|
|
856
|
+
check(reverted.length === 1 && reverted[0].reverted === true, '带我们标记的桌宠文件被还原', JSON.stringify(reverted));
|
|
857
|
+
check(readFileSync(petFile, 'utf8') === petPristine, '还原内容就是 .coopanion-orig 那份原文');
|
|
858
|
+
check(patchMod.revertObsoletePetPatches(petApp, {}).length === 0, '还原过就不再有可还原的文件(幂等)');
|
|
859
|
+
writeFileSync(petFile, `${petPristine}// 桌宠包自己升过级,我们的标记已经没了\n`);
|
|
860
|
+
writeFileSync(petBackup, petPristine);
|
|
861
|
+
check(patchMod.revertObsoletePetPatches(petApp, {}).length === 0, '文件里没有我们的标记时一律不动');
|
|
862
|
+
check(readFileSync(petFile, 'utf8').includes('桌宠包自己升过级'), '没标记的文件原样留着');
|
|
863
|
+
rmSync(petBackup, { force: true });
|
|
864
|
+
writeFileSync(petFile, `${petPristine}/* coopanion-chat-image */\n`);
|
|
865
|
+
check(patchMod.revertObsoletePetPatches(petApp, {}).length === 0, '没有备份可还原时不动(不猜)');
|
|
866
|
+
// 没装 QQ 扩展:两处可选补丁都 absent,不算失败、也不请求重启。
|
|
867
|
+
const noQq = patchMod.applyChatImagePatches(petApp, {});
|
|
868
|
+
check(noQq.failed.length === 0 && noQq.results.every((r) => r.state === 'absent'), '没装 QQ 扩展时两处可选补丁记 absent,不算失败', JSON.stringify(noQq.results.map((r) => `${r.id}:${r.state}`)));
|
|
869
|
+
check(!noQq.restartPending, '没装 QQ 扩展时不请求重启');
|
|
870
|
+
rmSync(petWork, { recursive: true, force: true });
|
|
867
871
|
|
|
868
872
|
const portWork = mkdtempSync(join(tmpdir(), 'image-port-'));
|
|
869
873
|
mkdirSync(join(portWork, 'resources', 'app'), { recursive: true });
|
|
870
874
|
mkdirSync(join(portWork, 'data', 'home', 'companion'), { recursive: true });
|
|
871
|
-
const { writeFileSync } = await import('node:fs');
|
|
872
875
|
writeFileSync(join(portWork, 'data', 'home', 'companion', 'config.json'), JSON.stringify({ web: { port: 18099 } }));
|
|
873
876
|
check(patchMod.consolePort({ appRoot: join(portWork, 'resources', 'app') }) === 18099, '端口从 <安装>/data/home/companion/config.json 读出来');
|
|
874
877
|
check(patchMod.consolePort({ appRoot: join(portWork, 'resources', 'app'), explicit: 12345 }) === 12345, '显式端口优先');
|
|
@@ -880,7 +883,7 @@ try {
|
|
|
880
883
|
check(String(bogus.error ?? '').includes('resources/app'), '失败原因说清是哪个目录', String(bogus.error));
|
|
881
884
|
check(!bogus.restartPending, '没打上就不请求重启');
|
|
882
885
|
|
|
883
|
-
section('
|
|
886
|
+
section('QQ 发图路径补丁(接线)');
|
|
884
887
|
const worldSrc = readFileSync(join(pkgDir, 'src', 'world.js'), 'utf8');
|
|
885
888
|
const patchSrc = readFileSync(join(pkgDir, 'src', 'chat-image-patch.js'), 'utf8');
|
|
886
889
|
check(worldSrc.includes("from './chat-image-patch.js'"), 'world.js 引了补丁模块');
|
|
@@ -892,7 +895,7 @@ try {
|
|
|
892
895
|
check(worldSrc.includes('backups: patchBackupStatus('), '状态里带上每处补丁的备份现状');
|
|
893
896
|
check(patchSrc.includes('restoreChatImagePatches') && patchSrc.includes('patchBackupStatus'), '补丁模块导出还原与备份查询');
|
|
894
897
|
for (const id of wantIds) check(patchSrc.includes(`'${id}'`), `补丁模块里有 ${id}`);
|
|
895
|
-
check(patchSrc.includes('cortico-world-desktop-pet'), '
|
|
898
|
+
check(patchSrc.includes('cortico-world-desktop-pet'), '还认得桌宠包目录(只用来还原旧补丁)');
|
|
896
899
|
|
|
897
900
|
const patched = await world.invoke('status', 'repatch', []);
|
|
898
901
|
check(patched?.packageDir === pkgDir, 'repatch 回结果里带扩展目录', String(patched?.packageDir));
|
|
@@ -903,8 +906,8 @@ try {
|
|
|
903
906
|
check(Array.isArray(restored?.restored?.restored) && String(restored?.restored?.skipped ?? '').includes('COOPANION_CHAT_IMAGE_PATCH'), '检查模式下 restore 不动安装目录', JSON.stringify(restored?.restored));
|
|
904
907
|
const st = await world.invoke('status', 'state', []);
|
|
905
908
|
check(Array.isArray(st?.patch?.backups), '状态里能读到备份列表', JSON.stringify(st?.patch?.backups));
|
|
906
|
-
const badge = (world.webUi('zh').badges ?? []).find((b) => b.label === '
|
|
907
|
-
check(!!badge, '
|
|
909
|
+
const badge = (world.webUi('zh').badges ?? []).find((b) => b.label === 'QQ 发图路径');
|
|
910
|
+
check(!!badge, '控制台徽章里有「QQ 发图路径」');
|
|
908
911
|
check(badge?.value === '未检查', '检查模式下徽章显示「未检查」', String(badge?.value));
|
|
909
912
|
|
|
910
913
|
section('未知方法与面板');
|