From e0ee8c2e2ac2fbc44c2697011068073b810f516c Mon Sep 17 00:00:00 2001 From: karuo Date: Thu, 12 Mar 2026 23:10:26 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=84=20=E5=8D=A1=E8=8B=A5AI=20=E5=90=8C?= =?UTF-8?q?=E6=AD=A5=202026-03-12=2023:10=20|=20=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=EF=BC=9A=E6=B0=B4=E6=A1=A5=E5=B9=B3=E5=8F=B0=E5=AF=B9=E6=8E=A5?= =?UTF-8?q?=E3=80=81=E5=8D=A1=E6=9C=A8=E3=80=81=E6=80=BB=E7=B4=A2=E5=BC=95?= =?UTF-8?q?=E4=B8=8E=E5=85=A5=E5=8F=A3=E3=80=81=E8=BF=90=E8=90=A5=E4=B8=AD?= =?UTF-8?q?=E6=9E=A2=E5=B7=A5=E4=BD=9C=E5=8F=B0=20|=20=E6=8E=92=E9=99=A4?= =?UTF-8?q?=20>20MB:=2011=20=E4=B8=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../飞书管理/脚本/.feishu_tokens.json | 6 +- .../飞书管理/脚本/feishu_publish_blocks_with_images.py | 40 +- .../飞书管理/脚本/md_to_feishu_json.py | 187 ++++-- .../飞书管理/脚本/send_review_to_feishu_webhook.py | 82 +++ .../木识_软件识形/CLI万能化/SKILL.md | 206 ++++++ .../CLI万能化/harness_templates/repl_skin.py | 498 ++++++++++++++ .../木识_软件识形/CLI万能化/参考资料/HARNESS.md | 622 ++++++++++++++++++ 03_卡木(木)/木识_软件识形/SKILL.md | 25 + BOOTSTRAP.md | 2 +- SKILL_REGISTRY.md | 1 + 运营中枢/工作台/gitea_push_log.md | 1 + 运营中枢/工作台/代码管理.md | 1 + 12 files changed, 1603 insertions(+), 68 deletions(-) create mode 100644 02_卡人(水)/水桥_平台对接/飞书管理/脚本/send_review_to_feishu_webhook.py create mode 100644 03_卡木(木)/木识_软件识形/CLI万能化/SKILL.md create mode 100644 03_卡木(木)/木识_软件识形/CLI万能化/harness_templates/repl_skin.py create mode 100644 03_卡木(木)/木识_软件识形/CLI万能化/参考资料/HARNESS.md create mode 100644 03_卡木(木)/木识_软件识形/SKILL.md diff --git a/02_卡人(水)/水桥_平台对接/飞书管理/脚本/.feishu_tokens.json b/02_卡人(水)/水桥_平台对接/飞书管理/脚本/.feishu_tokens.json index 48e30ba7..937f3d3c 100644 --- a/02_卡人(水)/水桥_平台对接/飞书管理/脚本/.feishu_tokens.json +++ b/02_卡人(水)/水桥_平台对接/飞书管理/脚本/.feishu_tokens.json @@ -1,6 +1,6 @@ { - "access_token": "u-e9uHxerhZ7VrnwkCFC9Yvalh3Ix1ghWXpgGaZMk0260Y", - "refresh_token": "ur-dIaKx4ssV4SU.karG6lt9ulh1C11ghopOgGaYx00261E", + "access_token": "u-e1gGA5qAp2yXEyJOa2k9Zalh3KxxghONV0GaJwk0274U", + "refresh_token": "ur-cuP.iNyXxflV6qpKn0c2Lnlh3AxxghqPpwGaIQ4022gJ", "name": "飞书用户", - "auth_time": "2026-03-12T20:33:34.954705" + "auth_time": "2026-03-12T22:44:05.347461" } \ No newline at end of file diff --git a/02_卡人(水)/水桥_平台对接/飞书管理/脚本/feishu_publish_blocks_with_images.py b/02_卡人(水)/水桥_平台对接/飞书管理/脚本/feishu_publish_blocks_with_images.py index 53d5e786..2bb5f680 100644 --- a/02_卡人(水)/水桥_平台对接/飞书管理/脚本/feishu_publish_blocks_with_images.py +++ b/02_卡人(水)/水桥_平台对接/飞书管理/脚本/feishu_publish_blocks_with_images.py @@ -326,18 +326,29 @@ def _get_text_content(block: dict) -> str: return (tr.get("content") or "") +def _get_elements_content(elements: list) -> str: + if not elements: + return "" + return (elements[0].get("text_run") or {}).get("content", "") or "" + + def sanitize_blocks(blocks: list) -> list: - """ - 飞书 docx blocks 对“空段落/异常结构”会严格校验。 - 这里做一次轻量清洗:去掉纯空文本块,避免 invalid param。 - """ + """飞书 docx blocks 轻量清洗:去掉空文本/空代码块/空 callout,避免 invalid param。""" out = [] for b in blocks: if not isinstance(b, dict): continue - if b.get("block_type") == 2: - c = _get_text_content(b) - if not c or not c.strip(): + bt = b.get("block_type") + if bt == 2: + if not _get_text_content(b).strip(): + continue + elif bt == 14: + elems = (b.get("code") or {}).get("elements") or [] + if not _get_elements_content(elems).strip(): + continue + elif bt == 19: + elems = (b.get("callout") or {}).get("elements") or [] + if not _get_elements_content(elems).strip(): continue out.append(b) return out @@ -438,6 +449,21 @@ def _write_batch_with_fallback(doc_token: str, headers: dict, batch: list, total if r1.get("code") == 0: time.sleep(0.35) continue + bt = b.get("block_type") + # code(14) 和 callout(19) 失败时降级为文本块 + fallback_content = "" + if bt == 14: + elems = (b.get("code") or {}).get("elements") or [] + fallback_content = _get_elements_content(elems) + elif bt == 19: + elems = (b.get("callout") or {}).get("elements") or [] + fallback_content = _get_elements_content(elems) + if fallback_content: + r2 = _post_children(doc_token, headers, [{"block_type": 2, "text": {"elements": [{"text_run": {"content": fallback_content, "text_element_style": {}}}], "style": {}}}], None) + if r2.get("code") == 0: + print(f"⚠️ block_type={bt} 降级为文本块写入") + time.sleep(0.35) + continue c = _get_text_content(b) preview = (c[:60] + "...") if c and len(c) > 60 else (c or "") print(f"⚠️ 跳过非法块: code={r1.get('code')} msg={r1.get('msg')} preview={preview!r}") diff --git a/02_卡人(水)/水桥_平台对接/飞书管理/脚本/md_to_feishu_json.py b/02_卡人(水)/水桥_平台对接/飞书管理/脚本/md_to_feishu_json.py index c533b1a0..24e5baa4 100644 --- a/02_卡人(水)/水桥_平台对接/飞书管理/脚本/md_to_feishu_json.py +++ b/02_卡人(水)/水桥_平台对接/飞书管理/脚本/md_to_feishu_json.py @@ -1,7 +1,12 @@ #!/usr/bin/env python3 """ -将 Markdown 本地转换为飞书文档 JSON 格式。 -图片用占位符 __IMAGE:路径__ 标注,上传时替换为 file_token。 +将 Markdown 本地转换为飞书文档 JSON 格式(v2)。 +· 代码围栏 → block_type:14 code block(保留语言标注) +· > 引用 → block_type:19 callout(蓝色背景) +· --- → block_type:22 divider +· #### → block_type:6 heading4 +· 图片 → __IMAGE__ 占位符,上传时替换为 file_token +· 表格 → block_type:30 sheet(超出 9×9 自动截断) 用法: python3 md_to_feishu_json.py input.md output.json @@ -14,24 +19,71 @@ import argparse from pathlib import Path -def _h1(t): +# ── 语言代码映射(飞书 code block style.language) ────────────────────────── +LANG_MAP: dict[str, int] = { + "python": 2, "py": 2, + "javascript": 3, "js": 3, + "typescript": 3, "ts": 3, + "shell": 6, "bash": 6, "sh": 6, + "sql": 8, + "json": 9, + "html": 11, "xml": 11, + "go": 16, + "rust": 22, +} + + +# ── Block 构造函数 ──────────────────────────────────────────────────────────── + +def _h1(t: str) -> dict: return {"block_type": 3, "heading1": {"elements": [{"text_run": {"content": t, "text_element_style": {}}}], "style": {}}} -def _h2(t): +def _h2(t: str) -> dict: return {"block_type": 4, "heading2": {"elements": [{"text_run": {"content": t, "text_element_style": {}}}], "style": {}}} -def _h3(t): +def _h3(t: str) -> dict: return {"block_type": 5, "heading3": {"elements": [{"text_run": {"content": t, "text_element_style": {}}}], "style": {}}} -def _text(t): +def _h4(t: str) -> dict: + return {"block_type": 6, "heading4": {"elements": [{"text_run": {"content": t, "text_element_style": {}}}], "style": {}}} + + +def _text(t: str) -> dict: return {"block_type": 2, "text": {"elements": [{"text_run": {"content": t, "text_element_style": {}}}], "style": {}}} +def _code(t: str, lang: int = 1) -> dict: + """block_type:14 代码块,lang 见 LANG_MAP,1=纯文本/流程图""" + return { + "block_type": 14, + "code": { + "elements": [{"text_run": {"content": t, "text_element_style": {}}}], + "style": {"language": lang}, + }, + } + + +def _callout(t: str, bg: int = 2) -> dict: + """block_type:19 高亮块,bg: 1=白 2=蓝 3=绿 4=橙 5=黄 6=红 7=紫""" + return { + "block_type": 19, + "callout": { + "emoji_id": "blue_book", + "background_color": bg, + "border_color": bg, + "elements": [{"text_run": {"content": t, "text_element_style": {}}}], + }, + } + + +def _divider() -> dict: + return {"block_type": 22, "divider": {}} + + def _image_placeholder(idx: int, path: str) -> dict: - """图片占位符,上传时由脚本替换为 gallery block""" return {"__image__": path, "__index__": idx} @@ -45,6 +97,8 @@ def _sheet_table(values: list[list[str]]) -> dict: } +# ── 辅助函数 ────────────────────────────────────────────────────────────────── + def _parse_md_row(line: str) -> list[str]: s = line.strip() if s.startswith("|"): @@ -63,54 +117,71 @@ def _is_md_table_sep(line: str) -> bool: if s.endswith("|"): s = s[:-1] parts = [p.strip() for p in s.split("|")] - if not parts: - return False - return all(re.match(r"^:?-{3,}:?$", p or "") for p in parts) + return bool(parts) and all(re.match(r"^:?-{3,}:?$", p or "") for p in parts) def _clean_inline_markdown(text: str) -> str: - """清理常见行内 markdown 标记,输出更适合飞书阅读的纯文本。""" + """去掉常见行内 Markdown 标记,输出适合飞书的纯文本。""" t = text - # 粗体/斜体标记 + # 粗体 t = re.sub(r"\*\*(.*?)\*\*", r"\1", t) t = re.sub(r"__(.*?)__", r"\1", t) + # 斜体 t = re.sub(r"\*(.*?)\*", r"\1", t) - t = re.sub(r"_(.*?)_", r"\1", t) - # 行内代码保留内容,去掉反引号 + t = re.sub(r"_((?!_).*?)_", r"\1", t) + # 行内代码(保留内容) t = re.sub(r"`([^`]+)`", r"\1", t) + # 链接 [text](url) → text + t = re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", t) return t.strip() +# ── 主转换函数 ───────────────────────────────────────────────────────────────── + def md_to_blocks(md: str, image_paths: list[str] | None = None) -> list: - """将 Markdown 转为飞书 blocks""" - blocks = [] + """将 Markdown 字符串转为飞书 blocks 列表。""" + blocks: list[dict] = [] image_paths = image_paths or [] img_idx = 0 first_h1_consumed = False in_code = False - code_lines = [] + code_lines: list[str] = [] + code_lang = 1 + lines = md.split("\n") i = 0 while i < len(lines): line = lines[i] - if line.strip().startswith("```"): + stripped = line.strip() + + # ── 代码围栏 ─────────────────────────────────────────────────────────── + if stripped.startswith("```"): if in_code: - # 飞书 blocks 常对代码围栏/特殊格式更严格,这里转为普通文本行,提升美观与稳定性 - for cl in code_lines: - if cl.strip(): - blocks.append(_text(f"代码:{cl.strip()}")) + # 结束代码块 → 生成单个 block_type:14 + code_content = "\n".join(code_lines) + if code_content.strip(): + blocks.append(_code(code_content, code_lang)) code_lines = [] - in_code = not in_code + code_lang = 1 + in_code = False + else: + # 开始代码块 → 识别语言 + lang_match = re.match(r"```(\w+)?", stripped) + code_lang = 1 + if lang_match and lang_match.group(1): + code_lang = LANG_MAP.get(lang_match.group(1).lower(), 1) + in_code = True i += 1 continue + if in_code: code_lines.append(line) i += 1 continue - # 图片语法 ![](path) - img_match = re.match(r"^!\[([^\]]*)\]\(([^)]+)\)\s*$", line.strip()) + # ── 图片 ────────────────────────────────────────────────────────────── + img_match = re.match(r"^!\[([^\]]*)\]\(([^)]+)\)\s*$", stripped) if img_match: path = img_match.group(2) if img_idx < len(image_paths): @@ -120,7 +191,7 @@ def md_to_blocks(md: str, image_paths: list[str] | None = None) -> list: i += 1 continue - # Markdown 表格:表头 + 分隔行 + 数据行 + # ── Markdown 表格 ───────────────────────────────────────────────────── if "|" in line and i + 1 < len(lines) and _is_md_table_sep(lines[i + 1]): table_lines = [line] j = i + 2 @@ -128,7 +199,7 @@ def md_to_blocks(md: str, image_paths: list[str] | None = None) -> list: raw = lines[j].strip() if not raw or "|" not in raw: break - if raw.startswith("#") or raw.startswith(">") or raw.startswith("```"): + if raw.startswith(("#", ">", "```")): break table_lines.append(lines[j]) j += 1 @@ -143,46 +214,50 @@ def md_to_blocks(md: str, image_paths: list[str] | None = None) -> list: rr.extend([""] * (col_size - len(rr))) clean_rows.append(rr[:col_size]) - # 飞书空 sheet 创建限制:行列最大 9,超出时截断并提示 max_rows, max_cols = 9, 9 if len(clean_rows) > max_rows or col_size > max_cols: - blocks.append(_text("提示:原表格超出飞书单块上限,已自动截断为 9x9。")) + blocks.append(_text("(注:原表格超出飞书单块上限,已自动截断为 9×9 显示)")) clipped = [r[:max_cols] for r in clean_rows[:max_rows]] blocks.append(_sheet_table(clipped)) i = j continue - # 忽略 Markdown 水平分隔线(避免在飞书出现大量“---”影响观感) - if line.strip() in {"---", "***", "___"}: + # ── 分割线 → block_type:22 ──────────────────────────────────────────── + if stripped in {"---", "***", "___"}: + blocks.append(_divider()) i += 1 continue - # 标题 - if line.startswith("# "): - # 避免正文和文档标题重复:默认跳过第一行 H1 + # ── 标题 ────────────────────────────────────────────────────────────── + if line.startswith("#### "): + blocks.append(_h4(_clean_inline_markdown(line[5:].strip()))) + elif line.startswith("### "): + blocks.append(_h3(_clean_inline_markdown(line[4:].strip()))) + elif line.startswith("## "): + blocks.append(_h2(_clean_inline_markdown(line[3:].strip()))) + elif line.startswith("# "): if first_h1_consumed: blocks.append(_h1(_clean_inline_markdown(line[2:].strip()))) else: first_h1_consumed = True - elif line.startswith("## "): - blocks.append(_h2(_clean_inline_markdown(line[3:].strip()))) - elif line.startswith("### "): - blocks.append(_h3(_clean_inline_markdown(line[4:].strip()))) - elif line.lstrip().startswith(">"): - # 引用块转普通说明行,降低写入失败概率 - quote = line.lstrip() + + # ── 引用 → block_type:19 callout ───────────────────────────────────── + elif stripped.startswith(">"): + quote = stripped while quote.startswith(">"): quote = quote[1:].lstrip() quote = _clean_inline_markdown(quote) if quote: - blocks.append(_text(quote)) - elif line.strip(): - raw = line.strip() - # 无序列表统一成 •,减少 markdown 观感噪音 + blocks.append(_callout(quote)) + + # ── 正文、列表 ──────────────────────────────────────────────────────── + elif stripped: + raw = stripped + # 无序列表 → • if re.match(r"^[-*]\s+", raw): raw = "• " + re.sub(r"^[-*]\s+", "", raw) - # 有序列表统一成 1)2)样式 + # 有序列表 → 1)2) raw = re.sub(r"^(\d+)\.\s+", r"\1)", raw) cleaned = _clean_inline_markdown(raw) if cleaned: @@ -194,13 +269,9 @@ def md_to_blocks(md: str, image_paths: list[str] | None = None) -> list: def blocks_to_upload_format(blocks: list, base_dir: Path) -> tuple[list, list]: - """ - 将含 __image__ 占位符的 blocks 转为可上传格式。 - 返回 (文本 blocks 列表, 图片路径列表,按出现顺序)。 - image_paths 优先存相对路径(相对 base_dir),便于 JSON 移植。 - """ - out = [] - paths = [] + """将含 __image__ 占位符的 blocks 转为可上传格式,返回 (blocks, image_paths)。""" + out: list = [] + paths: list[str] = [] for b in blocks: if isinstance(b, dict) and "__image__" in b: path = b.get("__image__", "") @@ -216,14 +287,16 @@ def blocks_to_upload_format(blocks: list, base_dir: Path) -> tuple[list, list]: rel = str(resolved) paths.append(rel) else: - paths.append(path if path else "unknown") - out.append({"block_type": 2, "text": {"elements": [{"text_run": {"content": f"【配图 {len(paths)}:待上传】", "text_element_style": {}}}], "style": {}}}) + paths.append(path or "unknown") + out.append( + _text(f"【配图 {len(paths)}:待上传】") + ) else: out.append(b) return out, paths -def main(): +def main() -> None: ap = argparse.ArgumentParser() ap.add_argument("input", help="Markdown 文件") ap.add_argument("output", help="输出 JSON 文件") @@ -241,7 +314,7 @@ def main(): final, img_paths = blocks_to_upload_format(blocks, inp.parent) out = { - "description": f"由 {inp.name} 转换的飞书 docx blocks", + "description": f"由 {inp.name} 转换的飞书 docx blocks(v2)", "source": str(inp), "image_paths": img_paths, "children": final, diff --git a/02_卡人(水)/水桥_平台对接/飞书管理/脚本/send_review_to_feishu_webhook.py b/02_卡人(水)/水桥_平台对接/飞书管理/脚本/send_review_to_feishu_webhook.py new file mode 100644 index 00000000..5df33d1d --- /dev/null +++ b/02_卡人(水)/水桥_平台对接/飞书管理/脚本/send_review_to_feishu_webhook.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +""" +卡若AI 对话复盘总结 → 飞书群 webhook + +每次对话完成后,将简洁复盘总结发到指定飞书群。 +飞书 bot v2 hook 要求:POST JSON,含 msg_type(如 text)。 + +用法: + # 直接传入简洁总结(建议 ≤500 字) + python3 send_review_to_feishu_webhook.py "【卡若AI复盘】2026-03-12 15:30\n🎯 完成记忆系统使用手册\n📌 已写开发文档/9、手册/卡若AI记忆系统使用手册.md\n▶ 无" + + # 从文件读 + python3 send_review_to_feishu_webhook.py --file /path/to/summary.txt + + # 指定 webhook(否则用默认) + python3 send_review_to_feishu_webhook.py --webhook "https://open.feishu.cn/..." "总结内容" + +环境变量(可选): + FEISHU_REVIEW_WEBHOOK — 默认 webhook URL +""" +import argparse +import json +import os +import sys +from pathlib import Path + +import requests + +# 默认 webhook(卡若AI 复盘总结群);可被环境变量或 --webhook 覆盖 +DEFAULT_WEBHOOK = os.environ.get( + "FEISHU_REVIEW_WEBHOOK", + "https://open.feishu.cn/open-apis/bot/v2/hook/8b7f996e-2892-4075-989f-aa5593ea4fbc", +) + + +def send_text(webhook_url: str, text: str) -> bool: + """POST 文本到飞书 bot v2 webhook。""" + if not text or not webhook_url: + return False + payload = {"msg_type": "text", "content": {"text": text[:4000]}} # 飞书单条文本有长度限制 + try: + r = requests.post(webhook_url, json=payload, timeout=10) + body = r.json() + if body.get("code") != 0: + print(f"飞书 webhook 返回错误: {body}", file=sys.stderr) + return False + return True + except Exception as e: + print(f"发送失败: {e}", file=sys.stderr) + return False + + +def main(): + ap = argparse.ArgumentParser(description="卡若AI 复盘总结发飞书群") + ap.add_argument("text", nargs="?", default="", help="简洁复盘总结(建议≤500字)") + ap.add_argument("--file", "-f", help="从文件读取总结内容") + ap.add_argument("--webhook", "-w", default=DEFAULT_WEBHOOK, help="飞书群 webhook URL") + args = ap.parse_args() + + if args.file: + path = Path(args.file) + if path.exists(): + text = path.read_text(encoding="utf-8").strip() + else: + print(f"文件不存在: {args.file}", file=sys.stderr) + sys.exit(1) + else: + text = (args.text or sys.stdin.read()).strip() + + if not text: + print("无内容可发送", file=sys.stderr) + sys.exit(1) + + ok = send_text(args.webhook, text) + if ok: + print("已发送到飞书群") + else: + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/03_卡木(木)/木识_软件识形/CLI万能化/SKILL.md b/03_卡木(木)/木识_软件识形/CLI万能化/SKILL.md new file mode 100644 index 00000000..3af4fab6 --- /dev/null +++ b/03_卡木(木)/木识_软件识形/CLI万能化/SKILL.md @@ -0,0 +1,206 @@ +--- +name: CLI万能化 +description: 让任意软件变成 AI Agent 可驱动的 CLI 接口;一行命令完成7阶段自动流水线 +triggers: CLI万能化、cli-anything、软件识形、让软件Agent化、任意软件CLI、软件CLI接口、木识、软件识形、CLI接口生成、让软件可被AI控制、给软件生成CLI、GUI转CLI +owner: 木识 +group: 木(卡木) +version: "1.0" +updated: "2026-03-12" +source: https://github.com/HKUDS/CLI-Anything +--- + +# 木识 · CLI万能化 + +> **木识**(Mù Shí)是卡木第四成员,专司「软件识形」——识别任意软件的形体与架构,将其转化为 AI Agent 可驱动的 CLI 接口。 +> +> 「识」字连接记忆宫殿:佛教哲学中「阿赖耶识」为万物印记之储库,木识的使命就是将任意软件的能力识别、提炼并存入 Agent 可调用的指令库——让记忆宫殿的每个房间都多一扇可编程的门。 + +--- + +## 能做什么(Capabilities) + +- **任意软件 → CLI**:对任意有源码的软件(GIMP、Blender、LibreOffice、Gitea、Jenkins、Stable Diffusion、ComfyUI 等),自动生成生产级 CLI 接口 +- **7 阶段全自动流水线**:分析 → 设计 → 实现 → 规划测试 → 写测试 → 文档 → 发布,无需手写代码 +- **Agent 友好输出**:每条命令均支持 `--help` 自描述 + `--json` 机器可读输出 +- **REPL 交互模式**:有状态交互式会话,支持撤销/重做 +- **本机集成**:生成的 CLI 以 `pip install -e .` 直接装到 PATH,卡若AI 后续技能可直接调用 +- **迭代精化**:可对已生成的 CLI 做 gap analysis,增量扩展覆盖范围 +- **与木根联动**:木根做逆向分析找 API,木识生成完整可用的 CLI 层 + +--- + +## 怎么用(Usage) + +### 触发词 +`CLI万能化`、`cli-anything`、`软件识形`、`让[软件名]被Agent控制`、`给[软件]生成CLI`、`木识` + +### 使用示例 +``` +木识:把本机的 Gitea 生成 CLI 接口 +木识:给 Stable Diffusion 做 CLI 万能化 +木识:让 ComfyUI 可以被 Agent 调用 +CLI万能化 ./blender +``` + +--- + +## 执行步骤(Steps) + +### 前置检查 +```bash +python3 --version # 需要 ≥ 3.10 +which python3 +# 确认目标软件已安装(如需要) +``` + +### 方式一:在 Cursor / 卡若AI 内直接执行(推荐) + +木识在 Cursor 中按以下流程手动执行 7 阶段(无需 Claude Code 插件市场): + +**阶段0:获取源码** +```bash +# 本地路径(直接用) +TARGET_PATH="./gimp" + +# 或克隆 GitHub 仓库 +git clone https://github.com/GNOME/gimp /tmp/target-software/gimp +TARGET_PATH="/tmp/target-software/gimp" +``` + +**阶段1:分析(Analyze)** +- 读取目标软件源码目录 +- 识别后端引擎、GUI-API 映射关系 +- 参考:`参考资料/HARNESS.md` § Phase 1 + +**阶段2:设计(Design)** +- 设计命令分组、状态模型、输出格式 +- 参考:`参考资料/HARNESS.md` § Phase 2 + +**阶段3:实现(Implement)** +生成以下目录结构: +``` +/ +└── agent-harness/ + ├── .md ← 软件专属 SOP + ├── setup.py ← pip 可安装 + └── cli_anything/ + └── / + ├── README.md + ├── __init__.py + ├── __main__.py + ├── _cli.py ← 主 CLI(Click) + ├── core/ ← 核心操作模块 + ├── utils/ + │ ├── repl_skin.py ← REPL UI(从 harness_templates/ 复制) + │ └── _backend.py ← 真实后端封装 + └── tests/ + ├── test_core.py ← 单元测试 + └── test_full_e2e.py ← 端到端测试 +``` + +**阶段4-5:测试(Test)** +```bash +cd /agent-harness +pip install -e . --break-system-packages 2>/dev/null || pip install -e . --user +python -m pytest cli_anything//tests/ -v +``` + +**阶段6:文档** +- 更新 TEST.md,记录测试结果 + +**阶段7:发布(Publish)** +```bash +pip install -e . --break-system-packages 2>/dev/null || pip install -e . --user +which cli-anything- +cli-anything- --help +``` + +### 方式二:通过 Claude Code 插件(原生方式) + +如果用户已安装 Claude Code: +```bash +/plugin marketplace add HKUDS/CLI-Anything +/plugin install cli-anything +/cli-anything:cli-anything ./gimp +``` + +### 方式三:精化已有 CLI +```bash +# 通过 Claude Code +/cli-anything:refine ./gimp +/cli-anything:refine ./gimp "增加批量图像处理和滤镜功能" + +# 在 Cursor 内:读 HARNESS.md → 执行 gap analysis → 增量实现 +``` + +--- + +## 7 阶段流水线详解 + +| 阶段 | 名称 | 做什么 | +|:--|:---|:---| +| 1 | 分析 Analyze | 扫源码、找后端引擎、GUI→API 映射 | +| 2 | 设计 Design | 命令分组、状态模型、输出格式设计 | +| 3 | 实现 Implement | Click CLI + REPL + JSON 输出 + 后端封装 | +| 4 | 规划测试 Plan Tests | 写 TEST.md(单元+E2E 计划) | +| 5 | 编写测试 Write Tests | 实现 test_core.py + test_full_e2e.py | +| 6 | 文档 Document | 更新 TEST.md + README | +| 7 | 发布 Publish | setup.py + pip install -e . + 验证 | + +--- + +## 支持的软件类别(已验证) + +| 类别 | 代表软件 | +|:--|:---| +| 创意/媒体 | GIMP、Blender、Inkscape、Audacity、Kdenlive、OBS Studio | +| AI/ML 平台 | Stable Diffusion、ComfyUI、InvokeAI | +| 数据/分析 | JupyterLab、Apache Superset、Metabase、DBeaver | +| 开发工具 | Jenkins、**Gitea**、Portainer、pgAdmin、SonarQube | +| 办公/企业 | LibreOffice、GitLab、Grafana、Mattermost | +| 图表/可视化 | Draw.io、Mermaid、PlantUML、Excalidraw | +| 任意有源码软件 | 只要有代码库,均可生成 CLI | + +--- + +## 与卡若AI 系统的联动 + +- **木根(逆向分析)**:木根分析目标软件 API → 木识生成完整 CLI 层 +- **火炬(全栈开发)**:生成的 CLI 可集成进卡若AI 项目的自动化流水线 +- **金仓(系统监控)**:Gitea、Jenkins 等开发工具生成 CLI 后可纳入监控体系 +- **土砖(技能工厂)**:每个生成的 CLI harness 可打包为基因胶囊,分发复用 + +--- + +## 相关文件(Files) + +- 核心规范:`参考资料/HARNESS.md`(7阶段方法论完整版) +- REPL模板:`harness_templates/repl_skin.py`(REPL UI组件,直接复制使用) +- 上游仓库:`https://github.com/HKUDS/CLI-Anything`(⭐ 7700+,MIT License) +- 本机源码镜像:`/tmp/cli-anything-src`(对话内临时克隆,可重新 clone) + +--- + +## 依赖(Dependencies) + +- 前置技能:可与木根(逆向分析)联动 +- 外部工具: + - `python3 ≥ 3.10` + - `click ≥ 8.0`(`pip install click`) + - `pytest`(`pip install pytest`) + - `prompt_toolkit`(REPL 模式需要,`pip install prompt_toolkit`) + - 目标软件需在本机已安装(如需要) + +--- + +## 木识 · 身份档案 + +| 属性 | 内容 | +|:--|:---| +| **名字** | 木识(Mù Shí) | +| **所属** | 卡木(木组)第四成员 | +| **口号** | "识形成器,万物可用。" | +| **专司** | 软件识形:识别任意软件的能力形体,转化为 Agent 可驱动的 CLI | +| **记忆宫殿连接** | 「识」= 阿赖耶识,万物印记之储库;每识形一款软件,就为记忆宫殿多开一扇可编程的门 | +| **五行属性** | 木(生长、工具、创造)+ 识(认知、识别、意识) | +| **互补成员** | 木根(逆向分析找路)→ 木识(识形生成CLI通道) | diff --git a/03_卡木(木)/木识_软件识形/CLI万能化/harness_templates/repl_skin.py b/03_卡木(木)/木识_软件识形/CLI万能化/harness_templates/repl_skin.py new file mode 100644 index 00000000..47260beb --- /dev/null +++ b/03_卡木(木)/木识_软件识形/CLI万能化/harness_templates/repl_skin.py @@ -0,0 +1,498 @@ +"""cli-anything REPL Skin — Unified terminal interface for all CLI harnesses. + +Copy this file into your CLI package at: + cli_anything//utils/repl_skin.py + +Usage: + from cli_anything..utils.repl_skin import ReplSkin + + skin = ReplSkin("shotcut", version="1.0.0") + skin.print_banner() + prompt_text = skin.prompt(project_name="my_video.mlt", modified=True) + skin.success("Project saved") + skin.error("File not found") + skin.warning("Unsaved changes") + skin.info("Processing 24 clips...") + skin.status("Track 1", "3 clips, 00:02:30") + skin.table(headers, rows) + skin.print_goodbye() +""" + +import os +import sys + +# ── ANSI color codes (no external deps for core styling) ────────────── + +_RESET = "\033[0m" +_BOLD = "\033[1m" +_DIM = "\033[2m" +_ITALIC = "\033[3m" +_UNDERLINE = "\033[4m" + +# Brand colors +_CYAN = "\033[38;5;80m" # cli-anything brand cyan +_CYAN_BG = "\033[48;5;80m" +_WHITE = "\033[97m" +_GRAY = "\033[38;5;245m" +_DARK_GRAY = "\033[38;5;240m" +_LIGHT_GRAY = "\033[38;5;250m" + +# Software accent colors — each software gets a unique accent +_ACCENT_COLORS = { + "gimp": "\033[38;5;214m", # warm orange + "blender": "\033[38;5;208m", # deep orange + "inkscape": "\033[38;5;39m", # bright blue + "audacity": "\033[38;5;33m", # navy blue + "libreoffice": "\033[38;5;40m", # green + "obs_studio": "\033[38;5;55m", # purple + "kdenlive": "\033[38;5;69m", # slate blue + "shotcut": "\033[38;5;35m", # teal green +} +_DEFAULT_ACCENT = "\033[38;5;75m" # default sky blue + +# Status colors +_GREEN = "\033[38;5;78m" +_YELLOW = "\033[38;5;220m" +_RED = "\033[38;5;196m" +_BLUE = "\033[38;5;75m" +_MAGENTA = "\033[38;5;176m" + +# ── Brand icon ──────────────────────────────────────────────────────── + +# The cli-anything icon: a small colored diamond/chevron mark +_ICON = f"{_CYAN}{_BOLD}◆{_RESET}" +_ICON_SMALL = f"{_CYAN}▸{_RESET}" + +# ── Box drawing characters ──────────────────────────────────────────── + +_H_LINE = "─" +_V_LINE = "│" +_TL = "╭" +_TR = "╮" +_BL = "╰" +_BR = "╯" +_T_DOWN = "┬" +_T_UP = "┴" +_T_RIGHT = "├" +_T_LEFT = "┤" +_CROSS = "┼" + + +def _strip_ansi(text: str) -> str: + """Remove ANSI escape codes for length calculation.""" + import re + return re.sub(r"\033\[[^m]*m", "", text) + + +def _visible_len(text: str) -> int: + """Get visible length of text (excluding ANSI codes).""" + return len(_strip_ansi(text)) + + +class ReplSkin: + """Unified REPL skin for cli-anything CLIs. + + Provides consistent branding, prompts, and message formatting + across all CLI harnesses built with the cli-anything methodology. + """ + + def __init__(self, software: str, version: str = "1.0.0", + history_file: str | None = None): + """Initialize the REPL skin. + + Args: + software: Software name (e.g., "gimp", "shotcut", "blender"). + version: CLI version string. + history_file: Path for persistent command history. + Defaults to ~/.cli-anything-/history + """ + self.software = software.lower().replace("-", "_") + self.display_name = software.replace("_", " ").title() + self.version = version + self.accent = _ACCENT_COLORS.get(self.software, _DEFAULT_ACCENT) + + # History file + if history_file is None: + from pathlib import Path + hist_dir = Path.home() / f".cli-anything-{self.software}" + hist_dir.mkdir(parents=True, exist_ok=True) + self.history_file = str(hist_dir / "history") + else: + self.history_file = history_file + + # Detect terminal capabilities + self._color = self._detect_color_support() + + def _detect_color_support(self) -> bool: + """Check if terminal supports color.""" + if os.environ.get("NO_COLOR"): + return False + if os.environ.get("CLI_ANYTHING_NO_COLOR"): + return False + if not hasattr(sys.stdout, "isatty"): + return False + return sys.stdout.isatty() + + def _c(self, code: str, text: str) -> str: + """Apply color code if colors are supported.""" + if not self._color: + return text + return f"{code}{text}{_RESET}" + + # ── Banner ──────────────────────────────────────────────────────── + + def print_banner(self): + """Print the startup banner with branding.""" + inner = 54 + + def _box_line(content: str) -> str: + """Wrap content in box drawing, padding to inner width.""" + pad = inner - _visible_len(content) + vl = self._c(_DARK_GRAY, _V_LINE) + return f"{vl}{content}{' ' * max(0, pad)}{vl}" + + top = self._c(_DARK_GRAY, f"{_TL}{_H_LINE * inner}{_TR}") + bot = self._c(_DARK_GRAY, f"{_BL}{_H_LINE * inner}{_BR}") + + # Title: ◆ cli-anything · Shotcut + icon = self._c(_CYAN + _BOLD, "◆") + brand = self._c(_CYAN + _BOLD, "cli-anything") + dot = self._c(_DARK_GRAY, "·") + name = self._c(self.accent + _BOLD, self.display_name) + title = f" {icon} {brand} {dot} {name}" + + ver = f" {self._c(_DARK_GRAY, f' v{self.version}')}" + tip = f" {self._c(_DARK_GRAY, ' Type help for commands, quit to exit')}" + empty = "" + + print(top) + print(_box_line(title)) + print(_box_line(ver)) + print(_box_line(empty)) + print(_box_line(tip)) + print(bot) + print() + + # ── Prompt ──────────────────────────────────────────────────────── + + def prompt(self, project_name: str = "", modified: bool = False, + context: str = "") -> str: + """Build a styled prompt string for prompt_toolkit or input(). + + Args: + project_name: Current project name (empty if none open). + modified: Whether the project has unsaved changes. + context: Optional extra context to show in prompt. + + Returns: + Formatted prompt string. + """ + parts = [] + + # Icon + if self._color: + parts.append(f"{_CYAN}◆{_RESET} ") + else: + parts.append("> ") + + # Software name + parts.append(self._c(self.accent + _BOLD, self.software)) + + # Project context + if project_name or context: + ctx = context or project_name + mod = "*" if modified else "" + parts.append(f" {self._c(_DARK_GRAY, '[')}") + parts.append(self._c(_LIGHT_GRAY, f"{ctx}{mod}")) + parts.append(self._c(_DARK_GRAY, ']')) + + parts.append(self._c(_GRAY, " ❯ ")) + + return "".join(parts) + + def prompt_tokens(self, project_name: str = "", modified: bool = False, + context: str = ""): + """Build prompt_toolkit formatted text tokens for the prompt. + + Use with prompt_toolkit's FormattedText for proper ANSI handling. + + Returns: + list of (style, text) tuples for prompt_toolkit. + """ + accent_hex = _ANSI_256_TO_HEX.get(self.accent, "#5fafff") + tokens = [] + + tokens.append(("class:icon", "◆ ")) + tokens.append(("class:software", self.software)) + + if project_name or context: + ctx = context or project_name + mod = "*" if modified else "" + tokens.append(("class:bracket", " [")) + tokens.append(("class:context", f"{ctx}{mod}")) + tokens.append(("class:bracket", "]")) + + tokens.append(("class:arrow", " ❯ ")) + + return tokens + + def get_prompt_style(self): + """Get a prompt_toolkit Style object matching the skin. + + Returns: + prompt_toolkit.styles.Style + """ + try: + from prompt_toolkit.styles import Style + except ImportError: + return None + + accent_hex = _ANSI_256_TO_HEX.get(self.accent, "#5fafff") + + return Style.from_dict({ + "icon": "#5fdfdf bold", # cyan brand color + "software": f"{accent_hex} bold", + "bracket": "#585858", + "context": "#bcbcbc", + "arrow": "#808080", + # Completion menu + "completion-menu.completion": "bg:#303030 #bcbcbc", + "completion-menu.completion.current": f"bg:{accent_hex} #000000", + "completion-menu.meta.completion": "bg:#303030 #808080", + "completion-menu.meta.completion.current": f"bg:{accent_hex} #000000", + # Auto-suggest + "auto-suggest": "#585858", + # Bottom toolbar + "bottom-toolbar": "bg:#1c1c1c #808080", + "bottom-toolbar.text": "#808080", + }) + + # ── Messages ────────────────────────────────────────────────────── + + def success(self, message: str): + """Print a success message with green checkmark.""" + icon = self._c(_GREEN + _BOLD, "✓") + print(f" {icon} {self._c(_GREEN, message)}") + + def error(self, message: str): + """Print an error message with red cross.""" + icon = self._c(_RED + _BOLD, "✗") + print(f" {icon} {self._c(_RED, message)}", file=sys.stderr) + + def warning(self, message: str): + """Print a warning message with yellow triangle.""" + icon = self._c(_YELLOW + _BOLD, "⚠") + print(f" {icon} {self._c(_YELLOW, message)}") + + def info(self, message: str): + """Print an info message with blue dot.""" + icon = self._c(_BLUE, "●") + print(f" {icon} {self._c(_LIGHT_GRAY, message)}") + + def hint(self, message: str): + """Print a subtle hint message.""" + print(f" {self._c(_DARK_GRAY, message)}") + + def section(self, title: str): + """Print a section header.""" + print() + print(f" {self._c(self.accent + _BOLD, title)}") + print(f" {self._c(_DARK_GRAY, _H_LINE * len(title))}") + + # ── Status display ──────────────────────────────────────────────── + + def status(self, label: str, value: str): + """Print a key-value status line.""" + lbl = self._c(_GRAY, f" {label}:") + val = self._c(_WHITE, f" {value}") + print(f"{lbl}{val}") + + def status_block(self, items: dict[str, str], title: str = ""): + """Print a block of status key-value pairs. + + Args: + items: Dict of label -> value pairs. + title: Optional title for the block. + """ + if title: + self.section(title) + + max_key = max(len(k) for k in items) if items else 0 + for label, value in items.items(): + lbl = self._c(_GRAY, f" {label:<{max_key}}") + val = self._c(_WHITE, f" {value}") + print(f"{lbl}{val}") + + def progress(self, current: int, total: int, label: str = ""): + """Print a simple progress indicator. + + Args: + current: Current step number. + total: Total number of steps. + label: Optional label for the progress. + """ + pct = int(current / total * 100) if total > 0 else 0 + bar_width = 20 + filled = int(bar_width * current / total) if total > 0 else 0 + bar = "█" * filled + "░" * (bar_width - filled) + text = f" {self._c(_CYAN, bar)} {self._c(_GRAY, f'{pct:3d}%')}" + if label: + text += f" {self._c(_LIGHT_GRAY, label)}" + print(text) + + # ── Table display ───────────────────────────────────────────────── + + def table(self, headers: list[str], rows: list[list[str]], + max_col_width: int = 40): + """Print a formatted table with box-drawing characters. + + Args: + headers: Column header strings. + rows: List of rows, each a list of cell strings. + max_col_width: Maximum column width before truncation. + """ + if not headers: + return + + # Calculate column widths + col_widths = [min(len(h), max_col_width) for h in headers] + for row in rows: + for i, cell in enumerate(row): + if i < len(col_widths): + col_widths[i] = min( + max(col_widths[i], len(str(cell))), max_col_width + ) + + def pad(text: str, width: int) -> str: + t = str(text)[:width] + return t + " " * (width - len(t)) + + # Header + header_cells = [ + self._c(_CYAN + _BOLD, pad(h, col_widths[i])) + for i, h in enumerate(headers) + ] + sep = self._c(_DARK_GRAY, f" {_V_LINE} ") + header_line = f" {sep.join(header_cells)}" + print(header_line) + + # Separator + sep_parts = [self._c(_DARK_GRAY, _H_LINE * w) for w in col_widths] + sep_line = self._c(_DARK_GRAY, f" {'───'.join([_H_LINE * w for w in col_widths])}") + print(sep_line) + + # Rows + for row in rows: + cells = [] + for i, cell in enumerate(row): + if i < len(col_widths): + cells.append(self._c(_LIGHT_GRAY, pad(str(cell), col_widths[i]))) + row_sep = self._c(_DARK_GRAY, f" {_V_LINE} ") + print(f" {row_sep.join(cells)}") + + # ── Help display ────────────────────────────────────────────────── + + def help(self, commands: dict[str, str]): + """Print a formatted help listing. + + Args: + commands: Dict of command -> description pairs. + """ + self.section("Commands") + max_cmd = max(len(c) for c in commands) if commands else 0 + for cmd, desc in commands.items(): + cmd_styled = self._c(self.accent, f" {cmd:<{max_cmd}}") + desc_styled = self._c(_GRAY, f" {desc}") + print(f"{cmd_styled}{desc_styled}") + print() + + # ── Goodbye ─────────────────────────────────────────────────────── + + def print_goodbye(self): + """Print a styled goodbye message.""" + print(f"\n {_ICON_SMALL} {self._c(_GRAY, 'Goodbye!')}\n") + + # ── Prompt toolkit session factory ──────────────────────────────── + + def create_prompt_session(self): + """Create a prompt_toolkit PromptSession with skin styling. + + Returns: + A configured PromptSession, or None if prompt_toolkit unavailable. + """ + try: + from prompt_toolkit import PromptSession + from prompt_toolkit.history import FileHistory + from prompt_toolkit.auto_suggest import AutoSuggestFromHistory + from prompt_toolkit.formatted_text import FormattedText + + style = self.get_prompt_style() + + session = PromptSession( + history=FileHistory(self.history_file), + auto_suggest=AutoSuggestFromHistory(), + style=style, + enable_history_search=True, + ) + return session + except ImportError: + return None + + def get_input(self, pt_session, project_name: str = "", + modified: bool = False, context: str = "") -> str: + """Get input from user using prompt_toolkit or fallback. + + Args: + pt_session: A prompt_toolkit PromptSession (or None). + project_name: Current project name. + modified: Whether project has unsaved changes. + context: Optional context string. + + Returns: + User input string (stripped). + """ + if pt_session is not None: + from prompt_toolkit.formatted_text import FormattedText + tokens = self.prompt_tokens(project_name, modified, context) + return pt_session.prompt(FormattedText(tokens)).strip() + else: + raw_prompt = self.prompt(project_name, modified, context) + return input(raw_prompt).strip() + + # ── Toolbar builder ─────────────────────────────────────────────── + + def bottom_toolbar(self, items: dict[str, str]): + """Create a bottom toolbar callback for prompt_toolkit. + + Args: + items: Dict of label -> value pairs to show in toolbar. + + Returns: + A callable that returns FormattedText for the toolbar. + """ + def toolbar(): + from prompt_toolkit.formatted_text import FormattedText + parts = [] + for i, (k, v) in enumerate(items.items()): + if i > 0: + parts.append(("class:bottom-toolbar.text", " │ ")) + parts.append(("class:bottom-toolbar.text", f" {k}: ")) + parts.append(("class:bottom-toolbar", v)) + return FormattedText(parts) + return toolbar + + +# ── ANSI 256-color to hex mapping (for prompt_toolkit styles) ───────── + +_ANSI_256_TO_HEX = { + "\033[38;5;33m": "#0087ff", # audacity navy blue + "\033[38;5;35m": "#00af5f", # shotcut teal + "\033[38;5;39m": "#00afff", # inkscape bright blue + "\033[38;5;40m": "#00d700", # libreoffice green + "\033[38;5;55m": "#5f00af", # obs purple + "\033[38;5;69m": "#5f87ff", # kdenlive slate blue + "\033[38;5;75m": "#5fafff", # default sky blue + "\033[38;5;80m": "#5fd7d7", # brand cyan + "\033[38;5;208m": "#ff8700", # blender deep orange + "\033[38;5;214m": "#ffaf00", # gimp warm orange +} diff --git a/03_卡木(木)/木识_软件识形/CLI万能化/参考资料/HARNESS.md b/03_卡木(木)/木识_软件识形/CLI万能化/参考资料/HARNESS.md new file mode 100644 index 00000000..9b16737e --- /dev/null +++ b/03_卡木(木)/木识_软件识形/CLI万能化/参考资料/HARNESS.md @@ -0,0 +1,622 @@ +# Agent Harness: GUI-to-CLI for Open Source Software + +## Purpose + +This harness provides a standard operating procedure (SOP) and toolkit for coding +agents (Claude Code, Codex, etc.) to build powerful, stateful CLI interfaces for +open-source GUI applications. The goal: let AI agents operate software that was +designed for humans, without needing a display or mouse. + +## General SOP: Turning Any GUI App into an Agent-Usable CLI + +### Phase 1: Codebase Analysis + +1. **Identify the backend engine** — Most GUI apps separate presentation from logic. + Find the core library/framework (e.g., MLT for Shotcut, ImageMagick for GIMP). +2. **Map GUI actions to API calls** — Every button click, drag, and menu item + corresponds to a function call. Catalog these mappings. +3. **Identify the data model** — What file formats does it use? How is project state + represented? (XML, JSON, binary, database?) +4. **Find existing CLI tools** — Many backends ship their own CLI (`melt`, `ffmpeg`, + `convert`). These are building blocks. +5. **Catalog the command/undo system** — If the app has undo/redo, it likely uses a + command pattern. These commands are your CLI operations. + +### Phase 2: CLI Architecture Design + +1. **Choose the interaction model**: + - **Stateful REPL** for interactive sessions (agents that maintain context) + - **Subcommand CLI** for one-shot operations (scripting, pipelines) + - **Both** (recommended) — a CLI that works in both modes + +2. **Define command groups** matching the app's logical domains: + - Project management (new, open, save, close) + - Core operations (the app's primary purpose) + - Import/Export (file I/O, format conversion) + - Configuration (settings, preferences, profiles) + - Session/State management (undo, redo, history, status) + +3. **Design the state model**: + - What must persist between commands? (open project, cursor position, selection) + - Where is state stored? (in-memory for REPL, file-based for CLI) + - How does state serialize? (JSON session files) + +4. **Plan the output format**: + - Human-readable (tables, colors) for interactive use + - Machine-readable (JSON) for agent consumption + - Both, controlled by `--json` flag + +### Phase 3: Implementation + +1. **Start with the data layer** — XML/JSON manipulation of project files +2. **Add probe/info commands** — Let agents inspect before they modify +3. **Add mutation commands** — One command per logical operation +4. **Add the backend integration** — A `utils/_backend.py` module that + wraps the real software's CLI. This module handles: + - Finding the software executable (`shutil.which()`) + - Invoking it with proper arguments (`subprocess.run()`) + - Error handling with clear install instructions if not found + - Example (LibreOffice): + ```python + # utils/lo_backend.py + def convert_odf_to(odf_path, output_format, output_path=None, overwrite=False): + lo = find_libreoffice() # raises RuntimeError with install instructions + subprocess.run([lo, "--headless", "--convert-to", output_format, ...]) + return {"output": final_path, "format": output_format, "method": "libreoffice-headless"} + ``` +5. **Add rendering/export** — The export pipeline calls the backend module. + Generate valid intermediate files, then invoke the real software for conversion. +6. **Add session management** — State persistence, undo/redo +7. **Add the REPL with unified skin** — Interactive mode wrapping the subcommands. + - Copy `repl_skin.py` from the plugin (`cli-anything-plugin/repl_skin.py`) into + `utils/repl_skin.py` in your CLI package + - Import and use `ReplSkin` for the REPL interface: + ```python + from cli_anything..utils.repl_skin import ReplSkin + + skin = ReplSkin("", version="1.0.0") + skin.print_banner() # Branded startup box + pt_session = skin.create_prompt_session() # prompt_toolkit with history + styling + line = skin.get_input(pt_session, project_name="my_project", modified=True) + skin.help(commands_dict) # Formatted help listing + skin.success("Saved") # ✓ green message + skin.error("Not found") # ✗ red message + skin.warning("Unsaved") # ⚠ yellow message + skin.info("Processing...") # ● blue message + skin.status("Key", "value") # Key-value status line + skin.table(headers, rows) # Formatted table + skin.progress(3, 10, "...") # Progress bar + skin.print_goodbye() # Styled exit message + ``` + - Make REPL the default behavior: use `invoke_without_command=True` on the main + Click group, and invoke the `repl` command when no subcommand is given: + ```python + @click.group(invoke_without_command=True) + @click.pass_context + def cli(ctx, ...): + ... + if ctx.invoked_subcommand is None: + ctx.invoke(repl, project_path=None) + ``` + - This ensures `cli-anything-` with no arguments enters the REPL + +### Phase 4: Test Planning (TEST.md - Part 1) + +**BEFORE writing any test code**, create a `TEST.md` file in the +`agent-harness/cli_anything//tests/` directory. This file serves as your test plan and +MUST contain: + +1. **Test Inventory Plan** — List planned test files and estimated test counts: + - `test_core.py`: XX unit tests planned + - `test_full_e2e.py`: XX E2E tests planned + +2. **Unit Test Plan** — For each core module, describe what will be tested: + - Module name (e.g., `project.py`) + - Functions to test + - Edge cases to cover (invalid inputs, boundary conditions, error handling) + - Expected test count + +3. **E2E Test Plan** — Describe the real-world scenarios to test: + - What workflows will be simulated? + - What real files will be generated/processed? + - What output properties will be verified? + - What format validations will be performed? + +4. **Realistic Workflow Scenarios** — Detail each multi-step workflow: + - **Workflow name**: Brief title + - **Simulates**: What real-world task (e.g., "photo editing pipeline", + "podcast production", "product render setup") + - **Operations chained**: Step-by-step operations + - **Verified**: What output properties will be checked + +This planning document ensures comprehensive test coverage before writing code. + +### Phase 5: Test Implementation + +Now write the actual test code based on the TEST.md plan: + +1. **Unit tests** (`test_core.py`) — Every core function tested in isolation with + synthetic data. No external dependencies. +2. **E2E tests — intermediate files** (`test_full_e2e.py`) — Verify the project files + your CLI generates are structurally correct (valid XML, correct ZIP structure, etc.) +3. **E2E tests — true backend** (`test_full_e2e.py`) — **MUST invoke the real software.** + Create a project, export via the actual software backend, and verify the output: + - File exists and size > 0 + - Correct format (PDF magic bytes `%PDF-`, DOCX/XLSX/PPTX is valid ZIP/OOXML, etc.) + - Content verification where possible (CSV contains expected data, etc.) + - **Print artifact paths** so users can manually inspect: `print(f"\n PDF: {path} ({size:,} bytes)")` + - **No graceful degradation** — if the software isn't installed, tests fail, not skip +4. **Output verification** — **Don't trust that export works just because it exits + successfully.** Verify outputs programmatically: + - Magic bytes / file format validation + - ZIP structure for OOXML formats (DOCX, XLSX, PPTX) + - Pixel-level analysis for video/images (probe frames, compare brightness) + - Audio analysis (RMS levels, spectral comparison) + - Duration/format checks against expected values +5. **CLI subprocess tests** — Test the installed CLI command as a real user/agent would. + The subprocess tests MUST also produce real final output (not just ODF intermediate). + Use the `_resolve_cli` helper to run the installed `cli-anything-` command: + ```python + def _resolve_cli(name): + """Resolve installed CLI command; falls back to python -m for dev. + + Set env CLI_ANYTHING_FORCE_INSTALLED=1 to require the installed command. + """ + import shutil + force = os.environ.get("CLI_ANYTHING_FORCE_INSTALLED", "").strip() == "1" + path = shutil.which(name) + if path: + print(f"[_resolve_cli] Using installed command: {path}") + return [path] + if force: + raise RuntimeError(f"{name} not found in PATH. Install with: pip install -e .") + module = name.replace("cli-anything-", "cli_anything.") + "." + name.split("-")[-1] + "_cli" + print(f"[_resolve_cli] Falling back to: {sys.executable} -m {module}") + return [sys.executable, "-m", module] + + + class TestCLISubprocess: + CLI_BASE = _resolve_cli("cli-anything-") + + def _run(self, args, check=True): + return subprocess.run( + self.CLI_BASE + args, + capture_output=True, text=True, + check=check, + ) + + def test_help(self): + result = self._run(["--help"]) + assert result.returncode == 0 + + def test_project_new_json(self, tmp_dir): + out = os.path.join(tmp_dir, "test.json") + result = self._run(["--json", "project", "new", "-o", out]) + assert result.returncode == 0 + data = json.loads(result.stdout) + # ... verify structure + ``` + + **Key rules for subprocess tests:** + - Always use `_resolve_cli("cli-anything-")` — never hardcode + `sys.executable` or module paths directly + - Do NOT set `cwd` — installed commands must work from any directory + - Use `CLI_ANYTHING_FORCE_INSTALLED=1` in CI/release testing to ensure the + installed command (not a fallback) is being tested + - Test `--help`, `--json`, project creation, key commands, and full workflows + +6. **Round-trip test** — Create project via CLI, open in GUI, verify correctness +7. **Agent test** — Have an AI agent complete a real task using only the CLI + +### Phase 6: Test Documentation (TEST.md - Part 2) + +After running all tests successfully, **append** to the existing TEST.md: + +1. **Test Results** — Paste the full `pytest -v --tb=no` output showing all tests + passing with their names and status +2. **Summary Statistics** — Total tests, pass rate, execution time +3. **Coverage Notes** — Any gaps or areas not covered by tests + +The TEST.md now serves as both the test plan (written before implementation) and +the test results documentation (appended after execution), providing a complete +record of the testing process. + +## Critical Lessons Learned + +### Use the Real Software — Don't Reimplement It + +**This is the #1 rule.** The CLI MUST call the actual software for rendering and +export — not reimplement the software's functionality in Python. + +**The anti-pattern:** Building a Pillow-based image compositor to replace GIMP, +or generating bpy scripts without ever calling Blender. This produces a toy that +can't handle real workloads and diverges from the actual software's behavior. + +**The correct approach:** +1. **Use the software's CLI/scripting interface** as the backend: + - LibreOffice: `libreoffice --headless --convert-to pdf/docx/xlsx/pptx` + - Blender: `blender --background --python script.py` + - GIMP: `gimp -i -b '(script-fu-console-eval ...)'` + - Inkscape: `inkscape --actions="..." --export-filename=...` + - Shotcut/Kdenlive: `melt project.mlt -consumer avformat:output.mp4` + - Audacity: `sox` for effects processing + - OBS: `obs-websocket` protocol + +2. **The software is a required dependency**, not optional. Add it to installation + instructions. The CLI is useless without the actual software. + +3. **Generate valid project/intermediate files** (ODF, MLT XML, .blend, SVG, etc.) + then hand them to the real software for rendering. Your CLI is a structured + command-line interface to the software, not a replacement for it. + +**Example — LibreOffice CLI export pipeline:** +```python +# 1. Build the document as a valid ODF file (our XML builder) +odf_path = write_odf(tmp_path, doc_type, project) + +# 2. Convert via the REAL LibreOffice (not a reimplementation) +subprocess.run([ + "libreoffice", "--headless", + "--convert-to", "pdf", + "--outdir", output_dir, + odf_path, +]) +# Result: a real PDF rendered by LibreOffice's full engine +``` + +### The Rendering Gap + +**This is the #2 pitfall.** Most GUI apps apply effects at render time via their +engine. When you build a CLI that manipulates project files directly, you must also +handle rendering — and naive approaches will silently drop effects. + +**The problem:** Your CLI adds filters/effects to the project file format. But when +rendering, if you use a simple tool (e.g., ffmpeg concat demuxer), it reads raw +media files and **ignores** all project-level effects. The output looks identical to +the input. Users can't tell anything happened. + +**The solution — a filter translation layer:** +1. **Best case:** Use the app's native renderer (`melt` for MLT projects). It reads + the project file and applies everything. +2. **Fallback:** Build a translation layer that converts project-format effects into + the rendering tool's native syntax (e.g., MLT filters → ffmpeg `-filter_complex`). +3. **Last resort:** Generate a render script the user can run manually. + +**Priority order for rendering:** native engine → translated filtergraph → script. + +### Filter Translation Pitfalls + +When translating effects between formats (e.g., MLT → ffmpeg), watch for: + +- **Duplicate filter types:** Some tools (ffmpeg) don't allow the same filter twice + in a chain. If your project has both `brightness` and `saturation` filters, and + both map to ffmpeg's `eq=`, you must **merge** them into a single `eq=brightness=X:saturation=Y`. +- **Ordering constraints:** ffmpeg's `concat` filter requires **interleaved** stream + ordering: `[v0][a0][v1][a1][v2][a2]`, NOT grouped `[v0][v1][v2][a0][a1][a2]`. + The error message ("media type mismatch") is cryptic if you don't know this. +- **Parameter space differences:** Effect parameters often use different scales. + MLT brightness `1.15` = +15%, but ffmpeg `eq=brightness=0.06` on a -1..1 scale. + Document every mapping explicitly. +- **Unmappable effects:** Some effects have no equivalent in the render tool. Handle + gracefully (warn, skip) rather than crash. + +### Timecode Precision + +Non-integer frame rates (29.97fps = 30000/1001) cause cumulative rounding errors: + +- **Use `round()`, not `int()`** for float-to-frame conversion. `int(9000 * 29.97)` + truncates and loses frames; `round()` gets the right answer. +- **Use integer arithmetic for timecode display.** Convert frames → total milliseconds + via `round(frames * fps_den * 1000 / fps_num)`, then decompose with integer + division. Avoid intermediate floats that drift over long durations. +- **Accept ±1 frame tolerance** in roundtrip tests at non-integer FPS. Exact equality + is mathematically impossible. + +### Output Verification Methodology + +Never assume an export is correct just because it ran without errors. Verify: + +```python +# Video: probe specific frames with ffmpeg +# Frame 0 for fade-in (should be near-black) +# Middle frames for color effects (compare brightness/saturation vs source) +# Last frame for fade-out (should be near-black) + +# When comparing pixel values between different resolutions, +# exclude letterboxing/pillarboxing (black padding bars). +# A vertical video in a horizontal frame will have ~40% black pixels. + +# Audio: check RMS levels at start/end for fades +# Compare spectral characteristics against source +``` + +### Testing Strategy + +Four test layers with complementary purposes: + +1. **Unit tests** (`test_core.py`): Synthetic data, no external dependencies. Tests + every function in isolation. Fast, deterministic, good for CI. +2. **E2E tests — native** (`test_full_e2e.py`): Tests the project file generation + pipeline (ODF structure, XML content, format validation). Verifies the + intermediate files your CLI produces are correct. +3. **E2E tests — true backend** (`test_full_e2e.py`): Invokes the **real software** + (LibreOffice, Blender, melt, etc.) to produce final output files (PDF, DOCX, + rendered images, videos). Verifies the output files: + - Exist and have size > 0 + - Have correct format (magic bytes, ZIP structure, etc.) + - Contain expected content where verifiable + - **Print artifact paths** so users can manually inspect results +4. **CLI subprocess tests** (in `test_full_e2e.py`): Invokes the installed + `cli-anything-` command via `subprocess.run` to run the full workflow + end-to-end: create project → add content → export via real software → verify output. + +**No graceful degradation.** The real software MUST be installed. Tests must NOT +skip or fake results when the software is missing — the CLI is useless without it. +The software is a hard dependency, not optional. + +**Example — true E2E test for LibreOffice:** +```python +class TestWriterToPDF: + def test_rich_writer_to_pdf(self, tmp_dir): + proj = create_document(doc_type="writer", name="Report") + add_heading(proj, text="Quarterly Report", level=1) + add_table(proj, rows=3, cols=3, data=[...]) + + pdf_path = os.path.join(tmp_dir, "report.pdf") + result = export(proj, pdf_path, preset="pdf", overwrite=True) + + # Verify the REAL output file + assert os.path.exists(result["output"]) + assert result["file_size"] > 1000 # Not suspiciously small + with open(result["output"], "rb") as f: + assert f.read(5) == b"%PDF-" # Validate format magic bytes + print(f"\n PDF: {result['output']} ({result['file_size']:,} bytes)") + + +class TestCLISubprocessE2E: + CLI_BASE = _resolve_cli("cli-anything-libreoffice") + + def test_full_writer_pdf_workflow(self, tmp_dir): + proj_path = os.path.join(tmp_dir, "test.json") + pdf_path = os.path.join(tmp_dir, "output.pdf") + self._run(["document", "new", "-o", proj_path, "--type", "writer"]) + self._run(["--project", proj_path, "writer", "add-heading", "-t", "Title"]) + self._run(["--project", proj_path, "export", "render", pdf_path, "-p", "pdf", "--overwrite"]) + assert os.path.exists(pdf_path) + with open(pdf_path, "rb") as f: + assert f.read(5) == b"%PDF-" +``` + + Run tests in force-installed mode to guarantee the real command is used: + ```bash + CLI_ANYTHING_FORCE_INSTALLED=1 python3 -m pytest cli_anything//tests/ -v -s + ``` + The `-s` flag shows the `[_resolve_cli]` print output confirming which backend + is being used and **prints artifact paths** for manual inspection. + +Real-world workflow test scenarios should include: +- Multi-segment editing (YouTube-style cut/trim) +- Montage assembly (many short clips) +- Picture-in-picture compositing +- Color grading pipelines +- Audio mixing (podcast-style) +- Heavy undo/redo stress testing +- Save/load round-trips of complex projects +- Iterative refinement (add, modify, remove, re-add) + +## Key Principles + +- **Use the real software** — The CLI MUST invoke the actual application for rendering + and export. Generate valid intermediate files (ODF, MLT XML, .blend, SVG), then hand + them to the real software. Never reimplement the rendering engine in Python. +- **The software is a hard dependency** — Not optional, not gracefully degraded. If + LibreOffice isn't installed, `cli-anything-libreoffice` must error clearly, not + silently produce inferior output with a fallback library. +- **Manipulate the native format directly** — Parse and modify the app's native project + files (MLT XML, ODF, SVG, etc.) as the data layer. +- **Leverage existing CLI tools** — Use `libreoffice --headless`, `blender --background`, + `melt`, `ffmpeg`, `inkscape --actions`, `sox` as subprocesses for rendering. +- **Verify rendering produces correct output** — See "The Rendering Gap" above. +- **E2E tests must produce real artifacts** — PDF, DOCX, rendered images, videos. + Print output paths so users can inspect. Never test only the intermediate format. +- **Fail loudly and clearly** — Agents need unambiguous error messages to self-correct. +- **Be idempotent where possible** — Running the same command twice should be safe. +- **Provide introspection** — `info`, `list`, `status` commands are critical for agents + to understand current state before acting. +- **JSON output mode** — Every command should support `--json` for machine parsing. + +## Rules + +- **The real software MUST be a hard dependency.** The CLI must invoke the actual + software (LibreOffice, Blender, GIMP, etc.) for rendering and export. Do NOT + reimplement rendering in Python. Do NOT gracefully degrade to a fallback library. + If the software is not installed, the CLI must error with clear install instructions. +- **Every `cli_anything//` directory MUST contain a `README.md`** that explains how to + install the software dependency, install the CLI, run tests, and shows basic usage. +- **E2E tests MUST invoke the real software** and produce real output files (PDF, DOCX, + rendered images, videos). Tests must verify output exists, has correct format, and + print artifact paths so users can inspect results. Never test only intermediate files. +- **Every export/render function MUST be verified** with programmatic output analysis + before being marked as working. "It ran without errors" is not sufficient. +- **Every filter/effect in the registry MUST have a corresponding render mapping** + or be explicitly documented as "project-only (not rendered)". +- **Test suites MUST include real-file E2E tests**, not just unit tests with synthetic + data. Format assumptions break constantly with real media. +- **E2E tests MUST include subprocess tests** that invoke the installed + `cli-anything-` command via `_resolve_cli()`. Tests must work against + the actual installed package, not just source imports. +- **Every `cli_anything//tests/` directory MUST contain a `TEST.md`** documenting what the tests + cover, what realistic workflows are tested, and the full test results output. +- **Every CLI MUST use the unified REPL skin** (`repl_skin.py`) for the interactive mode. + Copy `cli-anything-plugin/repl_skin.py` to `utils/repl_skin.py` and use `ReplSkin` + for the banner, prompt, help, messages, and goodbye. REPL MUST be the default behavior + when the CLI is invoked without a subcommand (`invoke_without_command=True`). + +## Directory Structure + +``` +/ +└── agent-harness/ + ├── .md # Project-specific analysis and SOP + ├── setup.py # PyPI package configuration (Phase 7) + ├── cli_anything/ # Namespace package (NO __init__.py here) + │ └── / # Sub-package for this CLI + │ ├── __init__.py + │ ├── __main__.py # python3 -m cli_anything. + │ ├── README.md # HOW TO RUN — required + │ ├── _cli.py # Main CLI entry point (Click + REPL) + │ ├── core/ # Core modules (one per domain) + │ │ ├── __init__.py + │ │ ├── project.py # Project create/open/save/info + │ │ ├── ... # Domain-specific modules + │ │ ├── export.py # Render pipeline + filter translation + │ │ └── session.py # Stateful session, undo/redo + │ ├── utils/ # Shared utilities + │ │ ├── __init__.py + │ │ ├── _backend.py # Backend: invokes the real software + │ │ └── repl_skin.py # Unified REPL skin (copy from plugin) + │ └── tests/ # Test suites + │ ├── TEST.md # Test documentation and results — required + │ ├── test_core.py # Unit tests (synthetic data) + │ └── test_full_e2e.py # E2E tests (real files) + └── examples/ # Example scripts and workflows +``` + +**Critical:** The `cli_anything/` directory must NOT contain an `__init__.py`. +This is what makes it a PEP 420 namespace package — multiple separately-installed +PyPI packages can each contribute a sub-package under `cli_anything/` without +conflicting. For example, `cli-anything-gimp` adds `cli_anything/gimp/` and +`cli-anything-blender` adds `cli_anything/blender/`, and both coexist in the +same Python environment. + +Note: This HARNESS.md is part of the cli-anything-plugin. Individual software directories reference this file — do NOT duplicate it. + +## Applying This to Other Software + +This same SOP applies to any GUI application: + +| Software | Backend CLI | Native Format | System Package | How the CLI Uses It | +|----------|-------------|---------------|----------------|-------------------| +| LibreOffice | `libreoffice --headless` | .odt/.ods/.odp (ODF ZIP) | `apt install libreoffice` | Generate ODF → convert to PDF/DOCX/XLSX/PPTX | +| Blender | `blender --background --python` | .blend-cli.json | `apt install blender` | Generate bpy script → Blender renders to PNG/MP4 | +| GIMP | `gimp -i -b '(script-fu ...)'` | .xcf | `apt install gimp` | Script-Fu commands → GIMP processes & exports | +| Inkscape | `inkscape --actions="..."` | .svg (XML) | `apt install inkscape` | Manipulate SVG → Inkscape exports to PNG/PDF | +| Shotcut/Kdenlive | `melt` or `ffmpeg` | .mlt (XML) | `apt install melt ffmpeg` | Build MLT XML → melt/ffmpeg renders video | +| Audacity | `sox` | .aup3 | `apt install sox` | Generate sox commands → sox processes audio | +| OBS Studio | `obs-websocket` | scene.json | `apt install obs-studio` | WebSocket API → OBS captures/records | + +**The software is a required dependency, not optional.** The CLI generates valid +intermediate files (ODF, MLT XML, bpy scripts, SVG) and hands them to the real +software for rendering. This is what makes the CLI actually useful — it's a +command-line interface TO the software, not a replacement for it. + +The pattern is always the same: **build the data → call the real software → verify +the output**. + +### Phase 7: PyPI Publishing and Installation + +After building and testing the CLI, make it installable and discoverable. + +All cli-anything CLIs use **PEP 420 namespace packages** under the shared +`cli_anything` namespace. This allows multiple CLI packages to be installed +side-by-side in the same Python environment without conflicts. + +1. **Structure the package** as a namespace package: + ``` + agent-harness/ + ├── setup.py + └── cli_anything/ # NO __init__.py here (namespace package) + └── / # e.g., gimp, blender, audacity + ├── __init__.py # HAS __init__.py (regular sub-package) + ├── _cli.py + ├── core/ + ├── utils/ + └── tests/ + ``` + + The key rule: `cli_anything/` has **no** `__init__.py`. Each sub-package + (`gimp/`, `blender/`, etc.) **does** have `__init__.py`. This is what + enables multiple packages to contribute to the same namespace. + +2. **Create setup.py** in the `agent-harness/` directory: + ```python + from setuptools import setup, find_namespace_packages + + setup( + name="cli-anything-", + version="1.0.0", + packages=find_namespace_packages(include=["cli_anything.*"]), + install_requires=[ + "click>=8.0.0", + "prompt-toolkit>=3.0.0", + # Add Python library dependencies here + ], + entry_points={ + "console_scripts": [ + "cli-anything-=cli_anything.._cli:main", + ], + }, + python_requires=">=3.10", + ) + ``` + + **Important details:** + - Use `find_namespace_packages`, NOT `find_packages` + - Use `include=["cli_anything.*"]` to scope discovery + - Entry point format: `cli_anything.._cli:main` + - The **system package** (LibreOffice, Blender, etc.) is a **hard dependency** + that cannot be expressed in `install_requires`. Document it in README.md and + have the backend module raise a clear error with install instructions: + ```python + # In utils/_backend.py + def find_(): + path = shutil.which("") + if path: + return path + raise RuntimeError( + " is not installed. Install it with:\n" + " apt install # Debian/Ubuntu\n" + " brew install # macOS" + ) + ``` + +3. **All imports** use the `cli_anything.` prefix: + ```python + from cli_anything.gimp.core.project import create_project + from cli_anything.gimp.core.session import Session + from cli_anything.blender.core.scene import create_scene + ``` + +4. **Test local installation**: + ```bash + cd /root/cli-anything//agent-harness + pip install -e . + ``` + +5. **Verify PATH installation**: + ```bash + which cli-anything- + cli-anything- --help + ``` + +6. **Run tests against the installed command**: + ```bash + cd /root/cli-anything//agent-harness + CLI_ANYTHING_FORCE_INSTALLED=1 python3 -m pytest cli_anything//tests/ -v -s + ``` + The output must show `[_resolve_cli] Using installed command: /path/to/cli-anything-` + confirming subprocess tests ran against the real installed binary, not a module fallback. + +7. **Verify namespace works across packages** (when multiple CLIs installed): + ```python + import cli_anything.gimp + import cli_anything.blender + # Both resolve to their respective source directories + ``` + +**Why namespace packages:** +- Multiple CLIs coexist in the same Python environment without conflicts +- Clean, organized imports under a single `cli_anything` namespace +- Each CLI is independently installable/uninstallable via pip +- Agents can discover all installed CLIs via `cli_anything.*` +- Standard Python packaging — no hacks or workarounds diff --git a/03_卡木(木)/木识_软件识形/SKILL.md b/03_卡木(木)/木识_软件识形/SKILL.md new file mode 100644 index 00000000..4476548b --- /dev/null +++ b/03_卡木(木)/木识_软件识形/SKILL.md @@ -0,0 +1,25 @@ +--- +name: 木识(软件识形) +description: 卡木第四成员,专司软件识形——识别任意软件架构,生成 AI Agent 可驱动的 CLI 接口 +triggers: 木识、软件识形、CLI万能化、cli-anything、让软件被Agent控制 +owner: 木识 +group: 木(卡木) +version: "1.0" +updated: "2026-03-12" +--- + +# 木识 · 软件识形 + +> 「识形成器,万物可用。」 + +卡木第四成员。识别任意软件的能力与架构,通过 CLI-Anything 7阶段流水线,将任意有源码的软件转化为 AI Agent 可驱动的 CLI 接口。 + +## 技能清单 + +| 技能 | 路径 | 触发词 | +|:--|:---|:---| +| CLI万能化 | `CLI万能化/SKILL.md` | cli-anything、软件识形、让软件Agent化 | + +## 快速触发 + +说「木识」或「CLI万能化」即可激活,按 `CLI万能化/SKILL.md` 执行。 diff --git a/BOOTSTRAP.md b/BOOTSTRAP.md index 189fb4f3..ebfec312 100644 --- a/BOOTSTRAP.md +++ b/BOOTSTRAP.md @@ -19,7 +19,7 @@ 卡若AI(大总管) ├── 卡资(金)"稳了。" → 金仓(存储备份)、金盾(数据安全) ├── 卡人(水)"搞定了。" → 水溪(整理归档)、水泉(规划拆解)、水桥(平台对接) -├── 卡木(木)"搞起!" → 木叶(视频内容)、木根(逆向分析)、木果(项目模板) +├── 卡木(木)"搞起!" → 木叶(视频内容)、木根(逆向分析)、木果(项目模板)、木识(软件识形) ├── 卡火(火)"让我想想…" → 火炬(全栈消息)、火锤(代码修复)、火眼(智能追问)、火种(知识模型) └── 卡土(土)"先算账。" → 土基(商业分析)、土砖(技能复制)、土渠(流量招商)、土簿(财务管理) ``` diff --git a/SKILL_REGISTRY.md b/SKILL_REGISTRY.md index f70448da..9b255da9 100644 --- a/SKILL_REGISTRY.md +++ b/SKILL_REGISTRY.md @@ -116,6 +116,7 @@ | M07 | PPT制作 | 木果 | **PPT、做PPT、制作PPT、演示文稿、汇报PPT** | `03_卡木(木)/木果_项目模板/PPT制作/SKILL.md` | python-pptx 创建/编辑 .pptx,输出到报告目录 | | M08 | Next AI Draw | 木果 | **next ai draw、AI画图、画图表、架构图、流程图** | `03_卡木(木)/木果_项目模板/Next AI Draw/SKILL.md` | AI 生成 draw.io 风格图、Mermaid 图表,与 PPT 联动 | | M09 | 卡若个人介绍 | 木果 | **卡若介绍、个人介绍、卡若人设、我是谁** | `03_卡木(木)/木果_项目模板/卡若个人介绍/SKILL.md` | 生成卡若个人介绍(PPT/短文/一页纸) | +| M10 | **CLI万能化** | 木识 | **木识、CLI万能化、cli-anything、软件识形、让软件Agent化、任意软件CLI、给软件生成CLI、GUI转CLI、让软件可被AI控制** | `03_卡木(木)/木识_软件识形/CLI万能化/SKILL.md` | 任意软件→Agent可驱动CLI;7阶段全自动流水线;与木根联动 | ## 火组 · 卡火(技术研发优化) diff --git a/运营中枢/工作台/gitea_push_log.md b/运营中枢/工作台/gitea_push_log.md index 19060b6f..6a1d5790 100644 --- a/运营中枢/工作台/gitea_push_log.md +++ b/运营中枢/工作台/gitea_push_log.md @@ -312,3 +312,4 @@ | 2026-03-12 20:55:40 | 🔄 卡若AI 同步 2026-03-12 20:55 | 更新:运营中枢、运营中枢工作台 | 排除 >20MB: 11 个 | | 2026-03-12 21:30:48 | 🔄 卡若AI 同步 2026-03-12 21:26 | 更新:运营中枢、运营中枢工作台 | 排除 >20MB: 11 个 | | 2026-03-12 22:05:51 | 🔄 卡若AI 同步 2026-03-12 22:05 | 更新:水桥平台对接、运营中枢工作台 | 排除 >20MB: 11 个 | +| 2026-03-12 22:33:45 | 🔄 卡若AI 同步 2026-03-12 22:33 | 更新:水桥平台对接、总索引与入口、运营中枢工作台 | 排除 >20MB: 11 个 | diff --git a/运营中枢/工作台/代码管理.md b/运营中枢/工作台/代码管理.md index 7fc7ae39..b69a2741 100644 --- a/运营中枢/工作台/代码管理.md +++ b/运营中枢/工作台/代码管理.md @@ -315,3 +315,4 @@ | 2026-03-12 20:55:40 | 成功 | 成功 | 🔄 卡若AI 同步 2026-03-12 20:55 | 更新:运营中枢、运营中枢工作台 | 排除 >20MB: 11 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) | | 2026-03-12 21:30:48 | 成功 | 成功 | 🔄 卡若AI 同步 2026-03-12 21:26 | 更新:运营中枢、运营中枢工作台 | 排除 >20MB: 11 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) | | 2026-03-12 22:05:51 | 成功 | 成功 | 🔄 卡若AI 同步 2026-03-12 22:05 | 更新:水桥平台对接、运营中枢工作台 | 排除 >20MB: 11 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) | +| 2026-03-12 22:33:45 | 成功 | 成功 | 🔄 卡若AI 同步 2026-03-12 22:33 | 更新:水桥平台对接、总索引与入口、运营中枢工作台 | 排除 >20MB: 11 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |