cortico-world-image-attach 0.0.0-stage → 0.1.0

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/CHANGELOG.md ADDED
@@ -0,0 +1,3 @@
1
+ ## 0.1.0
2
+
3
+ - 首个版本:`image_attach`,把硬盘上的一张图片读成对话附件,回执给 `log:` 句柄。
package/README.md CHANGED
@@ -1,3 +1,64 @@
1
- # Temporary Holding Version
1
+ # cortico-world-image-attach
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Owner: `src/definition.ts`
4
+
5
+ `image_attach` 读硬盘上的一张图片(png / jpg / jpeg / gif / webp,不超过 5 MB),把字节作为这次对话的
6
+ 附件交出去。回执正文写原路径与字节数;句柄是 Core 追加在后面的 `[blob log:… image/png 文件名]` 行。
7
+ 把那个 `log:` 句柄填进 `pet_say` 的 `images`,对话页就会出现这张图。
8
+
9
+ 图片字节进日志附件(`log:`),不写 Memory,也不复制原文件。要长期留在工作区,再让 bot 调 Persona 的
10
+ `save_blob` 存成 `mem:`。
11
+
12
+ ## 装到哪
13
+
14
+ 这个目录就是包,放在哪里都行。Coopanion 便携版把扩展装在
15
+ `<Coopanion>\data\extensions`;WebUI 的「从本机目录安装」会以链接方式把它放进去。
16
+
17
+ ## 怎么加载
18
+
19
+ 1. 打开 Coopanion 的 WebUI,进「扩展」页,「手动安装」选「从本机目录安装」,填这个目录的绝对路径
20
+ (例如 `D:\AI\Cortina-main\Cortina\state\packages\cortico-world-image-attach`)。
21
+ 2. 重启 Coopanion 进程。
22
+ 3. 在 World 总览里启用「本地图片递送」(配置 `worlds.image.enabled`)。它是新 World,默认关闭。
23
+
24
+ 命令行也可以:在 `<Coopanion>\data\extensions` 下 `corepack pnpm add <这个目录>`。WebUI 装完同样要重启。
25
+
26
+ ## 怎么确认加载成功
27
+
28
+ - 扩展页卡片显示「已加载」,名称是「本地图片递送」。
29
+ - World 总览里多一张「本地图片递送」卡,启用后工具表里出现 `image_attach`。
30
+ - 对话里让桌宠调一次,回执里应有 `[blob log:…]` 行。
31
+
32
+ ## 怎么测
33
+
34
+ 1. 在对话页对桌宠说:「用 image_attach 读 `D:\图片\cat.png`,然后用 pet_say 把这张图发给我」。
35
+ 2. 工具回执应包含原路径、字节数和一行 `[blob log:… image/png cat.png]`。
36
+ 3. 桌宠调 `pet_say` 时,`images` 填回执里的那个 `log:` 句柄;对话页里看到图片就算成了。
37
+
38
+ ## 失败回执
39
+
40
+ 工具失败时返回可读的文本,并把这次调用标成失败,不抛异常:
41
+
42
+ - `path 不能为空;给图片文件的绝对路径。`
43
+ - `路径必须是绝对路径: cat.png`
44
+ - `文件不存在: D:\…\nope.png`
45
+ - `不是普通文件(可能是目录或设备): D:\…\folder.png`
46
+ - `不支持的图片格式: .bmp。支持 png、jpg、jpeg、gif、webp。`
47
+ - `文件太大: 6291456 字节,上限 5 MB(5242880 字节)。`
48
+ - `读取失败: …`
49
+
50
+ ## 开发
51
+
52
+ ```bash
53
+ corepack pnpm install
54
+ pnpm typecheck && pnpm test
55
+ ```
56
+
57
+ 在 Cortico 检出根下重写自描述并校验:
58
+
59
+ ```bash
60
+ pnpm describe:extension <这个目录>
61
+ pnpm check:extension <这个目录>
62
+ ```
63
+
64
+ 契约版本是 world 6;`describe.json` 由 `describe:extension` 生成,工具或描述改了要重跑。
package/describe.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "tools": [
3
+ {
4
+ "name": "image_attach",
5
+ "description": "Attach one image file from the local disk to this conversation. Give its absolute path; the receipt carries the image as a [blob ...] line with a log: handle you can hand to image-sending tools. Accepts png, jpg, jpeg, gif and webp up to 5 MB.",
6
+ "parameters": {
7
+ "type": "object",
8
+ "additionalProperties": false,
9
+ "properties": {
10
+ "path": {
11
+ "type": "string",
12
+ "description": "Absolute path of the image file on this machine."
13
+ }
14
+ },
15
+ "required": [
16
+ "path"
17
+ ]
18
+ },
19
+ "tags": [
20
+ "read"
21
+ ],
22
+ "summary": {
23
+ "zh": "读取硬盘上的一张图片,作为附件交给这次对话,回执给 log: 句柄。",
24
+ "en": "Reads one image file from disk into the conversation as an attachment and returns its log: handle."
25
+ }
26
+ }
27
+ ],
28
+ "events": []
29
+ }
package/package.json CHANGED
@@ -1,6 +1,42 @@
1
1
  {
2
2
  "name": "cortico-world-image-attach",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Cortico World: 把电脑里任意一张图片发进对话,可以配合有搜图能力的扩展,将找到的图片直接发在对话里。",
6
+ "license": "MIT",
7
+ "keywords": [
8
+ "cortico-world",
9
+ "cortico"
10
+ ],
11
+ "engines": {
12
+ "node": ">=22"
13
+ },
14
+ "packageManager": "pnpm@11.28.6",
15
+ "main": "./src/index.ts",
16
+ "cortico": {
17
+ "kind": "world",
18
+ "api": 6,
19
+ "displayName": "本地图片递送",
20
+ "tags": [
21
+ "unsafe"
22
+ ],
23
+ "describe": "describe.json"
24
+ },
25
+ "files": [
26
+ "src",
27
+ "README.md",
28
+ "CHANGELOG.md",
29
+ "describe.json"
30
+ ],
31
+ "scripts": {
32
+ "test": "vitest run",
33
+ "typecheck": "tsc --noEmit"
34
+ },
35
+ "devDependencies": {
36
+ "@types/node": "^22.10.0",
37
+ "cortico": "^0.2.5",
38
+ "tsx": "^4.19.2",
39
+ "typescript": "^5.7.2",
40
+ "vitest": "^2.1.8"
41
+ }
42
+ }
package/src/config.ts ADDED
@@ -0,0 +1,8 @@
1
+ /** config.json 的 worlds.image 段;这个 World 没有自己的旋钮,只有启用开关。 */
2
+ export interface ImageConfigSection {
3
+ enabled: boolean;
4
+ }
5
+
6
+ export const IMAGE_DEFAULTS: ImageConfigSection = {
7
+ enabled: false,
8
+ };
@@ -0,0 +1,20 @@
1
+ import { describeTools, type WorldDefinition } from 'cortico/world.ts';
2
+ import { IMAGE_DEFAULTS, type ImageConfigSection } from './config.ts';
3
+ import { IMAGE_TOOL_DECLS, ImageWorld } from './world.ts';
4
+
5
+ export const IMAGE: WorldDefinition<ImageConfigSection> = {
6
+ id: 'image',
7
+ label: 'Image',
8
+ defaults: () => ({ ...IMAGE_DEFAULTS }),
9
+ create: () => new ImageWorld(),
10
+ // 自描述:全部工具;这个 World 不推事件。
11
+ describe: {
12
+ tools: describeTools(IMAGE_TOOL_DECLS, {
13
+ image_attach: {
14
+ zh: '读取硬盘上的一张图片,作为附件交给这次对话,回执给 log: 句柄。',
15
+ en: 'Reads one image file from disk into the conversation as an attachment and returns its log: handle.',
16
+ },
17
+ }),
18
+ events: [],
19
+ },
20
+ };
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ /** 包入口:默认导出 `WorldDefinition`,加载器按 `cortico.kind === 'world'` 认它。 */
2
+ import { IMAGE } from './definition.ts';
3
+
4
+ export default IMAGE;
5
+ export { IMAGE };
6
+ export type { ImageConfigSection } from './config.ts';
package/src/world.ts ADDED
@@ -0,0 +1,88 @@
1
+ /** `image_attach`:把硬盘上的一张图片读成这次对话的附件,回执给句柄。 */
2
+ import { readFileSync, statSync } from 'node:fs';
3
+ import { basename, extname, isAbsolute } from 'node:path';
4
+ import type { ToolDef, ToolOutcome, World } from 'cortico/core/types.ts';
5
+
6
+ /** 5 MiB;再大的图不值得随上下文走。 */
7
+ const MAX_BYTES = 5 * 1024 * 1024;
8
+
9
+ const MIME_BY_EXTENSION: Record<string, string> = {
10
+ '.png': 'image/png',
11
+ '.jpg': 'image/jpeg',
12
+ '.jpeg': 'image/jpeg',
13
+ '.gif': 'image/gif',
14
+ '.webp': 'image/webp',
15
+ };
16
+
17
+ /** 全部工具的声明;定义里的 describe 与 `tools()` 都取它。 */
18
+ export const IMAGE_TOOL_DECLS: ReadonlyArray<Omit<ToolDef, 'handler'>> = [
19
+ {
20
+ name: 'image_attach',
21
+ description: 'Attach one image file from the local disk to this conversation. Give its absolute path; '
22
+ + 'the receipt carries the image as a [blob ...] line with a log: handle you can hand to image-sending tools. '
23
+ + 'Accepts png, jpg, jpeg, gif and webp up to 5 MB.',
24
+ tags: ['read'],
25
+ parameters: {
26
+ type: 'object',
27
+ additionalProperties: false,
28
+ properties: {
29
+ path: { type: 'string', description: 'Absolute path of the image file on this machine.' },
30
+ },
31
+ required: ['path'],
32
+ },
33
+ },
34
+ ];
35
+
36
+ export class ImageWorld implements World {
37
+ readonly id = 'image';
38
+
39
+ /** 这个 World 不进环境提示词。 */
40
+ envPromptVars(): null {
41
+ return null;
42
+ }
43
+
44
+ tools(): ToolDef[] {
45
+ return [{ ...IMAGE_TOOL_DECLS[0], handler: async (args) => this.attach(args) }];
46
+ }
47
+
48
+ async start(): Promise<void> {}
49
+
50
+ async stop(): Promise<void> {}
51
+
52
+ private attach(args: Record<string, unknown>): ToolOutcome {
53
+ const path = String(args.path ?? '').trim();
54
+ if (!path) return fail('path 不能为空;给图片文件的绝对路径。');
55
+ if (!isAbsolute(path)) return fail(`路径必须是绝对路径: ${path}`);
56
+
57
+ let size: number;
58
+ try {
59
+ const stat = statSync(path);
60
+ if (!stat.isFile()) return fail(`不是普通文件(可能是目录或设备): ${path}`);
61
+ size = stat.size;
62
+ } catch {
63
+ return fail(`文件不存在: ${path}`);
64
+ }
65
+
66
+ const extension = extname(path).toLowerCase();
67
+ const mime = MIME_BY_EXTENSION[extension];
68
+ if (!mime) return fail(`不支持的图片格式: ${extension || '(无扩展名)'}。支持 png、jpg、jpeg、gif、webp。`);
69
+ if (size > MAX_BYTES) return fail(`文件太大: ${size} 字节,上限 5 MB(${MAX_BYTES} 字节)。`);
70
+
71
+ let bytes: Uint8Array;
72
+ try {
73
+ bytes = readFileSync(path);
74
+ } catch (err) {
75
+ return fail(`读取失败: ${err instanceof Error ? err.message : String(err)}`);
76
+ }
77
+
78
+ const name = basename(path);
79
+ return {
80
+ text: `已读取 ${path}(${bytes.byteLength} 字节,${mime})。句柄在下一行 [blob …] 里,把它填进 pet_say 的 images。`,
81
+ blobs: [{ bytes, mime, name, fallbackText: `原路径 ${path}` }],
82
+ };
83
+ }
84
+ }
85
+
86
+ function fail(text: string): ToolOutcome {
87
+ return { text, failed: true };
88
+ }