mirror of
https://github.com/JimLiu/baoyu-skills.git
synced 2026-07-12 05:51:44 +08:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f490f7e5e3 | |||
| 22031aa2d2 | |||
| 51b31aa050 | |||
| 441ca307a6 |
@@ -6,7 +6,7 @@
|
|||||||
},
|
},
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"description": "Skills shared by Baoyu for improving daily work efficiency",
|
"description": "Skills shared by Baoyu for improving daily work efficiency",
|
||||||
"version": "2.5.1"
|
"version": "2.5.2"
|
||||||
},
|
},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,6 +2,14 @@
|
|||||||
|
|
||||||
English | [中文](./CHANGELOG.zh.md)
|
English | [中文](./CHANGELOG.zh.md)
|
||||||
|
|
||||||
|
## 2.5.2 - 2026-06-18
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
- `codex-imagegen`: stop Codex from satisfying a request by copying an unrelated pre-existing image from `generated_images` instead of generating a new one. Verification now requires real evidence that `image_gen` ran in the current thread — an `image_gen` event in the stream **or** a PNG in this thread's `generated_images` dir — and the spawned-agent instruction forbids reading or reusing history images before `image_gen` is called ([#185](https://github.com/JimLiu/baoyu-skills/issues/185))
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
- README: document Codex project-level install (`.agents/skills`) and project- vs user-level WeChat credential scopes
|
||||||
|
|
||||||
## 2.5.1 - 2026-06-13
|
## 2.5.1 - 2026-06-13
|
||||||
|
|
||||||
### Documentation
|
### Documentation
|
||||||
|
|||||||
@@ -2,6 +2,14 @@
|
|||||||
|
|
||||||
[English](./CHANGELOG.md) | 中文
|
[English](./CHANGELOG.md) | 中文
|
||||||
|
|
||||||
|
## 2.5.2 - 2026-06-18
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
- `codex-imagegen`:阻止 Codex 通过复制 `generated_images` 中无关旧图片来代替真正生成的取巧行为。校验改为要求当前线程内 `image_gen` 真正运行的证据——事件流中的 `image_gen` 事件,**或**当前线程 `generated_images` 目录下新生成的 PNG——并在派生 agent 指令中禁止在调用 `image_gen` 之前读取或复用历史图片([#185](https://github.com/JimLiu/baoyu-skills/issues/185))
|
||||||
|
|
||||||
|
### 文档
|
||||||
|
- README:补充 Codex 项目级安装(`.agents/skills`)以及项目级与全局公众号凭证的放置位置
|
||||||
|
|
||||||
## 2.5.1 - 2026-06-13
|
## 2.5.1 - 2026-06-13
|
||||||
|
|
||||||
### 文档
|
### 文档
|
||||||
|
|||||||
@@ -19,6 +19,31 @@ Skills shared by Baoyu for improving daily work efficiency with AI Agents (Claud
|
|||||||
npx skills add jimliu/baoyu-skills
|
npx skills add jimliu/baoyu-skills
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Codex Project-Level Install
|
||||||
|
|
||||||
|
If you only need a subset of skills in one project, you do not need to install the full plugin. Codex scans `.agents/skills` inside a project, so copy or symlink each needed skill as a full directory:
|
||||||
|
|
||||||
|
```text
|
||||||
|
<project>/.agents/skills/baoyu-cover-image/SKILL.md
|
||||||
|
<project>/.agents/skills/baoyu-article-illustrator/SKILL.md
|
||||||
|
<project>/.agents/skills/baoyu-post-to-wechat/SKILL.md
|
||||||
|
```
|
||||||
|
|
||||||
|
For a WeChat Official Account article workflow, the usual minimal set is:
|
||||||
|
|
||||||
|
- `baoyu-cover-image`
|
||||||
|
- `baoyu-article-illustrator`
|
||||||
|
- `baoyu-post-to-wechat`
|
||||||
|
|
||||||
|
You do not need to install `baoyu-markdown-to-html` separately. `baoyu-post-to-wechat` already includes the Markdown to WeChat-ready HTML conversion flow. Install `baoyu-format-markdown` only if you need to first turn raw text or drafts into structured Markdown articles with titles, summaries, headings, bold text, lists, and similar formatting.
|
||||||
|
|
||||||
|
Place WeChat API credentials according to the scope you want:
|
||||||
|
|
||||||
|
- User-level: `~/.baoyu-skills/.env`
|
||||||
|
- Project-level: `<project>/.baoyu-skills/.env`
|
||||||
|
|
||||||
|
Project-level `.env` files are useful when credentials should apply only to the current project. Do not commit them to Git.
|
||||||
|
|
||||||
### Publish to ClawHub / OpenClaw
|
### Publish to ClawHub / OpenClaw
|
||||||
|
|
||||||
This repository now supports publishing each `skills/baoyu-*` directory as an individual ClawHub skill.
|
This repository now supports publishing each `skills/baoyu-*` directory as an individual ClawHub skill.
|
||||||
|
|||||||
@@ -29,6 +29,31 @@
|
|||||||
npx skills add jimliu/baoyu-skills
|
npx skills add jimliu/baoyu-skills
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Codex 项目级安装
|
||||||
|
|
||||||
|
如果只在某个项目中使用部分技能,不需要安装整个插件。Codex 会扫描项目里的 `.agents/skills`,可以只把需要的 skill 按整个目录复制或软链接到当前项目:
|
||||||
|
|
||||||
|
```text
|
||||||
|
<project>/.agents/skills/baoyu-cover-image/SKILL.md
|
||||||
|
<project>/.agents/skills/baoyu-article-illustrator/SKILL.md
|
||||||
|
<project>/.agents/skills/baoyu-post-to-wechat/SKILL.md
|
||||||
|
```
|
||||||
|
|
||||||
|
公众号文章发布的最小组合通常是:
|
||||||
|
|
||||||
|
- `baoyu-cover-image`
|
||||||
|
- `baoyu-article-illustrator`
|
||||||
|
- `baoyu-post-to-wechat`
|
||||||
|
|
||||||
|
不需要单独安装 `baoyu-markdown-to-html`,`baoyu-post-to-wechat` 已内置 Markdown 到公众号可用 HTML 的转换流程。只有需要先把原始文本或草稿整理成 Markdown 文章结构(标题、摘要、层级标题、加粗、列表等)时,再额外安装 `baoyu-format-markdown`。
|
||||||
|
|
||||||
|
公众号 API 凭证按作用范围放置:
|
||||||
|
|
||||||
|
- 全局:`~/.baoyu-skills/.env`
|
||||||
|
- 项目:`<project>/.baoyu-skills/.env`
|
||||||
|
|
||||||
|
项目级 `.env` 适合只给当前项目使用,注意不要提交到 Git。
|
||||||
|
|
||||||
### 发布到 ClawHub / OpenClaw
|
### 发布到 ClawHub / OpenClaw
|
||||||
|
|
||||||
现在这个仓库支持把每个 `skills/baoyu-*` 目录作为独立 ClawHub skill 发布。
|
现在这个仓库支持把每个 `skills/baoyu-*` 目录作为独立 ClawHub skill 发布。
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import process from "node:process";
|
|||||||
import { setTimeout as delay } from "node:timers/promises";
|
import { setTimeout as delay } from "node:timers/promises";
|
||||||
import { GenError, type CliOptions, type GenerateResult } from "./types.ts";
|
import { GenError, type CliOptions, type GenerateResult } from "./types.ts";
|
||||||
import { runCodexExec } from "./spawn.ts";
|
import { runCodexExec } from "./spawn.ts";
|
||||||
import { findCpToTarget, verifyImageGenWasInvoked, verifyOutput } from "./validator.ts";
|
import { hasImageGenEvidence, verifyImageGenWasInvoked, verifyOutput } from "./validator.ts";
|
||||||
import { cacheKey, lookupCache, storeCache, FileLock } from "./cache.ts";
|
import { cacheKey, lookupCache, storeCache, FileLock } from "./cache.ts";
|
||||||
import { JsonLogger } from "./logger.ts";
|
import { JsonLogger } from "./logger.ts";
|
||||||
|
|
||||||
@@ -125,7 +125,7 @@ function buildInstruction(prompt: string, opts: CliOptions): string {
|
|||||||
const refHint = opts.refImages.length > 0
|
const refHint = opts.refImages.length > 0
|
||||||
? `\nREFERENCE IMAGES (attached above): ${opts.refImages.length} image(s) provided for style/composition guidance.\n`
|
? `\nREFERENCE IMAGES (attached above): ${opts.refImages.length} image(s) provided for style/composition guidance.\n`
|
||||||
: "";
|
: "";
|
||||||
return `You have an internal tool called image_gen for image generation. Use it.
|
return `You have an internal tool called image_gen for image generation. You MUST call it before doing anything else.
|
||||||
|
|
||||||
TASK: Generate an image with the spec below, then save to disk.
|
TASK: Generate an image with the spec below, then save to disk.
|
||||||
|
|
||||||
@@ -137,12 +137,15 @@ OUTPUT PATH: ${opts.outputPath}
|
|||||||
${refHint}
|
${refHint}
|
||||||
STEPS:
|
STEPS:
|
||||||
1. Call image_gen with the prompt and aspect ratio above${opts.refImages.length > 0 ? " (using the attached reference images for guidance)" : ""}.
|
1. Call image_gen with the prompt and aspect ratio above${opts.refImages.length > 0 ? " (using the attached reference images for guidance)" : ""}.
|
||||||
2. Move or copy the resulting image from Codex default location ($CODEX_HOME/generated_images/...) to: ${opts.outputPath}
|
2. Move or copy ONLY the image produced by that image_gen call from Codex default location ($CODEX_HOME/generated_images/...) to: ${opts.outputPath}
|
||||||
3. Verify with: ls -la ${opts.outputPath}
|
3. Verify with: ls -la ${opts.outputPath}
|
||||||
4. Reply with ONLY this JSON line (no markdown fences, no other text):
|
4. Reply with ONLY this JSON line (no markdown fences, no other text):
|
||||||
{"status":"ok","path":"${opts.outputPath}","bytes":<file_size_in_bytes>}
|
{"status":"ok","path":"${opts.outputPath}","bytes":<file_size_in_bytes>}
|
||||||
|
|
||||||
HARD CONSTRAINTS:
|
HARD CONSTRAINTS:
|
||||||
|
- Do NOT search for, find, inspect, reuse, or copy any pre-existing files from $CODEX_HOME/generated_images/ or any other directory.
|
||||||
|
- Do NOT run ls/find/rg/grep/glob over $CODEX_HOME/generated_images/ before image_gen has been called.
|
||||||
|
- You MUST call image_gen first. Only after image_gen completes may you copy the newly created file from this turn.
|
||||||
- Do NOT use curl, wget, Python, or any external API.
|
- Do NOT use curl, wget, Python, or any external API.
|
||||||
- Do NOT use bash to fabricate an image; only image_gen produces real pixels.
|
- Do NOT use bash to fabricate an image; only image_gen produces real pixels.
|
||||||
- Use ONLY the image_gen internal tool.`;
|
- Use ONLY the image_gen internal tool.`;
|
||||||
@@ -175,13 +178,16 @@ async function attemptGenerate(
|
|||||||
throw new GenError("agent_refused", "No thread id in event stream");
|
throw new GenError("agent_refused", "No thread id in event stream");
|
||||||
}
|
}
|
||||||
|
|
||||||
// verify: image_gen was actually invoked (check $CODEX_HOME/generated_images/{threadId}/)
|
// verify image_gen ran in THIS thread. A PNG in this thread's
|
||||||
|
// generated_images dir is the real signal (image_gen does not surface as a
|
||||||
|
// stream item); the stream check is a forward-compatible fallback. The #185
|
||||||
|
// shortcut (copying an unrelated history image) yields neither.
|
||||||
const ver = await verifyImageGenWasInvoked(run.threadId);
|
const ver = await verifyImageGenWasInvoked(run.threadId);
|
||||||
if (!ver.ok) {
|
if (!hasImageGenEvidence(run.toolCalls, ver.ok)) {
|
||||||
// secondary verify: did tool_calls include cp/mv from generated_images to our target
|
throw new GenError(
|
||||||
if (!findCpToTarget(run.toolCalls, opts.outputPath)) {
|
"no_image_gen_tool_use",
|
||||||
throw new GenError("no_image_gen_tool_use", `image_gen was not invoked: ${ver.reason}`);
|
`image_gen was not invoked (no image_gen event in stream; ${ver.reason})`,
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// verify output
|
// verify output
|
||||||
|
|||||||
@@ -33,17 +33,26 @@ test("parseEventStream tolerates malformed lines", () => {
|
|||||||
expect(r.usage?.input).toBe(1);
|
expect(r.usage?.input).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("hasImageGenInvocation finds shell calls touching generated_images", () => {
|
test("hasImageGenInvocation does not infer image_gen from shell copies", () => {
|
||||||
const r = parseEventStream(REAL_PoC_STREAM);
|
const r = parseEventStream(REAL_PoC_STREAM);
|
||||||
// image_gen itself is not an event; inferred via generated_images cp path
|
|
||||||
// this test only verifies parser behavior; driver logic lives in validator
|
|
||||||
const hasCp = r.toolCalls.some((tc) => tc.command?.includes("generated_images"));
|
const hasCp = r.toolCalls.some((tc) => tc.command?.includes("generated_images"));
|
||||||
expect(hasCp).toBe(true);
|
expect(hasCp).toBe(true);
|
||||||
|
expect(hasImageGenInvocation(r.toolCalls)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("hasImageGenInvocation (proper) returns false when no image_gen tool", () => {
|
test("hasImageGenInvocation detects real image_gen tool calls only", () => {
|
||||||
expect(hasImageGenInvocation([{ id: "1", tool: "shell", status: "completed" }])).toBe(false);
|
expect(hasImageGenInvocation([{ id: "1", tool: "shell", status: "completed" }])).toBe(false);
|
||||||
expect(
|
expect(
|
||||||
hasImageGenInvocation([{ id: "1", tool: "image_gen", status: "completed" }]),
|
hasImageGenInvocation([{ id: "1", tool: "image_gen", status: "completed" }]),
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("parseEventStream normalizes an image_generation item to image_gen", () => {
|
||||||
|
const stream = `{"type":"thread.started","thread_id":"t1"}
|
||||||
|
{"type":"item.started","item":{"id":"g0","type":"image_generation","status":"in_progress"}}
|
||||||
|
{"type":"item.completed","item":{"id":"g0","type":"image_generation","status":"completed"}}
|
||||||
|
{"type":"item.completed","item":{"id":"m0","type":"agent_message","text":"done"}}`;
|
||||||
|
const r = parseEventStream(stream);
|
||||||
|
expect(r.toolCalls.some((tc) => tc.tool === "image_gen")).toBe(true);
|
||||||
|
expect(hasImageGenInvocation(r.toolCalls)).toBe(true);
|
||||||
|
});
|
||||||
|
|||||||
@@ -2,11 +2,52 @@ import { test, expect } from "bun:test";
|
|||||||
import { mkdtemp, writeFile, rm, mkdir } from "node:fs/promises";
|
import { mkdtemp, writeFile, rm, mkdir } from "node:fs/promises";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { verifyOutput, verifyImageGenWasInvoked, findCpToTarget } from "./validator.ts";
|
import { verifyOutput, verifyImageGenWasInvoked, hasImageGenEvidence } from "./validator.ts";
|
||||||
|
import { parseEventStream } from "./parser.ts";
|
||||||
import { GenError } from "./types.ts";
|
import { GenError } from "./types.ts";
|
||||||
|
|
||||||
const PNG_HEADER = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
const PNG_HEADER = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
||||||
|
|
||||||
|
// Condensed + sanitized capture of a REAL successful `codex exec --json` run
|
||||||
|
// (thread 019edc1c…, 16:9 maple-tree image, single attempt). The image_gen
|
||||||
|
// tool leaves NO stream item — success shows only reasoning / command_execution
|
||||||
|
// / agent_message, and a `cp` from generated_images/<FULL-thread-id>/ig_*.png.
|
||||||
|
const REAL_SUCCESS_STREAM = `{"type":"thread.started","thread_id":"019edc1c-e7a3-74f0-a276-13bea71d32d6"}
|
||||||
|
{"type":"turn.started"}
|
||||||
|
{"type":"item.completed","item":{"id":"r0","type":"reasoning"}}
|
||||||
|
{"type":"item.completed","item":{"id":"r1","type":"reasoning"}}
|
||||||
|
{"type":"item.completed","item":{"id":"m0","type":"agent_message","text":"Image generation is complete. Locating the newly produced image and copying it to the requested path."}}
|
||||||
|
{"type":"item.started","item":{"id":"c0","type":"command_execution","command":"ls $CODEX_HOME/generated_images","status":"in_progress"}}
|
||||||
|
{"type":"item.completed","item":{"id":"c0","type":"command_execution","command":"ls $CODEX_HOME/generated_images","exit_code":1,"status":"failed"}}
|
||||||
|
{"type":"item.started","item":{"id":"c1","type":"command_execution","command":"cp $CODEX_HOME/generated_images/019edc1c-e7a3-74f0-a276-13bea71d32d6/ig_03eda661.png /Users/x/out/maple.png","status":"in_progress"}}
|
||||||
|
{"type":"item.completed","item":{"id":"c1","type":"command_execution","command":"cp $CODEX_HOME/generated_images/019edc1c-e7a3-74f0-a276-13bea71d32d6/ig_03eda661.png /Users/x/out/maple.png","exit_code":0,"status":"completed"}}
|
||||||
|
{"type":"item.completed","item":{"id":"m1","type":"agent_message","text":"{\\"status\\":\\"ok\\",\\"path\\":\\"/Users/x/out/maple.png\\",\\"bytes\\":1317377}"}}
|
||||||
|
{"type":"turn.completed","usage":{"input_tokens":49489,"cached_input_tokens":34432,"output_tokens":2463,"reasoning_output_tokens":1990}}`;
|
||||||
|
|
||||||
|
test("real success stream carries no image_gen item — gating on the stream alone would false-negative (#185)", () => {
|
||||||
|
const r = parseEventStream(REAL_SUCCESS_STREAM);
|
||||||
|
expect(r.threadId).toBe("019edc1c-e7a3-74f0-a276-13bea71d32d6");
|
||||||
|
// success path tools: reasoning / shell / agent_message — never image_gen
|
||||||
|
expect(r.toolCalls.map((tc) => tc.tool).sort()).toEqual([
|
||||||
|
"agent_message",
|
||||||
|
"agent_message",
|
||||||
|
"reasoning",
|
||||||
|
"reasoning",
|
||||||
|
"shell",
|
||||||
|
"shell",
|
||||||
|
]);
|
||||||
|
expect(r.toolCalls.some((tc) => tc.tool === "image_gen")).toBe(false);
|
||||||
|
// filesystem evidence (PNG in this thread's dir) is what must let it through
|
||||||
|
expect(hasImageGenEvidence(r.toolCalls, true)).toBe(true);
|
||||||
|
// with no filesystem evidence and no stream item, it is rejected (the shortcut)
|
||||||
|
expect(hasImageGenEvidence(r.toolCalls, false)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("hasImageGenEvidence accepts a real image_gen stream item even without a dir PNG", () => {
|
||||||
|
expect(hasImageGenEvidence([{ id: "1", tool: "image_gen", status: "completed" }], false)).toBe(true);
|
||||||
|
expect(hasImageGenEvidence([{ id: "1", tool: "shell", status: "completed" }], false)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
test("verifyOutput passes for valid PNG", async () => {
|
test("verifyOutput passes for valid PNG", async () => {
|
||||||
const dir = await mkdtemp(path.join(tmpdir(), "cig-val-"));
|
const dir = await mkdtemp(path.join(tmpdir(), "cig-val-"));
|
||||||
try {
|
try {
|
||||||
@@ -73,26 +114,3 @@ test("verifyImageGenWasInvoked true when PNG exists in thread dir", async () =>
|
|||||||
await rm(tempHome, { recursive: true, force: true });
|
await rm(tempHome, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("findCpToTarget detects cp from generated_images", () => {
|
|
||||||
expect(
|
|
||||||
findCpToTarget(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
id: "1",
|
|
||||||
tool: "shell",
|
|
||||||
status: "completed",
|
|
||||||
command: "cp ~/.codex/generated_images/thread/ig_x.png /tmp/out.png",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"/tmp/out.png",
|
|
||||||
),
|
|
||||||
).toBe(true);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
findCpToTarget(
|
|
||||||
[{ id: "1", tool: "shell", status: "completed", command: "ls /tmp" }],
|
|
||||||
"/tmp/out.png",
|
|
||||||
),
|
|
||||||
).toBe(false);
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { homedir } from "node:os";
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { GenError } from "./types.ts";
|
import { GenError } from "./types.ts";
|
||||||
import type { ToolCall } from "./types.ts";
|
import type { ToolCall } from "./types.ts";
|
||||||
|
import { hasImageGenInvocation } from "./parser.ts";
|
||||||
|
|
||||||
const PNG_MAGIC = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
const PNG_MAGIC = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
||||||
|
|
||||||
@@ -23,15 +24,15 @@ export async function verifyImageGenWasInvoked(threadId: string | null): Promise
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function findCpToTarget(toolCalls: ToolCall[], target: string): boolean {
|
// Real evidence that image_gen ran in THIS thread. Codex's image_gen tool does
|
||||||
return toolCalls.some(
|
// not surface as a stream item, so a successful run shows only reasoning/shell/
|
||||||
(tc) =>
|
// agent_message — `dirHasImage` (a PNG in this thread's generated_images dir) is
|
||||||
tc.tool === "shell" &&
|
// what proves it. The stream check is kept as a forward-compatible signal in
|
||||||
typeof tc.command === "string" &&
|
// case a future Codex version emits the item. The #185 shortcut (copying an
|
||||||
(tc.command.includes(target) || tc.command.includes(path.basename(target))) &&
|
// unrelated history image, which lives under a different thread id) yields
|
||||||
/\b(cp|mv|cat)\b/.test(tc.command) &&
|
// neither, so it is correctly rejected.
|
||||||
tc.command.includes("generated_images"),
|
export function hasImageGenEvidence(toolCalls: ToolCall[], dirHasImage: boolean): boolean {
|
||||||
);
|
return dirHasImage || hasImageGenInvocation(toolCalls);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function verifyOutput(outputPath: string): Promise<{ bytes: number }> {
|
export async function verifyOutput(outputPath: string): Promise<{ bytes: number }> {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import process from "node:process";
|
|||||||
import { setTimeout as delay } from "node:timers/promises";
|
import { setTimeout as delay } from "node:timers/promises";
|
||||||
import { GenError, type CliOptions, type GenerateResult } from "./types.ts";
|
import { GenError, type CliOptions, type GenerateResult } from "./types.ts";
|
||||||
import { runCodexExec } from "./spawn.ts";
|
import { runCodexExec } from "./spawn.ts";
|
||||||
import { findCpToTarget, verifyImageGenWasInvoked, verifyOutput } from "./validator.ts";
|
import { hasImageGenEvidence, verifyImageGenWasInvoked, verifyOutput } from "./validator.ts";
|
||||||
import { cacheKey, lookupCache, storeCache, FileLock } from "./cache.ts";
|
import { cacheKey, lookupCache, storeCache, FileLock } from "./cache.ts";
|
||||||
import { JsonLogger } from "./logger.ts";
|
import { JsonLogger } from "./logger.ts";
|
||||||
|
|
||||||
@@ -125,7 +125,7 @@ function buildInstruction(prompt: string, opts: CliOptions): string {
|
|||||||
const refHint = opts.refImages.length > 0
|
const refHint = opts.refImages.length > 0
|
||||||
? `\nREFERENCE IMAGES (attached above): ${opts.refImages.length} image(s) provided for style/composition guidance.\n`
|
? `\nREFERENCE IMAGES (attached above): ${opts.refImages.length} image(s) provided for style/composition guidance.\n`
|
||||||
: "";
|
: "";
|
||||||
return `You have an internal tool called image_gen for image generation. Use it.
|
return `You have an internal tool called image_gen for image generation. You MUST call it before doing anything else.
|
||||||
|
|
||||||
TASK: Generate an image with the spec below, then save to disk.
|
TASK: Generate an image with the spec below, then save to disk.
|
||||||
|
|
||||||
@@ -137,12 +137,15 @@ OUTPUT PATH: ${opts.outputPath}
|
|||||||
${refHint}
|
${refHint}
|
||||||
STEPS:
|
STEPS:
|
||||||
1. Call image_gen with the prompt and aspect ratio above${opts.refImages.length > 0 ? " (using the attached reference images for guidance)" : ""}.
|
1. Call image_gen with the prompt and aspect ratio above${opts.refImages.length > 0 ? " (using the attached reference images for guidance)" : ""}.
|
||||||
2. Move or copy the resulting image from Codex default location ($CODEX_HOME/generated_images/...) to: ${opts.outputPath}
|
2. Move or copy ONLY the image produced by that image_gen call from Codex default location ($CODEX_HOME/generated_images/...) to: ${opts.outputPath}
|
||||||
3. Verify with: ls -la ${opts.outputPath}
|
3. Verify with: ls -la ${opts.outputPath}
|
||||||
4. Reply with ONLY this JSON line (no markdown fences, no other text):
|
4. Reply with ONLY this JSON line (no markdown fences, no other text):
|
||||||
{"status":"ok","path":"${opts.outputPath}","bytes":<file_size_in_bytes>}
|
{"status":"ok","path":"${opts.outputPath}","bytes":<file_size_in_bytes>}
|
||||||
|
|
||||||
HARD CONSTRAINTS:
|
HARD CONSTRAINTS:
|
||||||
|
- Do NOT search for, find, inspect, reuse, or copy any pre-existing files from $CODEX_HOME/generated_images/ or any other directory.
|
||||||
|
- Do NOT run ls/find/rg/grep/glob over $CODEX_HOME/generated_images/ before image_gen has been called.
|
||||||
|
- You MUST call image_gen first. Only after image_gen completes may you copy the newly created file from this turn.
|
||||||
- Do NOT use curl, wget, Python, or any external API.
|
- Do NOT use curl, wget, Python, or any external API.
|
||||||
- Do NOT use bash to fabricate an image; only image_gen produces real pixels.
|
- Do NOT use bash to fabricate an image; only image_gen produces real pixels.
|
||||||
- Use ONLY the image_gen internal tool.`;
|
- Use ONLY the image_gen internal tool.`;
|
||||||
@@ -175,13 +178,16 @@ async function attemptGenerate(
|
|||||||
throw new GenError("agent_refused", "No thread id in event stream");
|
throw new GenError("agent_refused", "No thread id in event stream");
|
||||||
}
|
}
|
||||||
|
|
||||||
// verify: image_gen was actually invoked (check $CODEX_HOME/generated_images/{threadId}/)
|
// verify image_gen ran in THIS thread. A PNG in this thread's
|
||||||
|
// generated_images dir is the real signal (image_gen does not surface as a
|
||||||
|
// stream item); the stream check is a forward-compatible fallback. The #185
|
||||||
|
// shortcut (copying an unrelated history image) yields neither.
|
||||||
const ver = await verifyImageGenWasInvoked(run.threadId);
|
const ver = await verifyImageGenWasInvoked(run.threadId);
|
||||||
if (!ver.ok) {
|
if (!hasImageGenEvidence(run.toolCalls, ver.ok)) {
|
||||||
// secondary verify: did tool_calls include cp/mv from generated_images to our target
|
throw new GenError(
|
||||||
if (!findCpToTarget(run.toolCalls, opts.outputPath)) {
|
"no_image_gen_tool_use",
|
||||||
throw new GenError("no_image_gen_tool_use", `image_gen was not invoked: ${ver.reason}`);
|
`image_gen was not invoked (no image_gen event in stream; ${ver.reason})`,
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// verify output
|
// verify output
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { homedir } from "node:os";
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { GenError } from "./types.ts";
|
import { GenError } from "./types.ts";
|
||||||
import type { ToolCall } from "./types.ts";
|
import type { ToolCall } from "./types.ts";
|
||||||
|
import { hasImageGenInvocation } from "./parser.ts";
|
||||||
|
|
||||||
const PNG_MAGIC = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
const PNG_MAGIC = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
||||||
|
|
||||||
@@ -23,15 +24,15 @@ export async function verifyImageGenWasInvoked(threadId: string | null): Promise
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function findCpToTarget(toolCalls: ToolCall[], target: string): boolean {
|
// Real evidence that image_gen ran in THIS thread. Codex's image_gen tool does
|
||||||
return toolCalls.some(
|
// not surface as a stream item, so a successful run shows only reasoning/shell/
|
||||||
(tc) =>
|
// agent_message — `dirHasImage` (a PNG in this thread's generated_images dir) is
|
||||||
tc.tool === "shell" &&
|
// what proves it. The stream check is kept as a forward-compatible signal in
|
||||||
typeof tc.command === "string" &&
|
// case a future Codex version emits the item. The #185 shortcut (copying an
|
||||||
(tc.command.includes(target) || tc.command.includes(path.basename(target))) &&
|
// unrelated history image, which lives under a different thread id) yields
|
||||||
/\b(cp|mv|cat)\b/.test(tc.command) &&
|
// neither, so it is correctly rejected.
|
||||||
tc.command.includes("generated_images"),
|
export function hasImageGenEvidence(toolCalls: ToolCall[], dirHasImage: boolean): boolean {
|
||||||
);
|
return dirHasImage || hasImageGenInvocation(toolCalls);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function verifyOutput(outputPath: string): Promise<{ bytes: number }> {
|
export async function verifyOutput(outputPath: string): Promise<{ bytes: number }> {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
name: baoyu-post-to-wechat
|
name: baoyu-post-to-wechat
|
||||||
description: Posts content to WeChat Official Account (微信公众号) via API or Chrome CDP. Supports article posting (文章) with HTML, markdown, or plain text input, and image-text posting (贴图, formerly 图文) with multiple images. Markdown article workflows default to converting ordinary external links into bottom citations for WeChat-friendly output. Use when user mentions "发布公众号", "post to wechat", "微信公众号", or "贴图/图文/文章".
|
description: Posts content to WeChat Official Account (微信公众号) via API or Chrome CDP. Supports article posting (文章) with HTML, markdown, or plain text input, and image-text posting (贴图, formerly 图文) with multiple images. Markdown article workflows default to converting ordinary external links into bottom citations for WeChat-friendly output. Use when user mentions "发布公众号", "post to wechat", "微信公众号", or "贴图/图文/文章".
|
||||||
version: 1.118.1
|
version: 1.118.2
|
||||||
metadata:
|
metadata:
|
||||||
openclaw:
|
openclaw:
|
||||||
homepage: https://github.com/JimLiu/baoyu-skills#baoyu-post-to-wechat
|
homepage: https://github.com/JimLiu/baoyu-skills#baoyu-post-to-wechat
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ description: >-
|
|||||||
"localize", "本地化", "refined translation", "精细翻译", "proofread translation", "快速翻译", "快翻",
|
"localize", "本地化", "refined translation", "精细翻译", "proofread translation", "快速翻译", "快翻",
|
||||||
"这篇文章翻译一下", or provides a URL/file with translation intent. Supports three modes
|
"这篇文章翻译一下", or provides a URL/file with translation intent. Supports three modes
|
||||||
(quick/normal/refined) with custom glossary support.
|
(quick/normal/refined) with custom glossary support.
|
||||||
version: 1.59.0
|
version: 1.117.3
|
||||||
metadata:
|
metadata:
|
||||||
openclaw:
|
openclaw:
|
||||||
homepage: https://github.com/JimLiu/baoyu-skills#baoyu-translate
|
homepage: https://github.com/JimLiu/baoyu-skills#baoyu-translate
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
name: baoyu-wechat-summary
|
name: baoyu-wechat-summary
|
||||||
description: Summarizes WeChat group chat highlights into a structured digest using the local wx-cli binary (https://github.com/jackwener/wx-cli). Generates a normal digest by default; a roast (毒舌) version is opt-in. Maintains per-group history (history.json + history-digests.jsonl), per-user profiles, and per-group fact memory (memory.md) across runs, with privacy guardrails baked in. Use when the user asks to "总结群聊", "群聊精华", "群聊摘要", "summarize group chat", "group chat digest", mentions a WeChat group name with a time range, says "帮我看看 XX 群最近聊了什么", "XX 群有什么值得看的", or asks to "回溯画像" / "初始化画像" / "backfill profiles". Adds the roast version when the user says "毒舌版", "roast 版", "再来个毒舌的", or similar.
|
description: Summarizes WeChat group chat highlights into a structured digest using the local wx-cli binary (https://github.com/jackwener/wx-cli). Generates a normal digest by default; a roast (毒舌) version is opt-in. Maintains per-group history (history.json + history-digests.jsonl), per-user profiles, and per-group fact memory (memory.md) across runs, with privacy guardrails baked in. Use when the user asks to "总结群聊", "群聊精华", "群聊摘要", "summarize group chat", "group chat digest", mentions a WeChat group name with a time range, says "帮我看看 XX 群最近聊了什么", "XX 群有什么值得看的", or asks to "回溯画像" / "初始化画像" / "backfill profiles". Adds the roast version when the user says "毒舌版", "roast 版", "再来个毒舌的", or similar.
|
||||||
version: 1.117.3
|
version: 1.117.4
|
||||||
metadata:
|
metadata:
|
||||||
openclaw:
|
openclaw:
|
||||||
homepage: https://github.com/JimLiu/baoyu-skills#baoyu-wechat-summary
|
homepage: https://github.com/JimLiu/baoyu-skills#baoyu-wechat-summary
|
||||||
|
|||||||
Reference in New Issue
Block a user