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/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
- // 「对话页发图」的自愈补丁会在 import src/world.js 时跑;检查脚本不该改安装目录,
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 === '0.2.0', 'version 抬到 0.2.0', String(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 === 8, '补丁一共 8 处(5 处打 world.ts + 1 处打 definition.ts + 2 处可选的 QQ 扩展)', String(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 = ['pet-fs-import', 'pet-path-import', 'pet-show-image-tool', 'pet-workspace-option', 'pet-workspace-relpaths', 'pet-workspace-wire', 'qq-image-relpaths', 'qq-image-hints'];
759
- check(wantIds.every((id) => specIds.includes(id)), '8 处补丁 id 齐全', specIds.join(','));
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
- // 这 4 处补丁已经删掉,不许再回来(锚点早就不匹配,留着只会每次启动都记一条失败)。
763
- for (const gone of ['client-say-images', 'pet-say-images', 'pet-record-blobs', 'pet-say-type']) {
764
- check(!specIds.includes(gone), `补丁 ${gone} 已删掉(框架原生已覆盖),不许再出现`, specIds.join(','));
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
- // pet_show_image 也要能认相对路径(按桌宠工作区解析)。
820
- const petSpecs = specs.filter((s) => s.id.startsWith('pet-workspace-'));
821
- check(petSpecs.length === 3 && petSpecs.every((s) => s.optional !== true), '桌宠工作区相对路径那 3 处是必备补丁', JSON.stringify(petSpecs.map((s) => s.id)));
822
- check(petSpecs.filter((s) => s.id !== 'pet-workspace-wire').every((s) => s.file.endsWith('world.ts')) && petSpecs.find((s) => s.id === 'pet-workspace-wire').file.endsWith('definition.ts'), '前两处打 world.ts、接线那处打 definition.ts', JSON.stringify(petSpecs.map((s) => `${s.id}:${s.file.endsWith('definition.ts') ? 'definition.ts' : 'world.ts'}`)));
823
- const petFixture = [
824
- "import { existsSync } from 'node:fs';",
825
- "import { isAbsolute, join } from 'node:path';",
826
- 'export interface DesktopPetWorldOptions {',
827
- ' avatarFile?: string;',
828
- ' controls?: unknown;',
829
- '}',
830
- 'export class DesktopPetWorld {',
831
- ' private opts: DesktopPetWorldOptions = {};',
832
- ' showImage(args: { path?: string }) {',
833
- " const raw = typeof args.path === 'string' ? args.path.trim() : '';",
834
- " if (!raw) return { text: '[pet_show_image 没执行] 要给图片的完整路径。', failed: true };",
835
- " if (!isAbsolute(raw)) return { text: '[pet_show_image 没执行] 需要完整路径(例如 C:\\\\Users\\\\...\\\\pic.png)。', failed: true };",
836
- " const caption = typeof args.caption === 'string' ? args.caption.trim() : '';",
837
- " if (!/\\\\.(png|jpe?g|gif|webp)$/i.test(raw)) return { text: '不是图片', failed: true };",
838
- " description: '图片文件的完整路径。',",
839
- ' return { raw, caption };',
840
- ' }',
841
- '}',
842
- '',
843
- ].join('\n');
844
- const fixtureDone = 'raw = join(ws, raw);';
845
- check(!petFixture.includes(fixtureDone), '夹具本身就是「只认绝对路径」的原文(自测有东西可改)');
846
- const petOpt = patchMod.rulesFor('pet-workspace-option');
847
- check(typeof petOpt?.done === 'function' && typeof petOpt?.apply === 'function', 'pet-workspace-option 有 done/apply');
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('未知方法与面板');