mirror of
https://github.com/JimLiu/baoyu-skills.git
synced 2026-07-14 06:39:48 +08:00
Compare commits
8 Commits
v2.5.1
...
c9a50cc908
| Author | SHA1 | Date | |
|---|---|---|---|
| c9a50cc908 | |||
| 348dc82612 | |||
| dba3d732c3 | |||
| 661bdd08ff | |||
| f490f7e5e3 | |||
| 22031aa2d2 | |||
| 51b31aa050 | |||
| 441ca307a6 |
@@ -6,7 +6,7 @@
|
||||
},
|
||||
"metadata": {
|
||||
"description": "Skills shared by Baoyu for improving daily work efficiency",
|
||||
"version": "2.5.1"
|
||||
"version": "2.5.2"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
|
||||
@@ -2,6 +2,14 @@
|
||||
|
||||
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
|
||||
|
||||
### Documentation
|
||||
|
||||
@@ -2,6 +2,14 @@
|
||||
|
||||
[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
|
||||
|
||||
### 文档
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Jim Liu
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -19,6 +19,31 @@ Skills shared by Baoyu for improving daily work efficiency with AI Agents (Claud
|
||||
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
|
||||
|
||||
This repository now supports publishing each `skills/baoyu-*` directory as an individual ClawHub skill.
|
||||
@@ -1362,7 +1387,9 @@ This project was inspired by and builds upon the following open source projects:
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
Unless otherwise noted, this repository is licensed under the [MIT License](./LICENSE).
|
||||
|
||||
Published ClawHub skills follow ClawHub registry rules and are distributed under `MIT-0`. Third-party code and assets retain their original licenses where noted.
|
||||
|
||||
## Star History
|
||||
|
||||
|
||||
+28
-1
@@ -29,6 +29,31 @@
|
||||
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
|
||||
|
||||
现在这个仓库支持把每个 `skills/baoyu-*` 目录作为独立 ClawHub skill 发布。
|
||||
@@ -1363,7 +1388,9 @@ HTTP_PROXY=http://127.0.0.1:7890 HTTPS_PROXY=http://127.0.0.1:7890 /baoyu-danger
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT
|
||||
除另有说明外,本仓库采用 [MIT License](./LICENSE) 授权。
|
||||
|
||||
发布到 ClawHub 的 skill 根据 ClawHub registry 规则以 `MIT-0` 分发。第三方代码和素材按其注明的原始许可授权。
|
||||
|
||||
## Star History
|
||||
|
||||
|
||||
Generated
+5
@@ -5,6 +5,7 @@
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "baoyu-skills",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
@@ -5018,12 +5019,14 @@
|
||||
},
|
||||
"packages/baoyu-chrome-cdp": {
|
||||
"version": "0.1.1",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"bun": ">=1.2.0"
|
||||
}
|
||||
},
|
||||
"packages/baoyu-codex-imagegen": {
|
||||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"codex-imagegen": "src/main.ts"
|
||||
},
|
||||
@@ -5033,6 +5036,7 @@
|
||||
},
|
||||
"packages/baoyu-fetch": {
|
||||
"version": "0.1.2",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mozilla/readability": "^0.6.0",
|
||||
"chrome-launcher": "^1.2.1",
|
||||
@@ -5061,6 +5065,7 @@
|
||||
},
|
||||
"packages/baoyu-md": {
|
||||
"version": "0.1.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fflate": "^0.8.2",
|
||||
"front-matter": "^4.0.2",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "baoyu-skills",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "baoyu-chrome-cdp",
|
||||
"version": "0.1.1",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"files": [
|
||||
"dist",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "baoyu-codex-imagegen",
|
||||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"description": "Generate images via Codex CLI's built-in image_gen tool from non-Codex runtimes (Claude Code, Hermes, …).",
|
||||
"bin": {
|
||||
|
||||
@@ -6,7 +6,7 @@ import process from "node:process";
|
||||
import { setTimeout as delay } from "node:timers/promises";
|
||||
import { GenError, type CliOptions, type GenerateResult } from "./types.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 { JsonLogger } from "./logger.ts";
|
||||
|
||||
@@ -125,7 +125,7 @@ function buildInstruction(prompt: string, opts: CliOptions): string {
|
||||
const refHint = opts.refImages.length > 0
|
||||
? `\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.
|
||||
|
||||
@@ -137,12 +137,15 @@ OUTPUT PATH: ${opts.outputPath}
|
||||
${refHint}
|
||||
STEPS:
|
||||
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}
|
||||
4. Reply with ONLY this JSON line (no markdown fences, no other text):
|
||||
{"status":"ok","path":"${opts.outputPath}","bytes":<file_size_in_bytes>}
|
||||
|
||||
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 bash to fabricate an image; only image_gen produces real pixels.
|
||||
- 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");
|
||||
}
|
||||
|
||||
// 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);
|
||||
if (!ver.ok) {
|
||||
// secondary verify: did tool_calls include cp/mv from generated_images to our target
|
||||
if (!findCpToTarget(run.toolCalls, opts.outputPath)) {
|
||||
throw new GenError("no_image_gen_tool_use", `image_gen was not invoked: ${ver.reason}`);
|
||||
}
|
||||
if (!hasImageGenEvidence(run.toolCalls, ver.ok)) {
|
||||
throw new GenError(
|
||||
"no_image_gen_tool_use",
|
||||
`image_gen was not invoked (no image_gen event in stream; ${ver.reason})`,
|
||||
);
|
||||
}
|
||||
|
||||
// verify output
|
||||
|
||||
@@ -33,17 +33,26 @@ test("parseEventStream tolerates malformed lines", () => {
|
||||
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);
|
||||
// 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"));
|
||||
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: "image_gen", status: "completed" }]),
|
||||
).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 { tmpdir } from "node:os";
|
||||
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";
|
||||
|
||||
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 () => {
|
||||
const dir = await mkdtemp(path.join(tmpdir(), "cig-val-"));
|
||||
try {
|
||||
@@ -73,26 +114,3 @@ test("verifyImageGenWasInvoked true when PNG exists in thread dir", async () =>
|
||||
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 { GenError } 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]);
|
||||
|
||||
@@ -23,15 +24,15 @@ export async function verifyImageGenWasInvoked(threadId: string | null): Promise
|
||||
}
|
||||
}
|
||||
|
||||
export function findCpToTarget(toolCalls: ToolCall[], target: string): boolean {
|
||||
return toolCalls.some(
|
||||
(tc) =>
|
||||
tc.tool === "shell" &&
|
||||
typeof tc.command === "string" &&
|
||||
(tc.command.includes(target) || tc.command.includes(path.basename(target))) &&
|
||||
/\b(cp|mv|cat)\b/.test(tc.command) &&
|
||||
tc.command.includes("generated_images"),
|
||||
);
|
||||
// Real evidence that image_gen ran in THIS thread. Codex's image_gen tool does
|
||||
// not surface as a stream item, so a successful run shows only reasoning/shell/
|
||||
// agent_message — `dirHasImage` (a PNG in this thread's generated_images dir) is
|
||||
// what proves it. The stream check is kept as a forward-compatible signal in
|
||||
// case a future Codex version emits the item. The #185 shortcut (copying an
|
||||
// unrelated history image, which lives under a different thread id) yields
|
||||
// neither, so it is correctly rejected.
|
||||
export function hasImageGenEvidence(toolCalls: ToolCall[], dirHasImage: boolean): boolean {
|
||||
return dirHasImage || hasImageGenInvocation(toolCalls);
|
||||
}
|
||||
|
||||
export async function verifyOutput(outputPath: string): Promise<{ bytes: number }> {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "baoyu-fetch",
|
||||
"version": "0.1.2",
|
||||
"license": "MIT",
|
||||
"description": "Read URLs into high-quality Markdown or JSON with Chrome CDP and site adapters.",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "baoyu-md",
|
||||
"version": "0.1.1",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.js",
|
||||
|
||||
@@ -456,19 +456,19 @@ export class GeminiClient extends GemMixin {
|
||||
const generated_images: GeneratedImage[] = [];
|
||||
const wants_generated =
|
||||
get_nested_value(candidate, [12, 7, 0], null) != null ||
|
||||
/http:\/\/googleusercontent\.com\/image_generation_content\/\d+/.test(text);
|
||||
/http:\/\/googleusercontent\.com\/image_generation_content\/\d+/.test(text) ||
|
||||
// Gemini 3 no longer emits the legacy marker in the candidate text; the generated
|
||||
// image arrives in a later response part. Detect it from the raw response instead.
|
||||
/\/gg-dl\//.test(txt) ||
|
||||
/image_generation_content\/\d+/.test(txt);
|
||||
|
||||
if (wants_generated) {
|
||||
const image_part = find_generated_image_part(response_json as unknown[], body_index, candidate_index, 1);
|
||||
const img_body = image_part?.body ?? null;
|
||||
|
||||
if (!img_body) {
|
||||
throw new ImageGenerationError(
|
||||
'Failed to parse generated images. Please update gemini_webapi to the latest version. If the error persists and is caused by the package, please report it on GitHub.',
|
||||
);
|
||||
}
|
||||
|
||||
const img_candidate = get_nested_value<unknown[]>(img_body, [4, candidate_index], []);
|
||||
// Not every candidate carries a generated image (e.g. multi-candidate responses).
|
||||
// If this candidate has no image part, skip extraction instead of failing the whole call.
|
||||
const img_candidate = img_body ? get_nested_value<unknown[]>(img_body, [4, candidate_index], []) : [];
|
||||
const finished = get_nested_value<string | null>(img_candidate, [1, 0], null);
|
||||
if (finished) {
|
||||
text = finished.replace(/http:\/\/googleusercontent\.com\/image_generation_content\/\d+/g, '').trimEnd();
|
||||
|
||||
@@ -6,7 +6,7 @@ import process from "node:process";
|
||||
import { setTimeout as delay } from "node:timers/promises";
|
||||
import { GenError, type CliOptions, type GenerateResult } from "./types.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 { JsonLogger } from "./logger.ts";
|
||||
|
||||
@@ -125,7 +125,7 @@ function buildInstruction(prompt: string, opts: CliOptions): string {
|
||||
const refHint = opts.refImages.length > 0
|
||||
? `\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.
|
||||
|
||||
@@ -137,12 +137,15 @@ OUTPUT PATH: ${opts.outputPath}
|
||||
${refHint}
|
||||
STEPS:
|
||||
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}
|
||||
4. Reply with ONLY this JSON line (no markdown fences, no other text):
|
||||
{"status":"ok","path":"${opts.outputPath}","bytes":<file_size_in_bytes>}
|
||||
|
||||
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 bash to fabricate an image; only image_gen produces real pixels.
|
||||
- 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");
|
||||
}
|
||||
|
||||
// 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);
|
||||
if (!ver.ok) {
|
||||
// secondary verify: did tool_calls include cp/mv from generated_images to our target
|
||||
if (!findCpToTarget(run.toolCalls, opts.outputPath)) {
|
||||
throw new GenError("no_image_gen_tool_use", `image_gen was not invoked: ${ver.reason}`);
|
||||
}
|
||||
if (!hasImageGenEvidence(run.toolCalls, ver.ok)) {
|
||||
throw new GenError(
|
||||
"no_image_gen_tool_use",
|
||||
`image_gen was not invoked (no image_gen event in stream; ${ver.reason})`,
|
||||
);
|
||||
}
|
||||
|
||||
// verify output
|
||||
|
||||
@@ -3,6 +3,7 @@ import { homedir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { GenError } 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]);
|
||||
|
||||
@@ -23,15 +24,15 @@ export async function verifyImageGenWasInvoked(threadId: string | null): Promise
|
||||
}
|
||||
}
|
||||
|
||||
export function findCpToTarget(toolCalls: ToolCall[], target: string): boolean {
|
||||
return toolCalls.some(
|
||||
(tc) =>
|
||||
tc.tool === "shell" &&
|
||||
typeof tc.command === "string" &&
|
||||
(tc.command.includes(target) || tc.command.includes(path.basename(target))) &&
|
||||
/\b(cp|mv|cat)\b/.test(tc.command) &&
|
||||
tc.command.includes("generated_images"),
|
||||
);
|
||||
// Real evidence that image_gen ran in THIS thread. Codex's image_gen tool does
|
||||
// not surface as a stream item, so a successful run shows only reasoning/shell/
|
||||
// agent_message — `dirHasImage` (a PNG in this thread's generated_images dir) is
|
||||
// what proves it. The stream check is kept as a forward-compatible signal in
|
||||
// case a future Codex version emits the item. The #185 shortcut (copying an
|
||||
// unrelated history image, which lives under a different thread id) yields
|
||||
// neither, so it is correctly rejected.
|
||||
export function hasImageGenEvidence(toolCalls: ToolCall[], dirHasImage: boolean): boolean {
|
||||
return dirHasImage || hasImageGenInvocation(toolCalls);
|
||||
}
|
||||
|
||||
export async function verifyOutput(outputPath: string): Promise<{ bytes: number }> {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
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 "贴图/图文/文章".
|
||||
version: 1.118.1
|
||||
version: 1.118.2
|
||||
metadata:
|
||||
openclaw:
|
||||
homepage: https://github.com/JimLiu/baoyu-skills#baoyu-post-to-wechat
|
||||
|
||||
@@ -98,3 +98,68 @@ test('parseMarkdown resolves encoded spaces and literal percent image paths', as
|
||||
assert.equal(result.contentImages[0]?.localPath, path.join(root, 'Pasted image.png'));
|
||||
assert.equal(result.contentImages[1]?.localPath, path.join(root, '100%.png'));
|
||||
});
|
||||
|
||||
test('parseMarkdown renders CJK-adjacent bold and italics (no literal asterisks)', async (t) => {
|
||||
const root = await makeTempDir('x-md-to-html-cjk-bold-');
|
||||
t.after(() => fs.rm(root, { recursive: true, force: true }));
|
||||
|
||||
const markdownPath = path.join(root, 'post.md');
|
||||
const tempDir = path.join(root, 'tmp');
|
||||
await fs.mkdir(tempDir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
markdownPath,
|
||||
[
|
||||
'# 标题',
|
||||
'',
|
||||
'分工在变细。**国际大厂卷基础设施,中文项目卷场景落地。**这其实是生态成熟的表现。',
|
||||
'',
|
||||
'半角场景 **Top 10 里平均有 8 个** 项目。',
|
||||
'',
|
||||
'斜体 *数据来源 GitHub* 收尾。',
|
||||
'',
|
||||
'参考 **[docs][d]** 了解更多。',
|
||||
'',
|
||||
'[d]: https://example.com',
|
||||
].join('\n'),
|
||||
);
|
||||
|
||||
const result = await parseMarkdown(markdownPath, { tempDir });
|
||||
|
||||
// Bold directly adjacent to CJK (closing ** followed by CJK) must render.
|
||||
assert.match(result.html, /<strong>国际大厂卷基础设施,中文项目卷场景落地。<\/strong>/);
|
||||
assert.match(result.html, /<strong>Top 10 里平均有 8 个<\/strong>/);
|
||||
// Italics.
|
||||
assert.match(result.html, /<em>数据来源 GitHub<\/em>/);
|
||||
// Reference-style links inside emphasis must render as links, not plain text.
|
||||
assert.match(result.html, /<strong><a href="https:\/\/example\.com" rel="noopener noreferrer nofollow">docs<\/a><\/strong>/);
|
||||
// No literal emphasis delimiters should leak into the output.
|
||||
assert.doesNotMatch(result.html, /\*\*/);
|
||||
assert.doesNotMatch(result.html, /(?<!\*)\*(?!\*)[^*\n]+\*(?!\*)/);
|
||||
});
|
||||
|
||||
test('parseMarkdown does not decode author-written literal HTML entities into tags', async (t) => {
|
||||
const root = await makeTempDir('x-md-to-html-entities-');
|
||||
t.after(() => fs.rm(root, { recursive: true, force: true }));
|
||||
|
||||
const markdownPath = path.join(root, 'post.md');
|
||||
const tempDir = path.join(root, 'tmp');
|
||||
await fs.mkdir(tempDir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
markdownPath,
|
||||
[
|
||||
'# 标题',
|
||||
'',
|
||||
'正文中写 <b>literal</b> 想显示字面标签。**加粗**收尾。',
|
||||
'',
|
||||
'代码里写 `<b>` 同样保留。',
|
||||
].join('\n'),
|
||||
);
|
||||
|
||||
const result = await parseMarkdown(markdownPath, { tempDir });
|
||||
|
||||
// CJK-adjacent bold still renders.
|
||||
assert.match(result.html, /<strong>加粗<\/strong>/);
|
||||
// Author-written literal entities must NOT be decoded into real tags.
|
||||
assert.doesNotMatch(result.html, /<b>literal<\/b>/);
|
||||
assert.match(result.html, /<b>literal<\/b>/);
|
||||
});
|
||||
|
||||
@@ -136,6 +136,21 @@ function highlightCode(code: string, lang: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize CJK-adjacent emphasis so `marked` renders it correctly.
|
||||
//
|
||||
// `marked`'s emphasis tokenizer treats a closing `**`/`*` directly followed by a
|
||||
// CJK character as not right-flanking, so it leaves the delimiters literal
|
||||
// (e.g. `**加粗**这` renders as plain text with the asterisks intact). We round-trip
|
||||
// the markdown through `remark-cjk-friendly`, whose stringify serializes the
|
||||
// boundary character as an HTML entity (`这`); the entity is treated as
|
||||
// punctuation by `marked`'s flanking rules, so emphasis parses as expected.
|
||||
//
|
||||
// We deliberately do NOT decode the entities afterward. They are valid HTML
|
||||
// character references that render correctly when the article HTML is pasted into
|
||||
// the X editor, and `marked` only emits them for characters outside the emphasis
|
||||
// span (the boundary char), never inside it. A blanket decode of the rendered
|
||||
// HTML would risk turning author-written literal entities (e.g. `<b>`
|
||||
// meant to display `<b>` as text) into real tags, so we leave them intact.
|
||||
function preprocessCjkMarkdown(markdown: string): string {
|
||||
try {
|
||||
const processor = unified()
|
||||
@@ -143,8 +158,7 @@ function preprocessCjkMarkdown(markdown: string): string {
|
||||
.use(remarkCjkFriendly)
|
||||
.use(remarkStringify);
|
||||
|
||||
const result = String(processor.processSync(markdown));
|
||||
return result.replace(/&#x([0-9A-Fa-f]+);/g, (_, hex: string) => String.fromCodePoint(parseInt(hex, 16)));
|
||||
return String(processor.processSync(markdown));
|
||||
} catch {
|
||||
return markdown;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ description: >-
|
||||
"localize", "本地化", "refined translation", "精细翻译", "proofread translation", "快速翻译", "快翻",
|
||||
"这篇文章翻译一下", or provides a URL/file with translation intent. Supports three modes
|
||||
(quick/normal/refined) with custom glossary support.
|
||||
version: 1.59.0
|
||||
version: 1.117.3
|
||||
metadata:
|
||||
openclaw:
|
||||
homepage: https://github.com/JimLiu/baoyu-skills#baoyu-translate
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
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.
|
||||
version: 1.117.3
|
||||
version: 1.117.4
|
||||
metadata:
|
||||
openclaw:
|
||||
homepage: https://github.com/JimLiu/baoyu-skills#baoyu-wechat-summary
|
||||
|
||||
Reference in New Issue
Block a user