feat(cli): 支持 opencode MCP 一键接入(autoclip mcp install opencode) (#229)

* feat(cli): 支持 opencode MCP 一键接入(autoclip mcp install opencode)

新增 autoclip mcp install opencode:自动探测启动命令(autoclip / autoclip-mcp / python -m backend.mcp_server),把 mcp.autoclip 合并写入 opencode 配置(全局 ~/.config/opencode/opencode.json,或 --scope project 写项目 opencode.json)。

不覆盖用户已有配置:写入前备份 opencode.json.bak;JSONC 注释配置默认不动,--force 才先备份再重写;无法解析的配置永不覆盖。新增 backend/services/opencode_setup.py(仅标准库)与 7 个单元测试;新增 docs/OPENCODE.md / docs/OPENCODE.en.md,并更新 CLI_AND_MCP、README、mcp_server.py docstring。

* docs: 文档中心收录 opencode 接入指南

* fix(cli): opencode 接入修复 [P2]:重装保留自定义字段、jsonc 优先级与冲突拒绝

重装只更新 type / command,按 key 合并 environment(模块回退写 PYTHONPATH),保留 timeout / enabled 等用户字段;只有 opencode.jsonc 时写 jsonc,两份配置都有且 jsonc 含 mcp 段时写入前直接拒绝;解析兼容 UTF-8 BOM。回归测试 16 passed。

* fix(cli): honor explicit OpenCode config and refresh fallback paths

---------

Co-authored-by: 周小舟 <christine_zhouye@163.com>
This commit is contained in:
Naman Arora
2026-09-30 22:03:11 +08:00
committed by GitHub
co-authored by 周小舟
parent 70e4f42495
commit 8968916b70
10 changed files with 901 additions and 9 deletions
+2 -2
View File
@@ -242,7 +242,7 @@ autoclip export PROJECT_ID --preset shorts
autoclip mcp
```
In your MCP client, set `command` to the absolute path of `autoclip` in your virtual environment and `args` to `["mcp"]`. See the [CLI / MCP guide](docs/CLI_AND_MCP.md) and [Agent skill](skills/autoclip/SKILL.md) (both in Chinese).
In your MCP client, set `command` to the absolute path of `autoclip` in your virtual environment and `args` to `["mcp"]`. opencode users can connect with one command: `autoclip mcp install opencode` (see the [OpenCode guide](docs/OPENCODE.en.md)). See the [CLI / MCP guide](docs/CLI_AND_MCP.md) and [Agent skill](skills/autoclip/SKILL.md) (both in Chinese).
</details>
@@ -306,7 +306,7 @@ See the [first-clip guide](docs/USER_INSTALLATION_GUIDE.en.md) for sample prepar
| Guide | Link |
| --- | --- |
| Getting started | [Installation](docs/USER_INSTALLATION_GUIDE.en.md) |
| Hosting and automation | [Docker](docs/DOCKER.en.md) · [CLI / MCP (Chinese)](docs/CLI_AND_MCP.md) · [Agent skill (Chinese)](skills/autoclip/SKILL.md) |
| Hosting and automation | [Docker](docs/DOCKER.en.md) · [CLI / MCP (Chinese)](docs/CLI_AND_MCP.md) · [Agent skill (Chinese)](skills/autoclip/SKILL.md) · [opencode guide](docs/OPENCODE.en.md) |
| Models and troubleshooting | [Model configuration (Chinese)](docs/MULTI_LLM_PROVIDER_GUIDE.md) · [Troubleshooting](docs/FAQ.en.md) |
| Versions and privacy | [Changelog](CHANGELOG.md) · [Privacy](docs/PRIVACY.en.md) |
| Development and translation | [Contributing (Chinese)](CONTRIBUTING.md) · [Translation maintenance (Chinese)](docs/i18n.md) |
+2 -2
View File
@@ -236,7 +236,7 @@ autoclip export PROJECT_ID --preset shorts
autoclip mcp
```
在 MCP 客户端中将 `command` 设为虚拟环境里 `autoclip` 的绝对路径,`args` 设为 `["mcp"]`。详见 [CLI / MCP 指南](docs/CLI_AND_MCP.md)(中文)和 [Agent skill](skills/autoclip/SKILL.md)(中文)。
在 MCP 客户端中将 `command` 设为虚拟环境里 `autoclip` 的绝对路径,`args` 设为 `["mcp"]`。opencode 用户一条命令即可接入:`autoclip mcp install opencode`(见 [OpenCode 接入](docs/OPENCODE.md))。详见 [CLI / MCP 指南](docs/CLI_AND_MCP.md)(中文)和 [Agent skill](skills/autoclip/SKILL.md)(中文)。
</details>
@@ -298,7 +298,7 @@ AutoClip 免费、开源(MIT)。云端模型由所选服务商计费,需
| 你想了解 | 文档 |
| --- | --- |
| 安装与首次出片 | [安装指南](docs/USER_INSTALLATION_GUIDE.md) |
| 自建服务与自动化 | [Docker 部署](DOCKER.md) · [CLI / MCP](docs/CLI_AND_MCP.md) · [Agent skill](skills/autoclip/SKILL.md) |
| 自建服务与自动化 | [Docker 部署](DOCKER.md) · [CLI / MCP](docs/CLI_AND_MCP.md) · [Agent skill](skills/autoclip/SKILL.md) · [OpenCode 接入](docs/OPENCODE.md) |
| 配置模型与排错 | [模型配置](docs/MULTI_LLM_PROVIDER_GUIDE.md) · [常见问题](docs/FAQ.md) |
| 版本与隐私 | [更新日志](CHANGELOG.md) · [隐私说明](docs/PRIVACY.md) |
| 参与开发与翻译 | [贡献指南](CONTRIBUTING.md) · [翻译维护](docs/i18n.md) |
+60 -2
View File
@@ -9,6 +9,7 @@ autoclip — 命令行出片。
autoclip run video.mp4 --srt video.srt --min-score 0.6 --json
autoclip list / show <project_id> / providers / doctor
autoclip publish <project_id> --clip 2 --platform tiktok --platform youtube # 经 Upload-Post 发到海外平台
autoclip mcp install opencode # 把 AutoClip 写进 opencode 的 MCP 配置(默认全局)
产物与桌面应用共用同一个数据目录(mac: ~/Library/Application Support/AutoClip),
跑完在桌面应用首页就能看到。用 --data-dir 或 AUTOCLIP_DATA_DIR 可以换目录。
@@ -444,11 +445,58 @@ def cmd_publish(args: argparse.Namespace) -> int:
# ---------------------------------------------------------------- mcp ---
def cmd_mcp(args: argparse.Namespace) -> int:
if getattr(args, "mcp_cmd", None) == "install":
return cmd_mcp_install(args)
from backend.mcp_server import main as mcp_main
return mcp_main()
def _print_opencode_next_steps() -> None:
print(_dim(" 在 opencode 里直接说:把 C:\\Videos\\talk.mp4 切片,它会调 AutoClip 的 MCP 工具"))
print(_dim(" 模型没配好先跑 autoclip doctor;opencode 要新开会话才会加载新的 MCP"))
def cmd_mcp_install(args: argparse.Namespace) -> int:
"""把 AutoClip 写进 MCP 客户端配置;目前支持 opencode。"""
from backend.services import opencode_setup
target = opencode_setup.opencode_config_path(args.scope, Path(args.dir) if args.dir else None)
if args.print_only:
snippet = opencode_setup.render_snippet(opencode_setup.build_entry(), name=args.name)
if args.json:
print(json.dumps({"client": args.client, "path": str(target), "snippet": snippet}, ensure_ascii=False, indent=2))
else:
print(snippet)
print(_dim(f"\n把上面的片段合并进 {target}"), file=sys.stderr)
return 0
explicit_config = args.scope == "global" and bool(os.environ.get("OPENCODE_CONFIG"))
report = opencode_setup.install_opencode(
target, name=args.name, force=args.force, discover_jsonc=not explicit_config,
)
if args.json:
print(json.dumps(report, ensure_ascii=False, indent=2))
elif report["action"] == "created":
print(_c("32", "✓ ") + f"已写入 {report['path']}")
_print_opencode_next_steps()
elif report["action"] == "updated":
print(_c("32", "✓ ") + f"已更新 {report['path']} 的 mcp.{args.name}")
if report.get("backup"):
print(_dim(f" 旧配置已备份:{report['backup']}"))
_print_opencode_next_steps()
elif report["action"] == "unchanged":
print(_dim(f"• {report['path']} 里已有相同的 mcp.{args.name},未改动"))
else:
_err(report.get("error") or "写入失败")
if report.get("hint"):
print(_dim(f" {report['hint']}"), file=sys.stderr)
print(report["snippet"])
for w in report.get("warnings") or []:
print(_dim(f" ! {w}"), file=sys.stderr)
return 0 if report["ok"] else 1
# ---------------------------------------------------------------- parser ---
def build_parser() -> argparse.ArgumentParser:
p = argparse.ArgumentParser(
@@ -494,8 +542,18 @@ def build_parser() -> argparse.ArgumentParser:
_add_llm_args(d)
d.set_defaults(func=cmd_doctor)
m = sub.add_parser("mcp", help="以 MCP server(stdio)方式运行,供 Cursor / Claude 调用")
m.set_defaults(func=cmd_mcp)
m = sub.add_parser("mcp", help="以 MCP server(stdio)方式运行,供 Cursor / Claude / opencode 调用;install 子命令自动写客户端配置")
m.set_defaults(func=cmd_mcp) # 不带子命令 = 直接起 server(子命令的分发在 cmd_mcp 里判断)
mi_sub = m.add_subparsers(dest="mcp_cmd")
mi = mi_sub.add_parser("install", help="把 AutoClip 写进 MCP 客户端配置(目前支持 opencode)")
mi.add_argument("client", nargs="?", default="opencode", choices=["opencode"], help="MCP 客户端(默认 opencode)")
mi.add_argument("--scope", choices=["global", "project"], default="global",
help="global=~/.config/opencode/opencode.json;project=<--dir 目录>/opencode.json")
mi.add_argument("--dir", help="project 模式的项目目录(默认当前目录)")
mi.add_argument("--name", default="autoclip", help="配置里的 MCP 服务名(默认 autoclip)")
mi.add_argument("--print", dest="print_only", action="store_true", help="只打印配置片段,不改文件")
mi.add_argument("--force", action="store_true", help="现有配置含注释 / 尾随逗号时,先备份再重写为纯 JSON")
mi.add_argument("--json", action="store_true")
e = sub.add_parser("export", help="把切片渲成可发布成片(9:16 / 烧字幕 / 标题卡)")
e.add_argument("project_id")
+3 -1
View File
@@ -5,10 +5,12 @@ AutoClip MCP server(stdio)——让 Cursor / Claude Code / 任何 MCP 客户
autoclip mcp # 装了包
python -m backend.mcp_server # 仓库内
客户端配置示例(Cursor `~/.cursor/mcp.json` / Claude `claude mcp add`):
客户端配置示例(Cursor `~/.cursor/mcp.json` / Claude `claude mcp add` / opencode):
{ "mcpServers": { "autoclip": { "command": "autoclip", "args": ["mcp"] } } }
或 { "command": "/path/to/autoclip/venv/bin/python", "args": ["-m", "backend.mcp_server"],
"env": { "PYTHONPATH": "/path/to/autoclip" } }
opencode(opencode.json;一条命令:`autoclip mcp install opencode`,见 docs/OPENCODE.md):
{ "mcp": { "autoclip": { "type": "local", "command": ["autoclip", "mcp"], "enabled": true } } }
工具:
clip_video 同步出片(几分钟到几十分钟,带进度通知)
+295
View File
@@ -0,0 +1,295 @@
"""把 AutoClip 接入 opencode CLI(opencode.json 的 mcp 配置)。
opencode(https://opencode.ai)的本地 MCP server 写在配置文件的 mcp 段:
{
"mcp": {
"autoclip": { "type": "local", "command": ["/abs/path/autoclip", "mcp"], "enabled": true }
}
}
- 全局配置:`~/.config/opencode/opencode.json`(`OPENCODE_CONFIG` / `XDG_CONFIG_HOME` 可改路径)
- 项目配置:`<项目>/opencode.json`;opencode 会合并多处配置,项目覆盖全局
- 同时存在 `.json` 与 `.jsonc` 时 opencode 先读 json 再读 jsonc(后者覆盖同名键):
只有 jsonc 时直接写它;两份都有且 jsonc 里已有 mcp 段时拒绝写入(避免“写入成功但实际加载旧配置”)
对应 CLI:`autoclip mcp install opencode`(`--scope project` 写项目配置、`--print` 只打印片段)。
本模块只做配置读写与命令探测,不 import 后端重依赖,方便 CLI 与测试单独使用。
"""
from __future__ import annotations
import json
import os
import shutil
import sys
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
OPENCODE_SCHEMA = "https://opencode.ai/config.json"
DEFAULT_SERVER_NAME = "autoclip"
def detect_server() -> Dict[str, Any]:
"""探测用什么命令启动 MCP server:PATH 里的 autoclip → autoclip-mcp → python -m backend.mcp_server。"""
exe = shutil.which("autoclip")
if exe:
return {"command": [exe, "mcp"], "cwd": None, "environment": None, "kind": "autoclip"}
exe = shutil.which("autoclip-mcp")
if exe:
return {"command": [exe], "cwd": None, "environment": None, "kind": "autoclip-mcp"}
repo_root = Path(__file__).resolve().parents[2]
env = {"PYTHONPATH": str(repo_root)}
cwd = str(repo_root) if (repo_root / "backend" / "mcp_server.py").is_file() else None
return {
"command": [sys.executable or "python", "-m", "backend.mcp_server"],
"cwd": cwd,
"environment": env,
"kind": "module",
}
def opencode_config_path(scope: str = "global", project_dir: Optional[Path] = None) -> Path:
"""目标配置文件:global → ~/.config/opencode/opencode.json;project → <目录>/opencode.json。"""
if scope == "project":
base = Path(project_dir).expanduser() if project_dir else Path.cwd()
return base / "opencode.json"
override = os.environ.get("OPENCODE_CONFIG")
if override:
return Path(override).expanduser()
xdg = os.environ.get("XDG_CONFIG_HOME")
config_home = Path(xdg).expanduser() if xdg else Path.home() / ".config"
return config_home / "opencode" / "opencode.json"
def build_entry(server: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
"""opencode `mcp` 段里的 autoclip 条目(type=local)。"""
server = server or detect_server()
entry: Dict[str, Any] = {"type": "local", "command": list(server["command"]), "enabled": True}
if server.get("cwd"):
entry["cwd"] = server["cwd"]
if server.get("environment"):
entry["environment"] = dict(server["environment"])
return entry
def render_snippet(entry: Dict[str, Any], name: str = DEFAULT_SERVER_NAME) -> str:
"""手动合并进 opencode 配置时用的 JSON 片段。"""
return json.dumps({"mcp": {name: entry}}, ensure_ascii=False, indent=2)
def _strip_comments(text: str) -> str:
"""去掉 // 与 /* */ 注释;字符串(含转义)里的内容不动。"""
out: List[str] = []
i, n = 0, len(text)
in_str = False
while i < n:
ch = text[i]
if in_str:
out.append(ch)
if ch == "\\" and i + 1 < n:
out.append(text[i + 1])
i += 2
continue
if ch == '"':
in_str = False
i += 1
continue
if ch == '"':
in_str = True
out.append(ch)
i += 1
continue
if ch == "/" and i + 1 < n and text[i + 1] == "/":
while i < n and text[i] not in "\r\n":
i += 1
continue
if ch == "/" and i + 1 < n and text[i + 1] == "*":
i += 2
while i + 1 < n and not (text[i] == "*" and text[i + 1] == "/"):
i += 1
i += 2
continue
out.append(ch)
i += 1
return "".join(out)
def _strip_trailing_commas(text: str) -> str:
"""去掉对象 / 数组结尾多余的逗号;字符串里的逗号不动。"""
out: List[str] = []
i, n = 0, len(text)
in_str = False
while i < n:
ch = text[i]
if in_str:
out.append(ch)
if ch == "\\" and i + 1 < n:
out.append(text[i + 1])
i += 2
continue
if ch == '"':
in_str = False
i += 1
continue
if ch == '"':
in_str = True
out.append(ch)
i += 1
continue
if ch == ",":
j = i + 1
while j < n and text[j] in " \t\r\n":
j += 1
if j < n and text[j] in "}]":
i += 1
continue
out.append(ch)
i += 1
return "".join(out)
def parse_config_text(text: str) -> Tuple[Dict[str, Any], bool]:
"""解析 opencode 配置文本,返回 (data, 是否走了 JSONC 宽松模式)。
先按严格 JSON 解析;失败再容忍注释与尾随逗号(opencode 官方支持 JSONC)。
两种都失败时抛 json.JSONDecodeError。
"""
text = text.lstrip("\ufeff") # Windows 编辑器可能带 UTF-8 BOM
try:
data = json.loads(text)
lenient = False
except json.JSONDecodeError:
data = json.loads(_strip_trailing_commas(_strip_comments(text)))
lenient = True
if not isinstance(data, dict):
raise json.JSONDecodeError("opencode 配置顶层必须是 JSON 对象", text, 0)
return data, lenient
def install_opencode(
target: Path,
*,
server: Optional[Dict[str, Any]] = None,
name: str = DEFAULT_SERVER_NAME,
force: bool = False,
discover_jsonc: bool = True,
) -> Dict[str, Any]:
"""把 autoclip 写进 opencode 配置(合并写入:不动其它键,重装也保留同名条目里的自定义字段)。
返回报告:ok / action / path / entry / snippet / backup / warnings / error / hint。
action 取值 created / updated / unchanged / manual;manual = 没有写文件,
需要用户手动合并 snippet(配置解析失败,或带注释又没给 force)。
discover_jsonc=False 时严格写 target,用于 OPENCODE_CONFIG 显式指定的文件。
"""
server = server or detect_server()
entry = build_entry(server)
report: Dict[str, Any] = {
"ok": False,
"action": "manual",
"path": str(target),
"entry": entry,
"snippet": render_snippet(entry, name=name),
"backup": None,
"warnings": [],
"error": None,
"hint": None,
}
# 同时存在 opencode.json / opencode.jsonc 时,opencode 先读 json 再读 jsonc(后者覆盖):
# 只有 jsonc 就写 jsonc;两份都有且 jsonc 里已有 mcp 段时拒绝写入,避免“成功但不生效”。
write_target = target
if discover_jsonc and target.name == "opencode.json":
jsonc_path = target.with_name("opencode.jsonc")
if jsonc_path.is_file():
if not target.is_file():
write_target = jsonc_path
else:
try:
jsonc_data, _ = parse_config_text(jsonc_path.read_text(encoding="utf-8-sig"))
except Exception as e: # noqa: BLE001
report.update(
error=f"同目录的 opencode.jsonc 无法解析:{e}",
hint="opencode.jsonc 覆盖 opencode.json;请先处理它,或手动合并下面的片段",
)
return report
if isinstance(jsonc_data.get("mcp"), dict):
report.update(
error="opencode.json 与 opencode.jsonc 同时存在,且 jsonc 里已有 mcp 段(它覆盖 json),"
"继续写入会出现“成功但实际不生效”",
hint=f"请只保留一份配置,或手动把下面的片段合并进 {jsonc_path.name}",
)
return report
report["path"] = str(write_target)
exists = write_target.is_file()
data: Dict[str, Any] = {}
lenient = False
if exists:
try:
data, lenient = parse_config_text(write_target.read_text(encoding="utf-8-sig"))
except Exception as e: # noqa: BLE001
report.update(error=f"现有配置无法解析:{e}", hint="为免误删,请手动把下面的片段合并进配置")
return report
if lenient and not force:
report.update(
error="现有配置带注释 / 尾随逗号(JSONC),重写会丢注释,没有动它",
hint=f"确认可接受时重跑并加 --force(会先备份为 {write_target.name}.bak)",
)
return report
if write_target is not target:
report["warnings"].append("未找到 opencode.json,本次写入 opencode.jsonc(它优先级更高)")
mcp = data.get("mcp")
if mcp is not None and not isinstance(mcp, dict):
report.update(action="error", error="配置里的 mcp 字段不是对象,未改动", hint="请手动清理 mcp 字段后重试")
return report
mcp = dict(mcp or {})
# 重装只更新安装器负责的字段(type / command / 模块回退的 cwd 与 PYTHONPATH),
# 保留同名条目里用户自己的 environment、timeout、enabled 等设置。
existing_entry = mcp.get(name)
merged = entry
if isinstance(existing_entry, dict):
merged = dict(existing_entry)
merged["type"] = entry["type"]
merged["command"] = entry["command"]
if server.get("kind") == "module":
env = dict(merged.get("environment") or {})
for k, v in (entry.get("environment") or {}).items():
if k == "PYTHONPATH":
# 当前安装路径优先,同时保留用户的额外模块路径;重复安装不再追加。
paths = env[k].split(os.pathsep) if env.get(k) else []
env[k] = os.pathsep.join([v, *(p for p in paths if p != v)])
else:
env.setdefault(k, v)
if env:
merged["environment"] = env
if entry.get("cwd"):
merged["cwd"] = entry["cwd"]
report["entry"] = merged
report["snippet"] = render_snippet(merged, name=name)
if exists and existing_entry == merged:
report.update(ok=True, action="unchanged")
return report
data.setdefault("$schema", OPENCODE_SCHEMA)
mcp[name] = merged
data["mcp"] = mcp
backup: Optional[Path] = None
if exists:
backup = write_target.with_name(write_target.name + ".bak")
shutil.copy2(write_target, backup)
write_target.parent.mkdir(parents=True, exist_ok=True)
write_target.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
if lenient:
report["warnings"].append(f"原配置的注释 / 尾随逗号未保留,已重写为纯 JSON(备份:{backup})")
if isinstance(existing_entry, dict) and existing_entry.get("enabled") is False:
report["warnings"].append("原有条目 enabled=false,opencode 不会加载它;本次未改动该字段")
if server.get("kind") == "module":
report["warnings"].append("PATH 里没有 autoclip 命令,已回退到 python -m backend.mcp_server(依赖 cwd / PYTHONPATH)")
report.update(ok=True, action="updated" if exists else "created", backup=str(backup) if backup else None)
return report
+324
View File
@@ -0,0 +1,324 @@
"""opencode 接入:配置路径 / 合并写入 / JSONC 与失败保护。全部落在 tmp_path,不碰真实用户配置。"""
import json
import os
import subprocess
import sys
import pytest
from backend.services import opencode_setup
SERVER = {"command": ["/opt/venv/bin/autoclip", "mcp"], "cwd": None, "environment": None, "kind": "autoclip"}
def test_global_path_respects_xdg_and_opencode_config(tmp_path, monkeypatch):
monkeypatch.delenv("OPENCODE_CONFIG", raising=False)
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path))
assert opencode_setup.opencode_config_path("global") == tmp_path / "opencode" / "opencode.json"
custom = tmp_path / "custom-opencode.json"
monkeypatch.setenv("OPENCODE_CONFIG", str(custom))
assert opencode_setup.opencode_config_path("global") == custom
def test_creates_new_config(tmp_path):
target = tmp_path / "opencode.json"
report = opencode_setup.install_opencode(target, server=SERVER)
assert report["ok"] is True
assert report["action"] == "created"
data = json.loads(target.read_text(encoding="utf-8"))
assert data["$schema"] == opencode_setup.OPENCODE_SCHEMA
assert data["mcp"]["autoclip"] == {
"type": "local",
"command": ["/opt/venv/bin/autoclip", "mcp"],
"enabled": True,
}
def test_merge_preserves_other_keys_and_backs_up(tmp_path):
target = tmp_path / "opencode.json"
original = {
"$schema": opencode_setup.OPENCODE_SCHEMA,
"model": "anthropic/claude-sonnet-4-5",
"mcp": {"other": {"type": "remote", "url": "https://example.com/mcp"}},
}
target.write_text(json.dumps(original, indent=2), encoding="utf-8")
report = opencode_setup.install_opencode(target, server=SERVER)
assert report["ok"] is True and report["action"] == "updated"
data = json.loads(target.read_text(encoding="utf-8"))
assert data["model"] == "anthropic/claude-sonnet-4-5"
assert data["mcp"]["other"] == original["mcp"]["other"]
assert data["mcp"]["autoclip"]["command"] == SERVER["command"]
assert json.loads((tmp_path / "opencode.json.bak").read_text(encoding="utf-8")) == original
def test_second_run_unchanged(tmp_path):
target = tmp_path / "opencode.json"
assert opencode_setup.install_opencode(target, server=SERVER)["action"] == "created"
report = opencode_setup.install_opencode(target, server=SERVER)
assert report["ok"] is True and report["action"] == "unchanged"
def test_jsonc_config_not_touched_without_force(tmp_path):
target = tmp_path / "opencode.json"
original = '{\n // opencode 支持注释\n "mcp": {},\n}\n'
target.write_text(original, encoding="utf-8")
report = opencode_setup.install_opencode(target, server=SERVER)
assert report["ok"] is False and report["action"] == "manual"
assert target.read_text(encoding="utf-8") == original
report = opencode_setup.install_opencode(target, server=SERVER, force=True)
assert report["ok"] is True and report["action"] == "updated"
data = json.loads(target.read_text(encoding="utf-8"))
assert data["mcp"]["autoclip"]["enabled"] is True
assert (tmp_path / "opencode.json.bak").is_file()
assert any("注释" in w for w in report["warnings"])
def test_broken_config_never_overwritten(tmp_path):
target = tmp_path / "opencode.json"
target.write_text("{不是 JSON", encoding="utf-8")
report = opencode_setup.install_opencode(target, server=SERVER, force=True)
assert report["ok"] is False and report["action"] == "manual"
assert target.read_text(encoding="utf-8") == "{不是 JSON"
assert not (tmp_path / "opencode.json.bak").exists()
def test_project_scope_and_module_fallback(tmp_path, monkeypatch):
monkeypatch.setattr(opencode_setup.shutil, "which", lambda name: None)
server = opencode_setup.detect_server()
assert server["kind"] == "module"
assert server["command"][1:] == ["-m", "backend.mcp_server"]
assert server["environment"]["PYTHONPATH"]
target = opencode_setup.opencode_config_path("project", tmp_path)
assert target == tmp_path / "opencode.json"
report = opencode_setup.install_opencode(target, server=server)
assert report["ok"] is True
entry = json.loads(target.read_text(encoding="utf-8"))["mcp"]["autoclip"]
assert entry["cwd"]
assert entry["environment"]["PYTHONPATH"]
assert any("回退" in w for w in report["warnings"])
def test_reinstall_preserves_custom_entry_fields(tmp_path):
"""[review P2] 重装只更新安装器负责的字段,用户自定义的 environment / timeout 不能丢。"""
target = tmp_path / "opencode.json"
existing = {
"$schema": opencode_setup.OPENCODE_SCHEMA,
"mcp": {
"autoclip": {
"type": "local",
"command": ["/old/venv/bin/autoclip", "mcp"],
"enabled": True,
"environment": {"AUTOCLIP_DATA_DIR": "D:/clips", "OPENAI_API_KEY": "sk-x"},
"timeout": 30000,
}
},
}
target.write_text(json.dumps(existing), encoding="utf-8")
report = opencode_setup.install_opencode(target, server=SERVER)
assert report["ok"] is True and report["action"] == "updated"
entry = json.loads(target.read_text(encoding="utf-8"))["mcp"]["autoclip"]
assert entry["command"] == SERVER["command"] # 命令更新
assert entry["timeout"] == 30000 # 用户字段保留
assert entry["environment"] == {"AUTOCLIP_DATA_DIR": "D:/clips", "OPENAI_API_KEY": "sk-x"}
def test_module_fallback_merges_environment(tmp_path, monkeypatch):
"""[review P2] 模块回退写 PYTHONPATH 时要合并 environment,而不是整体覆盖。"""
monkeypatch.setattr(opencode_setup.shutil, "which", lambda name: None)
server = opencode_setup.detect_server()
target = tmp_path / "opencode.json"
existing = {"mcp": {"autoclip": {
"type": "local", "command": ["python", "-m", "backend.mcp_server"],
"enabled": True, "environment": {"OPENAI_API_KEY": "sk-x"},
}}}
target.write_text(json.dumps(existing), encoding="utf-8")
report = opencode_setup.install_opencode(target, server=server)
assert report["ok"] is True
env = json.loads(target.read_text(encoding="utf-8"))["mcp"]["autoclip"]["environment"]
assert env["OPENAI_API_KEY"] == "sk-x" # 用户环境变量保留
assert env["PYTHONPATH"] # 回退所需的 PYTHONPATH 合并进来
def test_disabled_entry_stays_disabled_with_warning(tmp_path):
target = tmp_path / "opencode.json"
target.write_text(json.dumps({"mcp": {"autoclip": {
"type": "local", "command": ["/old/venv/bin/autoclip", "mcp"], "enabled": False,
}}}), encoding="utf-8")
report = opencode_setup.install_opencode(target, server=SERVER)
entry = json.loads(target.read_text(encoding="utf-8"))["mcp"]["autoclip"]
assert entry["enabled"] is False # 不静默改用户的 enabled
assert any("enabled=false" in w for w in report["warnings"])
def test_existing_jsonc_is_used_when_json_missing(tmp_path):
"""[review P2] 只有 opencode.jsonc 时直接写它;注释沿用非 --force 保护,不再多建一份不生效的 json。"""
jsonc = tmp_path / "opencode.jsonc"
jsonc.write_text('{\n // opencode 支持注释\n "mcp": {}\n}\n', encoding="utf-8")
report = opencode_setup.install_opencode(tmp_path / "opencode.json", server=SERVER)
assert report["ok"] is False and report["action"] == "manual" # JSONC 默认不动
report = opencode_setup.install_opencode(tmp_path / "opencode.json", server=SERVER, force=True)
assert report["ok"] is True and report["action"] == "updated"
assert report["path"].endswith("opencode.jsonc")
data = json.loads(jsonc.read_text(encoding="utf-8"))
assert data["mcp"]["autoclip"]["command"] == SERVER["command"]
assert (tmp_path / "opencode.jsonc.bak").is_file()
assert not (tmp_path / "opencode.json").exists()
def test_plain_jsonc_only_is_targeted(tmp_path):
jsonc = tmp_path / "opencode.jsonc"
jsonc.write_text('{"model": "anthropic/claude-sonnet-4-5"}', encoding="utf-8")
report = opencode_setup.install_opencode(tmp_path / "opencode.json", server=SERVER)
assert report["ok"] is True and report["action"] == "updated"
assert report["path"].endswith("opencode.jsonc")
assert json.loads(jsonc.read_text(encoding="utf-8"))["mcp"]["autoclip"]["enabled"] is True
assert not (tmp_path / "opencode.json").exists()
def test_conflicting_jsonc_with_mcp_blocks_write(tmp_path):
"""[review P2] 两份配置都有且 jsonc 含 mcp 段时必须明确拒绝,不能报告成功但加载旧配置。"""
json_file = tmp_path / "opencode.json"
original = {"$schema": opencode_setup.OPENCODE_SCHEMA, "model": "x"}
json_file.write_text(json.dumps(original), encoding="utf-8")
(tmp_path / "opencode.jsonc").write_text(
'{"mcp": {"autoclip": {"type": "local", "command": ["old"], "enabled": false}}}', encoding="utf-8")
report = opencode_setup.install_opencode(json_file, server=SERVER)
assert report["ok"] is False and report["action"] == "manual"
assert "jsonc" in report["error"]
assert json.loads(json_file.read_text(encoding="utf-8")) == original # json 未被写
assert report["snippet"]
def test_both_exist_jsonc_without_mcp_writes_json(tmp_path):
json_file = tmp_path / "opencode.json"
json_file.write_text(json.dumps({"model": "x"}), encoding="utf-8")
jsonc = tmp_path / "opencode.jsonc"
jsonc.write_text('{"theme": "dark"}', encoding="utf-8")
report = opencode_setup.install_opencode(json_file, server=SERVER)
assert report["ok"] is True and report["action"] == "updated"
assert report["path"].endswith("opencode.json")
assert json.loads(json_file.read_text(encoding="utf-8"))["mcp"]["autoclip"]["enabled"] is True
assert json.loads(jsonc.read_text(encoding="utf-8")) == {"theme": "dark"} # jsonc 未被改动
def test_utf8_bom_jsonc_conflict_still_detected(tmp_path):
"""Windows 编辑器常带 UTF-8 BOM,解析要容错,冲突判定不能因此失效。"""
json_file = tmp_path / "opencode.json"
json_file.write_text(json.dumps({"model": "x"}), encoding="utf-8")
(tmp_path / "opencode.jsonc").write_text(
'{"mcp": {"autoclip": {"type": "local", "command": ["old"], "enabled": false}}}', encoding="utf-8-sig")
report = opencode_setup.install_opencode(json_file, server=SERVER)
assert report["ok"] is False and report["action"] == "manual"
assert "jsonc" in report["error"]
def test_utf8_bom_json_can_be_merged(tmp_path):
target = tmp_path / "opencode.json"
target.write_text(json.dumps({"model": "x"}), encoding="utf-8-sig")
report = opencode_setup.install_opencode(target, server=SERVER)
assert report["ok"] is True and report["action"] == "updated"
assert json.loads(target.read_text(encoding="utf-8"))["mcp"]["autoclip"]["enabled"] is True
@pytest.mark.parametrize("config_exists", [False, True])
def test_cli_explicit_config_does_not_discover_sibling_jsonc(tmp_path, monkeypatch, capsys, config_exists):
from backend import cli
target = tmp_path / "custom" / "opencode.json"
target.parent.mkdir()
if config_exists:
target.write_text(json.dumps({"model": "original"}), encoding="utf-8")
sibling = target.with_suffix(".jsonc")
original_sibling = '{"mcp": {"other": {"type": "remote", "url": "https://example.com/mcp"}}}'
sibling.write_text(original_sibling, encoding="utf-8")
monkeypatch.setenv("OPENCODE_CONFIG", str(target))
monkeypatch.setattr(opencode_setup, "detect_server", lambda: SERVER)
args = cli.build_parser().parse_args(["mcp", "install", "opencode", "--json"])
assert cli.cmd_mcp_install(args) == 0
report = json.loads(capsys.readouterr().out)
assert report["path"] == str(target)
assert report["action"] == ("updated" if config_exists else "created")
data = json.loads(target.read_text(encoding="utf-8"))
assert data["mcp"]["autoclip"]["command"] == SERVER["command"]
if config_exists:
assert data["model"] == "original"
assert sibling.read_text(encoding="utf-8") == original_sibling
def test_cli_project_scope_discovers_jsonc_with_global_override(tmp_path, monkeypatch, capsys):
from backend import cli
global_config = tmp_path / "global" / "opencode.json"
monkeypatch.setenv("OPENCODE_CONFIG", str(global_config))
monkeypatch.setattr(opencode_setup, "detect_server", lambda: SERVER)
project = tmp_path / "project"
project.mkdir()
jsonc = project / "opencode.jsonc"
jsonc.write_text('{"mcp": {}}', encoding="utf-8")
args = cli.build_parser().parse_args([
"mcp", "install", "opencode", "--scope", "project", "--dir", str(project), "--json",
])
assert cli.cmd_mcp_install(args) == 0
report = json.loads(capsys.readouterr().out)
assert report["path"] == str(jsonc)
assert json.loads(jsonc.read_text(encoding="utf-8"))["mcp"]["autoclip"]["command"] == SERVER["command"]
assert not (project / "opencode.json").exists()
assert not global_config.exists()
def test_module_reinstall_refreshes_paths_after_checkout_move(tmp_path):
old_repo = tmp_path / "old-checkout"
backend = old_repo / "backend"
backend.mkdir(parents=True)
(backend / "__init__.py").write_text("", encoding="utf-8")
(backend / "mcp_server.py").write_text('print("current-checkout")\n', encoding="utf-8")
extra_modules = tmp_path / "custom-modules"
extra_modules.mkdir()
target = tmp_path / "opencode.json"
old_server = {
"kind": "module", "command": [sys.executable, "-m", "backend.mcp_server"],
"cwd": str(old_repo), "environment": {"PYTHONPATH": str(old_repo)},
}
assert opencode_setup.install_opencode(target, server=old_server)["ok"] is True
data = json.loads(target.read_text(encoding="utf-8"))
data["mcp"]["autoclip"]["environment"].update({
"PYTHONPATH": os.pathsep.join([str(old_repo), str(extra_modules)]),
"AUTOCLIP_DATA_DIR": str(tmp_path / "clips"),
})
target.write_text(json.dumps(data), encoding="utf-8")
new_repo = tmp_path / "new-checkout"
old_repo.rename(new_repo)
new_server = dict(old_server, cwd=str(new_repo), environment={"PYTHONPATH": str(new_repo)})
report = opencode_setup.install_opencode(target, server=new_server)
assert report["ok"] is True and report["action"] == "updated"
entry = json.loads(target.read_text(encoding="utf-8"))["mcp"]["autoclip"]
assert entry["cwd"] == str(new_repo)
paths = entry["environment"]["PYTHONPATH"].split(os.pathsep)
assert paths[0] == str(new_repo)
assert str(extra_modules) in paths
assert entry["environment"]["AUTOCLIP_DATA_DIR"] == str(tmp_path / "clips")
process = subprocess.run(
entry["command"], cwd=entry["cwd"], env=dict(os.environ, **entry["environment"]),
capture_output=True, text=True, timeout=10,
)
assert process.returncode == 0, process.stderr
assert process.stdout.strip() == "current-checkout"
assert opencode_setup.install_opencode(target, server=new_server)["action"] == "unchanged"
+16 -2
View File
@@ -5,7 +5,7 @@
| 形态 | 一句话 | 入口 |
|---|---|---|
| CLI | `autoclip run video.mp4 --provider ollama` 一条命令出片 | `backend/cli.py` |
| MCP server | 让 Cursor / Claude Code / 任何 MCP 客户端直接调 AutoClip | `backend/mcp_server.py` |
| MCP server | 让 opencode / Cursor / Claude Code / 任何 MCP 客户端直接调 AutoClip | `backend/mcp_server.py` |
| 本地模型预设 | 设置页 / CLI 直接选 Ollama、LM Studio,不用填 key | `backend/core/local_presets.py` |
共享逻辑在 `backend/services/local_runner.py`:不起 FastAPI / Celery,在当前进程里跑 `SimplePipelineAdapter`,
@@ -114,6 +114,20 @@ claude mcp add autoclip -- /path/to/autoclip/venv/bin/autoclip mcp
没装包时把 `command` 换成 `/path/to/autoclip/venv/bin/python`,`args` 为 `["-m", "backend.mcp_server"]`,
并加 `"env": {"PYTHONPATH": "/path/to/autoclip"}`。
**OpenCode**(`~/.config/opencode/opencode.json`,或项目里的 `opencode.json`;带注释的 `.jsonc` 也支持):
```json
{
"mcp": {
"autoclip": { "type": "local", "command": ["/path/to/autoclip/venv/bin/autoclip", "mcp"], "enabled": true }
}
}
```
也可以让 AutoClip 自己写:`autoclip mcp install opencode`(`--scope project --dir .` 写项目配置、`--print` 只打印片段、
`--force` 在现有配置带注释时先备份再重写)。重装会保留同名条目里的 `environment` / `timeout` 等自定义字段;存在
`opencode.jsonc` 时优先写它,两份配置冲突会在写入前直接拒绝。接入后在 opencode 里给一个视频路径即可出片,详见 `docs/OPENCODE.md`。
工具:
| 工具 | 说明 |
@@ -135,7 +149,7 @@ claude mcp add autoclip -- /path/to/autoclip/venv/bin/autoclip mcp
- 任务状态在内存里;server 重启后 `get_job_status` 会退回从磁盘读项目结果。
**Agent skill**:`skills/autoclip/SKILL.md` 教 agent 何时用哪个工具、参数怎么选、结果怎么呈现、切片为 0 怎么办。
复制到 `~/.cursor/skills/autoclip/` 或 `~/.claude/skills/autoclip/` 即生效。
复制到 `~/.cursor/skills/autoclip/`、`~/.claude/skills/autoclip/` 或 `~/.config/opencode/skills/autoclip/` 即生效。
---
+95
View File
@@ -0,0 +1,95 @@
# Connect AutoClip to opencode CLI (MCP)
[opencode](https://opencode.ai) is a terminal AI agent. Register AutoClip as a local MCP server and you can
point opencode at a video path — it will call AutoClip to clip highlights, export vertical videos, and publish.
| Goal | How |
|---|---|
| One-command setup (recommended) | `autoclip mcp install opencode` |
| Project-only setup | `autoclip mcp install opencode --scope project --dir <dir>` |
| Print the snippet only | `autoclip mcp install opencode --print` |
| Rewrite a commented (JSONC) config | add `--force` (backup written first) |
## 1. Prerequisites
- AutoClip CLI installed (`pip install -r requirements.txt && pip install -e .`) — see the CLI / MCP guide;
running from the repo (`python -m backend.mcp_server`) also works
- `ffmpeg` on PATH and a working model: `autoclip doctor`
- opencode installed (`opencode --version`)
## 2. One-command setup
```bash
autoclip mcp install opencode
# ✓ wrote ~/.config/opencode/opencode.json (Windows: %USERPROFILE%\.config\opencode\opencode.json)
```
The command detects the launch command (`autoclip` → `autoclip-mcp` → `python -m backend.mcp_server`),
merges `mcp.autoclip` into your config without touching other keys, and backs the old file up as
`opencode.json.bak`. Re-running keeps your custom fields on an existing `autoclip` entry while updating
`type` / `command`. The module fallback refreshes `cwd` and prepends the current installation to `PYTHONPATH`,
preserving other module paths, environment variables, `timeout`, and `enabled`.
If `opencode.jsonc` exists it becomes the target; when both `opencode.json` and `opencode.jsonc` exist
and the jsonc already defines `mcp`, the installer refuses with an explicit message instead of writing
a config that would not take effect. Commented (JSONC) configs are left untouched unless you pass `--force`.
| Option | Effect |
|---|---|
| `--scope global` (default) | `~/.config/opencode/opencode.json` |
| `--scope project --dir <dir>` | `<dir>/opencode.json` (project-only) |
| `--print` / `--json` | print the snippet / machine-readable report |
`OPENCODE_CONFIG` selects the exact global file to write, without switching to a sibling `.jsonc`;
`XDG_CONFIG_HOME` is honored.
## 3. Manual config
`opencode.json` (or `opencode.jsonc`):
```json
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"autoclip": {
"type": "local",
"command": ["C:\\Users\\<you>\\autoclip\\venv\\Scripts\\autoclip.exe", "mcp"],
"enabled": true
}
}
}
```
- macOS / Linux: `"command": ["/path/to/autoclip/venv/bin/autoclip", "mcp"]`
- Running from the repo without installing:
`"command": ["<python>", "-m", "backend.mcp_server"], "cwd": "<repo>", "environment": {"PYTHONPATH": "<repo>"}`
opencode merges global and project configs; a project entry with the same name overrides the global one.
## 4. Use it
Restart opencode (or start a new session) so it loads the MCP server, then just ask:
```text
Clip D:\Videos\lecture.mp4
Clip C:\Users\me\Downloads\podcast.mp4 using ollama
Export clip 2 of that project as Shorts
```
opencode calls `check_environment` and `clip_video` (or `start_clip_job` + `get_job_status` for long videos)
and reports clips by score, time range, and title. Output shares the same data directory as the desktop app.
## 5. Agent skill (optional)
Copy `skills/autoclip/` to `~/.config/opencode/skills/autoclip/` (or `.opencode/skills/` inside a project).
opencode also reads `~/.claude/skills/`, so Claude Code installs are picked up automatically.
## 6. Troubleshooting
| Symptom | Fix |
|---|---|
| No `autoclip` tools in opencode | Run with `--print`, verify the target path, keep only one `opencode.json` / `.jsonc`, start a new session |
| Tools listed but calls fail | Run `autoclip doctor` (ffmpeg / Whisper / model key) |
| Zero clips | Retry with `min_score=0.5`; pure music / no dialogue has no highlights |
| First run is slow | Local Whisper transcription downloads a model; pass an SRT to skip it |
Links: [CLI / MCP guide (Chinese)](CLI_AND_MCP.md) · [opencode MCP docs](https://opencode.ai/docs/mcp-servers/) · [opencode skills docs](https://opencode.ai/docs/skills/)
+103
View File
@@ -0,0 +1,103 @@
# 接入 opencode CLI(AutoClip MCP)
[opencode](https://opencode.ai) 是终端里的 AI agent。把 AutoClip 作为本地 MCP server 接进去之后,
你在 opencode 里给出一个视频路径,它就会调 AutoClip 出片——切高光、看结果、导 9:16、发平台,
不用自己拼 `autoclip` 参数。
| 你想做的 | 怎么做 |
|---|---|
| 一条命令接入(推荐) | `autoclip mcp install opencode` |
| 写进某个项目 | `autoclip mcp install opencode --scope project --dir <项目目录>` |
| 只看配置片段、不改文件 | `autoclip mcp install opencode --print` |
| 现有配置带注释,确认要重写 | 加 `--force`(先备份为 `opencode.json.bak`) |
## 1. 前置条件
- AutoClip CLI 装好(MCP server 与 CLI 同源):`pip install -r requirements.txt && pip install -e .`,
见 [CLI / MCP 指南](CLI_AND_MCP.md) 第 1 节;不装包也能用仓库里的 `python -m backend.mcp_server`
- `ffmpeg` 在 PATH、模型能连上:`autoclip doctor` 检查(云端 key,或本地 Ollama / LM Studio)
- opencode 已安装(`opencode --version`)
## 2. 一键接入
```bash
autoclip mcp install opencode
# ✓ 已写入 ~/.config/opencode/opencode.json(Windows: %USERPROFILE%\.config\opencode\opencode.json)
```
命令做的事:
- 按 `autoclip` → `autoclip-mcp` → `python -m backend.mcp_server` 的顺序探测启动命令(结果在输出的 `entry` 里)
- 把 `mcp.autoclip` **合并**写进配置(`type: local`),其它键原样保留;文件已存在时先备份 `opencode.json.bak`
- 重装**不会丢同名条目里的自定义字段**:更新 `type` / `command`;模块回退时刷新 `cwd`,
并把当前安装路径放到 `PYTHONPATH` 最前面,保留其余模块路径、环境变量、`timeout` 和 `enabled=false`(后者会给出提示)
- 优先识别已有配置文件:只有 `opencode.jsonc` 时写它;`opencode.json` 和 `opencode.jsonc` 同时存在且 jsonc 里已有 `mcp` 段时,
**写入前直接拒绝**(jsonc 覆盖 json,避免“写入成功但实际加载旧配置”)
- 配置带注释 / 尾随逗号(JSONC,opencode 支持)时默认**不动文件**、只打印片段;加 `--force` 才会先备份再重写成纯 JSON
| 参数 | 作用 |
|---|---|
| `--scope global`(默认) | 全局配置 `~/.config/opencode/opencode.json` |
| `--scope project --dir <目录>` | 只在这个项目生效:`<目录>/opencode.json` |
| `--name <名字>` | 配置里的服务名(默认 `autoclip`;opencode 里工具会带 `autoclip_` 前缀) |
| `--print` / `--json` | 只打印片段 / 输出机器可读报告 |
环境变量:设了 `OPENCODE_CONFIG` 就严格写它指向的文件,不改写同目录的 `.jsonc`;设了 `XDG_CONFIG_HOME` 用 `$XDG_CONFIG_HOME/opencode/opencode.json`。
## 3. 手动配置
`opencode.json`(或 `opencode.jsonc`):
```json
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"autoclip": {
"type": "local",
"command": ["C:\\Users\\<你>\\autoclip\\venv\\Scripts\\autoclip.exe", "mcp"],
"enabled": true
}
}
}
```
- macOS / Linux:`"command": ["/path/to/autoclip/venv/bin/autoclip", "mcp"]`
- 没装包(在仓库里跑):`"command": ["<python 绝对路径>", "-m", "backend.mcp_server"], "cwd": "<仓库根目录>", "environment": {"PYTHONPATH": "<仓库根目录>"}`
opencode 会合并多处配置(全局 + 项目),项目里的同名 server 覆盖全局。
## 4. 在 opencode 里出片
重启 opencode(或新开会话)让它加载 MCP,然后直接说:
```text
把 D:\Videos\lecture.mp4 切片 # 用桌面应用里配好的模型
把 C:\Users\me\Downloads\podcast.mp4 切片,用 ollama # 本地模型,不用 key
刚才那个项目第 2 条切片导成 Shorts
```
opencode 会依次调 `check_environment`、`clip_video`(长视频走 `start_clip_job` + `get_job_status` 轮询),
按 **评分 · 时间段 · 标题** 把结果整理给你。产物和桌面应用共用一个数据目录,桌面端首页能直接看到。
## 5. Agent skill(可选,建议装)
`skills/autoclip/SKILL.md` 教 agent 何时用哪个工具、`min_score` 怎么调、结果怎么呈现:
```bash
cp -r skills/autoclip ~/.config/opencode/skills/autoclip # 全局(Windows 放 %USERPROFILE%\.config\opencode\skills\)
```
只在一个项目里生效,就放 `.opencode/skills/autoclip/`。opencode 也会读 `~/.claude/skills/`,给 Claude Code 装过的无需重复。
## 6. 排错
| 现象 | 处理 |
|---|---|
| opencode 里没有 `autoclip` 工具 | 用 `--print` 确认目标文件路径;同目录只留一份 `opencode.json` / `.jsonc`;新开会话再试 |
| 报「jsonc 里已有 mcp 段」 | `opencode.jsonc` 覆盖 `opencode.json`:只保留一份配置,或手动把片段合并进 jsonc |
| 工具能列出但一调就报错 | 先在终端 `autoclip doctor`:多半是 ffmpeg 不在 PATH、Whisper 没装(传 `srt_path` 可绕过)、模型没 key |
| 切片数为 0 | 让 opencode 用 `min_score=0.5` 重试;纯音乐 / 无对话的视频本身不适合切高光 |
| 首次出片特别慢 | 无字幕时本地 Whisper 转写,首次要下模型;已有 SRT 就传进去,或改用 `ollama` |
| MCP 协议被日志污染 | AutoClip 已把 stdout 留给协议(print 都进 stderr);自建包装脚本时别往 stdout 打日志 |
相关:[CLI / MCP 全量说明](CLI_AND_MCP.md) · [opencode MCP 文档](https://opencode.ai/docs/mcp-servers/) · [opencode skills 文档](https://opencode.ai/docs/skills/)
+1
View File
@@ -24,6 +24,7 @@
- [源码 Web 服务启动](../STARTUP_GUIDE.md)
- [CLI、MCP 与本地模型](CLI_AND_MCP.md)(中文)
- [opencode CLI 接入(MCP 一键注册)](OPENCODE.md)(中文 · [English](OPENCODE.en.md))
- [AI 模型与本地转写配置](AI_MODEL_CONFIGURATION.md)(中文)
- [贡献指南](../CONTRIBUTING.md)(中文)
- [构建指南](../BUILD_GUIDE.md)(中文)