🔄 卡若AI 同步 2026-03-12 23:10 | 更新:水桥平台对接、卡木、总索引与入口、运营中枢工作台 | 排除 >20MB: 11 个

This commit is contained in:
2026-03-12 23:10:26 +08:00
parent 919c47e605
commit e0ee8c2e2a
12 changed files with 1603 additions and 68 deletions

View File

@@ -1,6 +1,6 @@
{ {
"access_token": "u-e9uHxerhZ7VrnwkCFC9Yvalh3Ix1ghWXpgGaZMk0260Y", "access_token": "u-e1gGA5qAp2yXEyJOa2k9Zalh3KxxghONV0GaJwk0274U",
"refresh_token": "ur-dIaKx4ssV4SU.karG6lt9ulh1C11ghopOgGaYx00261E", "refresh_token": "ur-cuP.iNyXxflV6qpKn0c2Lnlh3AxxghqPpwGaIQ4022gJ",
"name": "飞书用户", "name": "飞书用户",
"auth_time": "2026-03-12T20:33:34.954705" "auth_time": "2026-03-12T22:44:05.347461"
} }

View File

@@ -326,18 +326,29 @@ def _get_text_content(block: dict) -> str:
return (tr.get("content") or "") 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: def sanitize_blocks(blocks: list) -> list:
""" """飞书 docx blocks 轻量清洗:去掉空文本/空代码块/空 callout避免 invalid param。"""
飞书 docx blocks 对“空段落/异常结构”会严格校验。
这里做一次轻量清洗:去掉纯空文本块,避免 invalid param。
"""
out = [] out = []
for b in blocks: for b in blocks:
if not isinstance(b, dict): if not isinstance(b, dict):
continue continue
if b.get("block_type") == 2: bt = b.get("block_type")
c = _get_text_content(b) if bt == 2:
if not c or not c.strip(): 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 continue
out.append(b) out.append(b)
return out return out
@@ -438,6 +449,21 @@ def _write_batch_with_fallback(doc_token: str, headers: dict, batch: list, total
if r1.get("code") == 0: if r1.get("code") == 0:
time.sleep(0.35) time.sleep(0.35)
continue 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) c = _get_text_content(b)
preview = (c[:60] + "...") if c and len(c) > 60 else (c or "") 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}") print(f"⚠️ 跳过非法块: code={r1.get('code')} msg={r1.get('msg')} preview={preview!r}")

View File

@@ -1,7 +1,12 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
将 Markdown 本地转换为飞书文档 JSON 格式。 将 Markdown 本地转换为飞书文档 JSON 格式v2
图片用占位符 __IMAGE:路径__ 标注,上传时替换为 file_token。 · 代码围栏 → 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 python3 md_to_feishu_json.py input.md output.json
@@ -14,24 +19,71 @@ import argparse
from pathlib import Path 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": {}}} 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": {}}} 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": {}}} 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": {}}} 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_MAP1=纯文本/流程图"""
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: def _image_placeholder(idx: int, path: str) -> dict:
"""图片占位符,上传时由脚本替换为 gallery block"""
return {"__image__": path, "__index__": idx} 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]: def _parse_md_row(line: str) -> list[str]:
s = line.strip() s = line.strip()
if s.startswith("|"): if s.startswith("|"):
@@ -63,54 +117,71 @@ def _is_md_table_sep(line: str) -> bool:
if s.endswith("|"): if s.endswith("|"):
s = s[:-1] s = s[:-1]
parts = [p.strip() for p in s.split("|")] parts = [p.strip() for p in s.split("|")]
if not parts: return bool(parts) and all(re.match(r"^:?-{3,}:?$", p or "") for p in parts)
return False
return all(re.match(r"^:?-{3,}:?$", p or "") for p in parts)
def _clean_inline_markdown(text: str) -> str: def _clean_inline_markdown(text: str) -> str:
"""清理常见行内 markdown 标记,输出适合飞书阅读的纯文本。""" """去掉常见行内 Markdown 标记,输出适合飞书的纯文本。"""
t = text 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)
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() return t.strip()
# ── 主转换函数 ─────────────────────────────────────────────────────────────────
def md_to_blocks(md: str, image_paths: list[str] | None = None) -> list: def md_to_blocks(md: str, image_paths: list[str] | None = None) -> list:
"""将 Markdown 转为飞书 blocks""" """将 Markdown 字符串转为飞书 blocks 列表。"""
blocks = [] blocks: list[dict] = []
image_paths = image_paths or [] image_paths = image_paths or []
img_idx = 0 img_idx = 0
first_h1_consumed = False first_h1_consumed = False
in_code = False in_code = False
code_lines = [] code_lines: list[str] = []
code_lang = 1
lines = md.split("\n") lines = md.split("\n")
i = 0 i = 0
while i < len(lines): while i < len(lines):
line = lines[i] line = lines[i]
if line.strip().startswith("```"): stripped = line.strip()
# ── 代码围栏 ───────────────────────────────────────────────────────────
if stripped.startswith("```"):
if in_code: if in_code:
# 飞书 blocks 常对代码围栏/特殊格式更严格,这里转为普通文本行,提升美观与稳定性 # 结束代码块 → 生成单个 block_type:14
for cl in code_lines: code_content = "\n".join(code_lines)
if cl.strip(): if code_content.strip():
blocks.append(_text(f"代码:{cl.strip()}")) blocks.append(_code(code_content, code_lang))
code_lines = [] 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 i += 1
continue continue
if in_code: if in_code:
code_lines.append(line) code_lines.append(line)
i += 1 i += 1
continue continue
# 图片语法 ![](path) # ── 图片 ──────────────────────────────────────────────────────────────
img_match = re.match(r"^!\[([^\]]*)\]\(([^)]+)\)\s*$", line.strip()) img_match = re.match(r"^!\[([^\]]*)\]\(([^)]+)\)\s*$", stripped)
if img_match: if img_match:
path = img_match.group(2) path = img_match.group(2)
if img_idx < len(image_paths): 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 i += 1
continue continue
# Markdown 表格:表头 + 分隔行 + 数据行 # ── Markdown 表格 ─────────────────────────────────────────────────────
if "|" in line and i + 1 < len(lines) and _is_md_table_sep(lines[i + 1]): if "|" in line and i + 1 < len(lines) and _is_md_table_sep(lines[i + 1]):
table_lines = [line] table_lines = [line]
j = i + 2 j = i + 2
@@ -128,7 +199,7 @@ def md_to_blocks(md: str, image_paths: list[str] | None = None) -> list:
raw = lines[j].strip() raw = lines[j].strip()
if not raw or "|" not in raw: if not raw or "|" not in raw:
break break
if raw.startswith("#") or raw.startswith(">") or raw.startswith("```"): if raw.startswith(("#", ">", "```")):
break break
table_lines.append(lines[j]) table_lines.append(lines[j])
j += 1 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))) rr.extend([""] * (col_size - len(rr)))
clean_rows.append(rr[:col_size]) clean_rows.append(rr[:col_size])
# 飞书空 sheet 创建限制:行列最大 9超出时截断并提示
max_rows, max_cols = 9, 9 max_rows, max_cols = 9, 9
if len(clean_rows) > max_rows or col_size > max_cols: 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]] clipped = [r[:max_cols] for r in clean_rows[:max_rows]]
blocks.append(_sheet_table(clipped)) blocks.append(_sheet_table(clipped))
i = j i = j
continue continue
# 忽略 Markdown 水平分隔线(避免在飞书出现大量“---”影响观感) # ── 分割线 → block_type:22 ────────────────────────────────────────────
if line.strip() in {"---", "***", "___"}: if stripped in {"---", "***", "___"}:
blocks.append(_divider())
i += 1 i += 1
continue continue
# 标题 # ── 标题 ──────────────────────────────────────────────────────────────
if line.startswith("# "): if line.startswith("#### "):
# 避免正文和文档标题重复:默认跳过第一行 H1 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: if first_h1_consumed:
blocks.append(_h1(_clean_inline_markdown(line[2:].strip()))) blocks.append(_h1(_clean_inline_markdown(line[2:].strip())))
else: else:
first_h1_consumed = True first_h1_consumed = True
elif line.startswith("## "):
blocks.append(_h2(_clean_inline_markdown(line[3:].strip()))) # ── 引用 → block_type:19 callout ─────────────────────────────────────
elif line.startswith("### "): elif stripped.startswith(">"):
blocks.append(_h3(_clean_inline_markdown(line[4:].strip()))) quote = stripped
elif line.lstrip().startswith(">"):
# 引用块转普通说明行,降低写入失败概率
quote = line.lstrip()
while quote.startswith(">"): while quote.startswith(">"):
quote = quote[1:].lstrip() quote = quote[1:].lstrip()
quote = _clean_inline_markdown(quote) quote = _clean_inline_markdown(quote)
if quote: if quote:
blocks.append(_text(quote)) blocks.append(_callout(quote))
elif line.strip():
raw = line.strip() # ── 正文、列表 ────────────────────────────────────────────────────────
# 无序列表统一成 •,减少 markdown 观感噪音 elif stripped:
raw = stripped
# 无序列表 → •
if re.match(r"^[-*]\s+", raw): if re.match(r"^[-*]\s+", raw):
raw = "" + re.sub(r"^[-*]\s+", "", raw) raw = "" + re.sub(r"^[-*]\s+", "", raw)
# 有序列表统一成 12样式 # 有序列表 12
raw = re.sub(r"^(\d+)\.\s+", r"\1", raw) raw = re.sub(r"^(\d+)\.\s+", r"\1", raw)
cleaned = _clean_inline_markdown(raw) cleaned = _clean_inline_markdown(raw)
if cleaned: 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]: def blocks_to_upload_format(blocks: list, base_dir: Path) -> tuple[list, list]:
""" """将含 __image__ 占位符的 blocks 转为可上传格式,返回 (blocks, image_paths)。"""
将含 __image__ 占位符的 blocks 转为可上传格式。 out: list = []
返回 (文本 blocks 列表, 图片路径列表,按出现顺序)。 paths: list[str] = []
image_paths 优先存相对路径(相对 base_dir便于 JSON 移植。
"""
out = []
paths = []
for b in blocks: for b in blocks:
if isinstance(b, dict) and "__image__" in b: if isinstance(b, dict) and "__image__" in b:
path = b.get("__image__", "") path = b.get("__image__", "")
@@ -216,14 +287,16 @@ def blocks_to_upload_format(blocks: list, base_dir: Path) -> tuple[list, list]:
rel = str(resolved) rel = str(resolved)
paths.append(rel) paths.append(rel)
else: else:
paths.append(path if path else "unknown") paths.append(path or "unknown")
out.append({"block_type": 2, "text": {"elements": [{"text_run": {"content": f"【配图 {len(paths)}:待上传】", "text_element_style": {}}}], "style": {}}}) out.append(
_text(f"【配图 {len(paths)}:待上传】")
)
else: else:
out.append(b) out.append(b)
return out, paths return out, paths
def main(): def main() -> None:
ap = argparse.ArgumentParser() ap = argparse.ArgumentParser()
ap.add_argument("input", help="Markdown 文件") ap.add_argument("input", help="Markdown 文件")
ap.add_argument("output", help="输出 JSON 文件") ap.add_argument("output", help="输出 JSON 文件")
@@ -241,7 +314,7 @@ def main():
final, img_paths = blocks_to_upload_format(blocks, inp.parent) final, img_paths = blocks_to_upload_format(blocks, inp.parent)
out = { out = {
"description": f"{inp.name} 转换的飞书 docx blocks", "description": f"{inp.name} 转换的飞书 docx blocksv2",
"source": str(inp), "source": str(inp),
"image_paths": img_paths, "image_paths": img_paths,
"children": final, "children": final,

View File

@@ -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()

View File

@@ -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**
生成以下目录结构:
```
<software>/
└── agent-harness/
├── <SOFTWARE>.md ← 软件专属 SOP
├── setup.py ← pip 可安装
└── cli_anything/
└── <software>/
├── README.md
├── __init__.py
├── __main__.py
├── <software>_cli.py ← 主 CLIClick
├── core/ ← 核心操作模块
├── utils/
│ ├── repl_skin.py ← REPL UI从 harness_templates/ 复制)
│ └── <software>_backend.py ← 真实后端封装
└── tests/
├── test_core.py ← 单元测试
└── test_full_e2e.py ← 端到端测试
```
**阶段4-5测试Test**
```bash
cd <software>/agent-harness
pip install -e . --break-system-packages 2>/dev/null || pip install -e . --user
python -m pytest cli_anything/<software>/tests/ -v
```
**阶段6文档**
- 更新 TEST.md记录测试结果
**阶段7发布Publish**
```bash
pip install -e . --break-system-packages 2>/dev/null || pip install -e . --user
which cli-anything-<software>
cli-anything-<software> --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通道 |

View File

@@ -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/<software>/utils/repl_skin.py
Usage:
from cli_anything.<software>.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-<software>/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
}

View File

@@ -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/<software>_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.<software>.utils.repl_skin import ReplSkin
skin = ReplSkin("<software>", 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-<software>` 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/<software>/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-<software>` 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-<software>")
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-<software>")` — 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-<software>` 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/<software>/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/<software>/` 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-<software>` command via `_resolve_cli()`. Tests must work against
the actual installed package, not just source imports.
- **Every `cli_anything/<software>/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
```
<software>/
└── agent-harness/
├── <SOFTWARE>.md # Project-specific analysis and SOP
├── setup.py # PyPI package configuration (Phase 7)
├── cli_anything/ # Namespace package (NO __init__.py here)
│ └── <software>/ # Sub-package for this CLI
│ ├── __init__.py
│ ├── __main__.py # python3 -m cli_anything.<software>
│ ├── README.md # HOW TO RUN — required
│ ├── <software>_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
│ │ ├── <software>_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)
└── <software>/ # e.g., gimp, blender, audacity
├── __init__.py # HAS __init__.py (regular sub-package)
├── <software>_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-<software>",
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-<software>=cli_anything.<software>.<software>_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.<software>.<software>_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/<software>_backend.py
def find_<software>():
path = shutil.which("<software>")
if path:
return path
raise RuntimeError(
"<Software> is not installed. Install it with:\n"
" apt install <software> # Debian/Ubuntu\n"
" brew install <software> # macOS"
)
```
3. **All imports** use the `cli_anything.<software>` 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/<software>/agent-harness
pip install -e .
```
5. **Verify PATH installation**:
```bash
which cli-anything-<software>
cli-anything-<software> --help
```
6. **Run tests against the installed command**:
```bash
cd /root/cli-anything/<software>/agent-harness
CLI_ANYTHING_FORCE_INSTALLED=1 python3 -m pytest cli_anything/<software>/tests/ -v -s
```
The output must show `[_resolve_cli] Using installed command: /path/to/cli-anything-<software>`
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

View File

@@ -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` 执行。

View File

@@ -19,7 +19,7 @@
卡若AI大总管 卡若AI大总管
├── 卡资(金)"稳了。" → 金仓(存储备份)、金盾(数据安全) ├── 卡资(金)"稳了。" → 金仓(存储备份)、金盾(数据安全)
├── 卡人(水)"搞定了。" → 水溪(整理归档)、水泉(规划拆解)、水桥(平台对接) ├── 卡人(水)"搞定了。" → 水溪(整理归档)、水泉(规划拆解)、水桥(平台对接)
├── 卡木(木)"搞起!" → 木叶(视频内容)、木根(逆向分析)、木果(项目模板) ├── 卡木(木)"搞起!" → 木叶(视频内容)、木根(逆向分析)、木果(项目模板)、木识(软件识形)
├── 卡火(火)"让我想想…" → 火炬(全栈消息)、火锤(代码修复)、火眼(智能追问)、火种(知识模型) ├── 卡火(火)"让我想想…" → 火炬(全栈消息)、火锤(代码修复)、火眼(智能追问)、火种(知识模型)
└── 卡土(土)"先算账。" → 土基(商业分析)、土砖(技能复制)、土渠(流量招商)、土簿(财务管理) └── 卡土(土)"先算账。" → 土基(商业分析)、土砖(技能复制)、土渠(流量招商)、土簿(财务管理)
``` ```

View File

@@ -116,6 +116,7 @@
| M07 | PPT制作 | 木果 | **PPT、做PPT、制作PPT、演示文稿、汇报PPT** | `03_卡木/木果_项目模板/PPT制作/SKILL.md` | python-pptx 创建/编辑 .pptx输出到报告目录 | | 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 联动 | | M08 | Next AI Draw | 木果 | **next ai draw、AI画图、画图表、架构图、流程图** | `03_卡木/木果_项目模板/Next AI Draw/SKILL.md` | AI 生成 draw.io 风格图、Mermaid 图表,与 PPT 联动 |
| M09 | 卡若个人介绍 | 木果 | **卡若介绍、个人介绍、卡若人设、我是谁** | `03_卡木/木果_项目模板/卡若个人介绍/SKILL.md` | 生成卡若个人介绍PPT/短文/一页纸) | | M09 | 卡若个人介绍 | 木果 | **卡若介绍、个人介绍、卡若人设、我是谁** | `03_卡木/木果_项目模板/卡若个人介绍/SKILL.md` | 生成卡若个人介绍PPT/短文/一页纸) |
| M10 | **CLI万能化** | 木识 | **木识、CLI万能化、cli-anything、软件识形、让软件Agent化、任意软件CLI、给软件生成CLI、GUI转CLI、让软件可被AI控制** | `03_卡木/木识_软件识形/CLI万能化/SKILL.md` | 任意软件→Agent可驱动CLI7阶段全自动流水线与木根联动 |
## 火组 · 卡火(技术研发优化) ## 火组 · 卡火(技术研发优化)

View File

@@ -312,3 +312,4 @@
| 2026-03-12 20:55:40 | 🔄 卡若AI 同步 2026-03-12 20:55 | 更新:运营中枢、运营中枢工作台 | 排除 >20MB: 11 个 | | 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 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:05:51 | 🔄 卡若AI 同步 2026-03-12 22:05 | 更新:水桥平台对接、运营中枢工作台 | 排除 >20MB: 11 个 |
| 2026-03-12 22:33:45 | 🔄 卡若AI 同步 2026-03-12 22:33 | 更新:水桥平台对接、总索引与入口、运营中枢工作台 | 排除 >20MB: 11 个 |

View File

@@ -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 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 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: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) |